Skip to content

Commit 8adb5bd

Browse files
Kludexclaude
andauthored
refactor: move transport-specific parameters from FastMCP constructor to run() (#1898)
Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent 5d80f4e commit 8adb5bd

File tree

17 files changed

+339
-231
lines changed

17 files changed

+339
-231
lines changed

README.md

Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ Run from the repository root:
146146
from mcp.server.fastmcp import FastMCP
147147

148148
# Create an MCP server
149-
mcp = FastMCP("Demo", json_response=True)
149+
mcp = FastMCP("Demo")
150150

151151

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

179179
# Run with streamable HTTP transport
180180
if __name__ == "__main__":
181-
mcp.run(transport="streamable-http")
181+
mcp.run(transport="streamable-http", json_response=True)
182182
```
183183

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

10511050

10521051
if __name__ == "__main__":
1053-
mcp.run(transport="streamable-http")
1052+
mcp.run(transport="streamable-http", json_response=True)
10541053
```
10551054

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

12541253
from mcp.server.fastmcp import FastMCP
12551254

1256-
# Stateless server with JSON responses (recommended)
1257-
mcp = FastMCP("StatelessServer", stateless_http=True, json_response=True)
1258-
1259-
# Other configuration options:
1260-
# Stateless server with SSE streaming responses
1261-
# mcp = FastMCP("StatelessServer", stateless_http=True)
1262-
1263-
# Stateful server with session persistence
1264-
# mcp = FastMCP("StatefulServer")
1255+
mcp = FastMCP("StatelessServer")
12651256

12661257

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

12731264

12741265
# Run server with streamable_http transport
1266+
# Transport-specific options (stateless_http, json_response) are passed to run()
12751267
if __name__ == "__main__":
1276-
mcp.run(transport="streamable-http")
1268+
# Stateless server with JSON responses (recommended)
1269+
mcp.run(transport="streamable-http", stateless_http=True, json_response=True)
1270+
1271+
# Other configuration options:
1272+
# Stateless server with SSE streaming responses
1273+
# mcp.run(transport="streamable-http", stateless_http=True)
1274+
1275+
# Stateful server with session persistence
1276+
# mcp.run(transport="streamable-http")
12771277
```
12781278

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

12981298
# Create the Echo server
1299-
echo_mcp = FastMCP(name="EchoServer", stateless_http=True, json_response=True)
1299+
echo_mcp = FastMCP(name="EchoServer")
13001300

13011301

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

13071307

13081308
# Create the Math server
1309-
math_mcp = FastMCP(name="MathServer", stateless_http=True, json_response=True)
1309+
math_mcp = FastMCP(name="MathServer")
13101310

13111311

13121312
@math_mcp.tool()
@@ -1327,16 +1327,16 @@ async def lifespan(app: Starlette):
13271327
# Create the Starlette app and mount the MCP servers
13281328
app = Starlette(
13291329
routes=[
1330-
Mount("/echo", echo_mcp.streamable_http_app()),
1331-
Mount("/math", math_mcp.streamable_http_app()),
1330+
Mount("/echo", echo_mcp.streamable_http_app(stateless_http=True, json_response=True)),
1331+
Mount("/math", math_mcp.streamable_http_app(stateless_http=True, json_response=True)),
13321332
],
13331333
lifespan=lifespan,
13341334
)
13351335

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

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

14111411
# Create MCP server
1412-
mcp = FastMCP("My App", json_response=True)
1412+
mcp = FastMCP("My App")
14131413

14141414

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

14271427

14281428
# Mount the StreamableHTTP server to the existing ASGI server
1429+
# Transport-specific options are passed to streamable_http_app()
14291430
app = Starlette(
14301431
routes=[
1431-
Mount("/", app=mcp.streamable_http_app()),
1432+
Mount("/", app=mcp.streamable_http_app(json_response=True)),
14321433
],
14331434
lifespan=lifespan,
14341435
)
@@ -1456,7 +1457,7 @@ from starlette.routing import Host
14561457
from mcp.server.fastmcp import FastMCP
14571458

14581459
# Create MCP server
1459-
mcp = FastMCP("MCP Host App", json_response=True)
1460+
mcp = FastMCP("MCP Host App")
14601461

14611462

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

14741475

14751476
# Mount using Host-based routing
1477+
# Transport-specific options are passed to streamable_http_app()
14761478
app = Starlette(
14771479
routes=[
1478-
Host("mcp.acme.corp", app=mcp.streamable_http_app()),
1480+
Host("mcp.acme.corp", app=mcp.streamable_http_app(json_response=True)),
14791481
],
14801482
lifespan=lifespan,
14811483
)
@@ -1503,8 +1505,8 @@ from starlette.routing import Mount
15031505
from mcp.server.fastmcp import FastMCP
15041506

15051507
# Create multiple MCP servers
1506-
api_mcp = FastMCP("API Server", json_response=True)
1507-
chat_mcp = FastMCP("Chat Server", json_response=True)
1508+
api_mcp = FastMCP("API Server")
1509+
chat_mcp = FastMCP("Chat Server")
15081510

15091511

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

15211523

1522-
# Configure servers to mount at the root of each path
1523-
# This means endpoints will be at /api and /chat instead of /api/mcp and /chat/mcp
1524-
api_mcp.settings.streamable_http_path = "/"
1525-
chat_mcp.settings.streamable_http_path = "/"
1526-
1527-
15281524
# Create a combined lifespan to manage both session managers
15291525
@contextlib.asynccontextmanager
15301526
async def lifespan(app: Starlette):
@@ -1534,11 +1530,12 @@ async def lifespan(app: Starlette):
15341530
yield
15351531

15361532

1537-
# Mount the servers
1533+
# Mount the servers with transport-specific options passed to streamable_http_app()
1534+
# streamable_http_path="/" means endpoints will be at /api and /chat instead of /api/mcp and /chat/mcp
15381535
app = Starlette(
15391536
routes=[
1540-
Mount("/api", app=api_mcp.streamable_http_app()),
1541-
Mount("/chat", app=chat_mcp.streamable_http_app()),
1537+
Mount("/api", app=api_mcp.streamable_http_app(json_response=True, streamable_http_path="/")),
1538+
Mount("/chat", app=chat_mcp.streamable_http_app(json_response=True, streamable_http_path="/")),
15421539
],
15431540
lifespan=lifespan,
15441541
)
@@ -1552,7 +1549,7 @@ _Full example: [examples/snippets/servers/streamable_http_multiple_servers.py](h
15521549
<!-- snippet-source examples/snippets/servers/streamable_http_path_config.py -->
15531550
```python
15541551
"""
1555-
Example showing path configuration during FastMCP initialization.
1552+
Example showing path configuration when mounting FastMCP.
15561553
15571554
Run from the repository root:
15581555
uvicorn examples.snippets.servers.streamable_http_path_config:app --reload
@@ -1563,13 +1560,8 @@ from starlette.routing import Mount
15631560

15641561
from mcp.server.fastmcp import FastMCP
15651562

1566-
# Configure streamable_http_path during initialization
1567-
# This server will mount at the root of wherever it's mounted
1568-
mcp_at_root = FastMCP(
1569-
"My Server",
1570-
json_response=True,
1571-
streamable_http_path="/",
1572-
)
1563+
# Create a simple FastMCP server
1564+
mcp_at_root = FastMCP("My Server")
15731565

15741566

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

15801572

1581-
# Mount at /process - endpoints will be at /process instead of /process/mcp
1573+
# Mount at /process with streamable_http_path="/" so the endpoint is /process (not /process/mcp)
1574+
# Transport-specific options like json_response are passed to streamable_http_app()
15821575
app = Starlette(
15831576
routes=[
1584-
Mount("/process", app=mcp_at_root.streamable_http_app()),
1577+
Mount(
1578+
"/process",
1579+
app=mcp_at_root.streamable_http_app(json_response=True, streamable_http_path="/"),
1580+
),
15851581
]
15861582
)
15871583
```

docs/migration.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,63 @@ The `mount_path` parameter has been removed from `FastMCP.__init__()`, `FastMCP.
122122

123123
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.
124124

125+
### Transport-specific parameters moved from FastMCP constructor to run()/app methods
126+
127+
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.
128+
129+
**Parameters moved:**
130+
131+
- `host`, `port` - HTTP server binding
132+
- `sse_path`, `message_path` - SSE transport paths
133+
- `streamable_http_path` - StreamableHTTP endpoint path
134+
- `json_response`, `stateless_http` - StreamableHTTP behavior
135+
- `event_store`, `retry_interval` - StreamableHTTP event handling
136+
- `transport_security` - DNS rebinding protection
137+
138+
**Before (v1):**
139+
140+
```python
141+
from mcp.server.fastmcp import FastMCP
142+
143+
# Transport params in constructor
144+
mcp = FastMCP("Demo", json_response=True, stateless_http=True)
145+
mcp.run(transport="streamable-http")
146+
147+
# Or for SSE
148+
mcp = FastMCP("Server", host="0.0.0.0", port=9000, sse_path="/events")
149+
mcp.run(transport="sse")
150+
```
151+
152+
**After (v2):**
153+
154+
```python
155+
from mcp.server.fastmcp import FastMCP
156+
157+
# Transport params passed to run()
158+
mcp = FastMCP("Demo")
159+
mcp.run(transport="streamable-http", json_response=True, stateless_http=True)
160+
161+
# Or for SSE
162+
mcp = FastMCP("Server")
163+
mcp.run(transport="sse", host="0.0.0.0", port=9000, sse_path="/events")
164+
```
165+
166+
**For mounted apps:**
167+
168+
When mounting FastMCP in a Starlette app, pass transport params to the app methods:
169+
170+
```python
171+
# Before (v1)
172+
mcp = FastMCP("App", json_response=True)
173+
app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())])
174+
175+
# After (v2)
176+
mcp = FastMCP("App")
177+
app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app(json_response=True))])
178+
```
179+
180+
**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.
181+
125182
### Resource URI type changed from `AnyUrl` to `str`
126183

127184
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.

examples/servers/everything-server/mcp_everything_server/server.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,6 @@ async def replay_events_after(self, last_event_id: EventId, send_callback: Event
8383

8484
mcp = FastMCP(
8585
name="mcp-conformance-test-server",
86-
event_store=event_store,
87-
retry_interval=100, # 100ms retry interval for SSE polling
8886
)
8987

9088

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

451-
mcp.settings.port = port
452-
mcp.run(transport="streamable-http")
449+
mcp.run(
450+
transport="streamable-http",
451+
port=port,
452+
event_store=event_store,
453+
retry_interval=100, # 100ms retry interval for SSE polling
454+
)
453455

454456
return 0
455457

examples/servers/simple-auth/mcp_simple_auth/legacy_as_server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@ def create_simple_mcp_server(server_settings: ServerSettings, auth_settings: Sim
6666
name="Simple Auth MCP Server",
6767
instructions="A simple MCP server with simple credential authentication",
6868
auth_server_provider=oauth_provider,
69-
host=server_settings.host,
70-
port=server_settings.port,
7169
debug=True,
7270
auth=mcp_auth_settings,
7371
)
72+
# Store server settings for later use in run()
73+
app._server_settings = server_settings # type: ignore[attr-defined]
7474

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

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

137137

examples/servers/simple-auth/mcp_simple_auth/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ def create_resource_server(settings: ResourceServerSettings) -> FastMCP:
6666
app = FastMCP(
6767
name="MCP Resource Server",
6868
instructions="Resource Server that validates tokens via Authorization Server introspection",
69-
host=settings.host,
70-
port=settings.port,
7169
debug=True,
7270
# Auth configuration for RS mode
7371
token_verifier=token_verifier,
@@ -77,6 +75,8 @@ def create_resource_server(settings: ResourceServerSettings) -> FastMCP:
7775
resource_server_url=settings.server_url,
7876
),
7977
)
78+
# Store settings for later use in run()
79+
app._resource_server_settings = settings # type: ignore[attr-defined]
8080

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

155155
# Run the server - this should block and keep running
156-
mcp_server.run(transport=transport)
156+
mcp_server.run(transport=transport, host=host, port=port)
157157
logger.info("Server stopped")
158158
return 0
159159
except Exception:

0 commit comments

Comments
 (0)