Deploy a remote, stateless MCP server on Modal with FastMCP
This example demonstrates how to deploy a simple MCP server on Modal.
The server provides a tool to get the current date and time in a given timezone. It is a stateless MCP server, meaning that it does not store any state between requests, which is important for mapping onto Modal’s serverless Functions. It uses the “streamable HTTP” transport type.
Building the MCP server
First, we define our dependencies.
We use the FastMCP library to create the MCP server. We wrap with a FastAPI server to expose it to the Internet.
import modal
app = modal.App("example-mcp-server-stateless")
image = modal.Image.debian_slim(python_version="3.12").uv_pip_install(
"fastapi==0.115.14",
"fastmcp==2.10.6",
"pydantic==2.11.10",
)Next, we create the MCP server itself using FastMCP and add a tool to it that allows LLMs to get the current date and time in a given timezone.
def make_mcp_server():
from fastmcp import FastMCP
mcp = FastMCP("Date and Time MCP Server")
@mcp.tool()
async def current_date_and_time(timezone: str = "UTC") -> str:
"""Get the current date and time.
Args:
timezone: The timezone to get the date and time in (optional). Defaults to UTC.
Returns:
The current date and time in the given timezone, in ISO 8601 format.
"""
from datetime import datetime
from zoneinfo import ZoneInfo
try:
tz = ZoneInfo(timezone)
except Exception:
raise ValueError(
f"Invalid timezone '{timezone}'. Please use a valid timezone like 'UTC', "
"'America/New_York', or 'Europe/Stockholm'."
)
return datetime.now(tz).isoformat()
return mcpWe then use FastMCP to create a Starlette app with streamable-http as transport
type, and set stateless_http=True to make it stateless.
This will be mounted by the FastAPI app, which we deploy as a Modal web endpoint using the asgi_app decorator:
@app.function(image=image)
@modal.asgi_app()
def web():
"""ASGI web endpoint for the MCP server"""
from fastapi import FastAPI
mcp = make_mcp_server()
mcp_app = mcp.http_app(transport="streamable-http", stateless_http=True)
fastapi_app = FastAPI(lifespan=mcp_app.router.lifespan_context)
fastapi_app.mount("/", mcp_app, "mcp")
return fastapi_appAnd we’re done!
Testing the MCP server
Now you can serve the MCP server by running:
modal serve mcp_server_stateless.pyThen open the MCP inspector:
npx @modelcontextprotocol/inspectorEnter the URL of the MCP server that was printed by the modal serve command above,
suffixed with /mcp/ (so for example https://modal-labs-examples--datetime-mcp-server-web-dev.modal.run/mcp/). Also
make sure to select “Streamable HTTP” as the “Transport Type”.
After connecting and clicking “List Tools” in the “Tools” tab you should see your current_date_and_time tool listed, and if you “Run Tool” it should give you the
current date and time in UTC!
To automatically test the MCP server, we spin up a client and have it list the tools.
@app.function(image=image)
async def test_tool(tool_name: str | None = None):
from fastmcp import Client
from fastmcp.client.transports import StreamableHttpTransport
if tool_name is None:
tool_name = "current_date_and_time"
transport = StreamableHttpTransport(url=f"{web.get_web_url()}/mcp/")
client = Client(transport)
async with client:
tools = await client.list_tools()
for tool in tools:
print(tool)
if tool.name == tool_name:
result = await client.call_tool(tool_name)
print(result.data)
return
raise Exception(f"could not find tool {tool_name}")This test is executed by running the script with modal run:
modal run mcp_server_stateless::test_toolDeploying the MCP server
modal serve creates an ephemeral, hot-reloading server,
which is useful for testing and development.
When it’s time to move to production, you can deploy the server with
modal deploy mcp_server_stateless