Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 41 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ Run from the repository root:
from mcp.server.fastmcp import FastMCP

# Create an MCP server
mcp = FastMCP("Demo", json_response=True)
mcp = FastMCP("Demo")


# Add an addition tool
Expand Down Expand Up @@ -178,7 +178,7 @@ def greet_user(name: str, style: str = "friendly") -> str:

# Run with streamable HTTP transport
if __name__ == "__main__":
mcp.run(transport="streamable-http")
mcp.run(transport="streamable-http", json_response=True)
```

_Full example: [examples/snippets/servers/fastmcp_quickstart.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/fastmcp_quickstart.py)_
Expand Down Expand Up @@ -1026,7 +1026,6 @@ class SimpleTokenVerifier(TokenVerifier):
# Create FastMCP instance as a Resource Server
mcp = FastMCP(
"Weather Service",
json_response=True,
# Token verifier for authentication
token_verifier=SimpleTokenVerifier(),
# Auth settings for RFC 9728 Protected Resource Metadata
Expand All @@ -1050,7 +1049,7 @@ async def get_weather(city: str = "London") -> dict[str, str]:


if __name__ == "__main__":
mcp.run(transport="streamable-http")
mcp.run(transport="streamable-http", json_response=True)
```

_Full example: [examples/snippets/servers/oauth_server.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/oauth_server.py)_
Expand Down Expand Up @@ -1253,15 +1252,7 @@ Run from the repository root:

from mcp.server.fastmcp import FastMCP

# Stateless server with JSON responses (recommended)
mcp = FastMCP("StatelessServer", stateless_http=True, json_response=True)

# Other configuration options:
# Stateless server with SSE streaming responses
# mcp = FastMCP("StatelessServer", stateless_http=True)

# Stateful server with session persistence
# mcp = FastMCP("StatefulServer")
mcp = FastMCP("StatelessServer")


# Add a simple tool to demonstrate the server
Expand All @@ -1272,8 +1263,17 @@ def greet(name: str = "World") -> str:


# Run server with streamable_http transport
# Transport-specific options (stateless_http, json_response) are passed to run()
if __name__ == "__main__":
mcp.run(transport="streamable-http")
# Stateless server with JSON responses (recommended)
mcp.run(transport="streamable-http", stateless_http=True, json_response=True)

# Other configuration options:
# Stateless server with SSE streaming responses
# mcp.run(transport="streamable-http", stateless_http=True)

# Stateful server with session persistence
# mcp.run(transport="streamable-http")
```

_Full example: [examples/snippets/servers/streamable_config.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_config.py)_
Expand All @@ -1296,7 +1296,7 @@ from starlette.routing import Mount
from mcp.server.fastmcp import FastMCP

# Create the Echo server
echo_mcp = FastMCP(name="EchoServer", stateless_http=True, json_response=True)
echo_mcp = FastMCP(name="EchoServer")


@echo_mcp.tool()
Expand All @@ -1306,7 +1306,7 @@ def echo(message: str) -> str:


# Create the Math server
math_mcp = FastMCP(name="MathServer", stateless_http=True, json_response=True)
math_mcp = FastMCP(name="MathServer")


@math_mcp.tool()
Expand All @@ -1327,16 +1327,16 @@ async def lifespan(app: Starlette):
# Create the Starlette app and mount the MCP servers
app = Starlette(
routes=[
Mount("/echo", echo_mcp.streamable_http_app()),
Mount("/math", math_mcp.streamable_http_app()),
Mount("/echo", echo_mcp.streamable_http_app(stateless_http=True, json_response=True)),
Mount("/math", math_mcp.streamable_http_app(stateless_http=True, json_response=True)),
],
lifespan=lifespan,
)

# Note: Clients connect to http://localhost:8000/echo/mcp and http://localhost:8000/math/mcp
# To mount at the root of each path (e.g., /echo instead of /echo/mcp):
# echo_mcp.settings.streamable_http_path = "/"
# math_mcp.settings.streamable_http_path = "/"
# echo_mcp.streamable_http_app(streamable_http_path="/", stateless_http=True, json_response=True)
# math_mcp.streamable_http_app(streamable_http_path="/", stateless_http=True, json_response=True)
```

_Full example: [examples/snippets/servers/streamable_starlette_mount.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_starlette_mount.py)_
Expand Down Expand Up @@ -1409,7 +1409,7 @@ from starlette.routing import Mount
from mcp.server.fastmcp import FastMCP

# Create MCP server
mcp = FastMCP("My App", json_response=True)
mcp = FastMCP("My App")


@mcp.tool()
Expand All @@ -1426,9 +1426,10 @@ async def lifespan(app: Starlette):


# Mount the StreamableHTTP server to the existing ASGI server
# Transport-specific options are passed to streamable_http_app()
app = Starlette(
routes=[
Mount("/", app=mcp.streamable_http_app()),
Mount("/", app=mcp.streamable_http_app(json_response=True)),
],
lifespan=lifespan,
)
Expand Down Expand Up @@ -1456,7 +1457,7 @@ from starlette.routing import Host
from mcp.server.fastmcp import FastMCP

# Create MCP server
mcp = FastMCP("MCP Host App", json_response=True)
mcp = FastMCP("MCP Host App")


@mcp.tool()
Expand All @@ -1473,9 +1474,10 @@ async def lifespan(app: Starlette):


# Mount using Host-based routing
# Transport-specific options are passed to streamable_http_app()
app = Starlette(
routes=[
Host("mcp.acme.corp", app=mcp.streamable_http_app()),
Host("mcp.acme.corp", app=mcp.streamable_http_app(json_response=True)),
],
lifespan=lifespan,
)
Expand Down Expand Up @@ -1503,8 +1505,8 @@ from starlette.routing import Mount
from mcp.server.fastmcp import FastMCP

# Create multiple MCP servers
api_mcp = FastMCP("API Server", json_response=True)
chat_mcp = FastMCP("Chat Server", json_response=True)
api_mcp = FastMCP("API Server")
chat_mcp = FastMCP("Chat Server")


@api_mcp.tool()
Expand All @@ -1519,12 +1521,6 @@ def send_message(message: str) -> str:
return f"Message sent: {message}"


# Configure servers to mount at the root of each path
# This means endpoints will be at /api and /chat instead of /api/mcp and /chat/mcp
api_mcp.settings.streamable_http_path = "/"
chat_mcp.settings.streamable_http_path = "/"


# Create a combined lifespan to manage both session managers
@contextlib.asynccontextmanager
async def lifespan(app: Starlette):
Expand All @@ -1534,11 +1530,12 @@ async def lifespan(app: Starlette):
yield


# Mount the servers
# Mount the servers with transport-specific options passed to streamable_http_app()
# streamable_http_path="/" means endpoints will be at /api and /chat instead of /api/mcp and /chat/mcp
app = Starlette(
routes=[
Mount("/api", app=api_mcp.streamable_http_app()),
Mount("/chat", app=chat_mcp.streamable_http_app()),
Mount("/api", app=api_mcp.streamable_http_app(json_response=True, streamable_http_path="/")),
Mount("/chat", app=chat_mcp.streamable_http_app(json_response=True, streamable_http_path="/")),
],
lifespan=lifespan,
)
Expand All @@ -1552,7 +1549,7 @@ _Full example: [examples/snippets/servers/streamable_http_multiple_servers.py](h
<!-- snippet-source examples/snippets/servers/streamable_http_path_config.py -->
```python
"""
Example showing path configuration during FastMCP initialization.
Example showing path configuration when mounting FastMCP.

Run from the repository root:
uvicorn examples.snippets.servers.streamable_http_path_config:app --reload
Expand All @@ -1563,13 +1560,8 @@ from starlette.routing import Mount

from mcp.server.fastmcp import FastMCP

# Configure streamable_http_path during initialization
# This server will mount at the root of wherever it's mounted
mcp_at_root = FastMCP(
"My Server",
json_response=True,
streamable_http_path="/",
)
# Create a simple FastMCP server
mcp_at_root = FastMCP("My Server")


@mcp_at_root.tool()
Expand All @@ -1578,10 +1570,14 @@ def process_data(data: str) -> str:
return f"Processed: {data}"


# Mount at /process - endpoints will be at /process instead of /process/mcp
# Mount at /process with streamable_http_path="/" so the endpoint is /process (not /process/mcp)
# Transport-specific options like json_response are passed to streamable_http_app()
app = Starlette(
routes=[
Mount("/process", app=mcp_at_root.streamable_http_app()),
Mount(
"/process",
app=mcp_at_root.streamable_http_app(json_response=True, streamable_http_path="/"),
),
]
)
```
Expand Down
57 changes: 57 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,63 @@ The `mount_path` parameter has been removed from `FastMCP.__init__()`, `FastMCP.

This parameter was redundant because the SSE transport already handles sub-path mounting via ASGI's standard `root_path` mechanism. When using Starlette's `Mount("/path", app=mcp.sse_app())`, Starlette automatically sets `root_path` in the ASGI scope, and the `SseServerTransport` uses this to construct the correct message endpoint path.

### Transport-specific parameters moved from FastMCP constructor to run()/app methods

Transport-specific parameters have been moved from the `FastMCP` constructor to the `run()`, `sse_app()`, and `streamable_http_app()` methods. This provides better separation of concerns - the constructor now only handles server identity and authentication, while transport configuration is passed when starting the server.

**Parameters moved:**

- `host`, `port` - HTTP server binding
- `sse_path`, `message_path` - SSE transport paths
- `streamable_http_path` - StreamableHTTP endpoint path
- `json_response`, `stateless_http` - StreamableHTTP behavior
- `event_store`, `retry_interval` - StreamableHTTP event handling
- `transport_security` - DNS rebinding protection

**Before (v1):**

```python
from mcp.server.fastmcp import FastMCP

# Transport params in constructor
mcp = FastMCP("Demo", json_response=True, stateless_http=True)
mcp.run(transport="streamable-http")

# Or for SSE
mcp = FastMCP("Server", host="0.0.0.0", port=9000, sse_path="/events")
mcp.run(transport="sse")
```

**After (v2):**

```python
from mcp.server.fastmcp import FastMCP

# Transport params passed to run()
mcp = FastMCP("Demo")
mcp.run(transport="streamable-http", json_response=True, stateless_http=True)

# Or for SSE
mcp = FastMCP("Server")
mcp.run(transport="sse", host="0.0.0.0", port=9000, sse_path="/events")
```

**For mounted apps:**

When mounting FastMCP in a Starlette app, pass transport params to the app methods:

```python
# Before (v1)
mcp = FastMCP("App", json_response=True)
app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())])

# After (v2)
mcp = FastMCP("App")
app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app(json_response=True))])
```

**Note:** DNS rebinding protection is automatically enabled when `host` is `127.0.0.1`, `localhost`, or `::1`. This now happens in `sse_app()` and `streamable_http_app()` instead of the constructor.

### Resource URI type changed from `AnyUrl` to `str`

The `uri` field on resource-related types now uses `str` instead of Pydantic's `AnyUrl`. This aligns with the [MCP specification schema](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) which defines URIs as plain strings (`uri: string`) without strict URL validation. This change allows relative paths like `users/me` that were previously rejected.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ async def replay_events_after(self, last_event_id: EventId, send_callback: Event

mcp = FastMCP(
name="mcp-conformance-test-server",
event_store=event_store,
retry_interval=100, # 100ms retry interval for SSE polling
)


Expand Down Expand Up @@ -448,8 +446,12 @@ def main(port: int, log_level: str) -> int:
logger.info(f"Starting MCP Everything Server on port {port}")
logger.info(f"Endpoint will be: http://localhost:{port}/mcp")

mcp.settings.port = port
mcp.run(transport="streamable-http")
mcp.run(
transport="streamable-http",
port=port,
event_store=event_store,
retry_interval=100, # 100ms retry interval for SSE polling
)

return 0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ def create_simple_mcp_server(server_settings: ServerSettings, auth_settings: Sim
name="Simple Auth MCP Server",
instructions="A simple MCP server with simple credential authentication",
auth_server_provider=oauth_provider,
host=server_settings.host,
port=server_settings.port,
debug=True,
auth=mcp_auth_settings,
)
# Store server settings for later use in run()
app._server_settings = server_settings # type: ignore[attr-defined]

@app.custom_route("/login", methods=["GET"])
async def login_page_handler(request: Request) -> Response:
Expand Down Expand Up @@ -131,7 +131,7 @@ def main(port: int, transport: Literal["sse", "streamable-http"]) -> int:

mcp_server = create_simple_mcp_server(server_settings, auth_settings)
logger.info(f"🚀 MCP Legacy Server running on {server_url}")
mcp_server.run(transport=transport)
mcp_server.run(transport=transport, host=host, port=port)
return 0


Expand Down
6 changes: 3 additions & 3 deletions examples/servers/simple-auth/mcp_simple_auth/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ def create_resource_server(settings: ResourceServerSettings) -> FastMCP:
app = FastMCP(
name="MCP Resource Server",
instructions="Resource Server that validates tokens via Authorization Server introspection",
host=settings.host,
port=settings.port,
debug=True,
# Auth configuration for RS mode
token_verifier=token_verifier,
Expand All @@ -77,6 +75,8 @@ def create_resource_server(settings: ResourceServerSettings) -> FastMCP:
resource_server_url=settings.server_url,
),
)
# Store settings for later use in run()
app._resource_server_settings = settings # type: ignore[attr-defined]

@app.tool()
async def get_time() -> dict[str, Any]:
Expand Down Expand Up @@ -153,7 +153,7 @@ def main(port: int, auth_server: str, transport: Literal["sse", "streamable-http
logger.info(f"🔑 Using Authorization Server: {settings.auth_server_url}")

# Run the server - this should block and keep running
mcp_server.run(transport=transport)
mcp_server.run(transport=transport, host=host, port=port)
logger.info("Server stopped")
return 0
except Exception:
Expand Down
Loading
Loading