Skip to content

Commit 382d37d

Browse files
authored
Merge branch 'main' into main
2 parents 10a7a79 + 09e3a05 commit 382d37d

File tree

9 files changed

+349
-1
lines changed

9 files changed

+349
-1
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ cython_debug/
162162
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
163163
# and can be added to the global gitignore or merged into this file. For a more nuclear
164164
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
165-
#.idea/
165+
.idea/
166166

167167
# vscode
168168
.vscode/

README.md

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,11 @@ app = Starlette(
11431143
],
11441144
lifespan=lifespan,
11451145
)
1146+
1147+
# Note: Clients connect to http://localhost:8000/echo/mcp and http://localhost:8000/math/mcp
1148+
# To mount at the root of each path (e.g., /echo instead of /echo/mcp):
1149+
# echo_mcp.settings.streamable_http_path = "/"
1150+
# math_mcp.settings.streamable_http_path = "/"
11461151
```
11471152

11481153
_Full example: [examples/snippets/servers/streamable_starlette_mount.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_starlette_mount.py)_
@@ -1160,12 +1165,204 @@ The streamable HTTP transport supports:
11601165
- JSON or SSE response formats
11611166
- Better scalability for multi-node deployments
11621167

1168+
#### CORS Configuration for Browser-Based Clients
1169+
1170+
If you'd like your server to be accessible by browser-based MCP clients, you'll need to configure CORS headers. The `Mcp-Session-Id` header must be exposed for browser clients to access it:
1171+
1172+
```python
1173+
from starlette.applications import Starlette
1174+
from starlette.middleware.cors import CORSMiddleware
1175+
1176+
# Create your Starlette app first
1177+
starlette_app = Starlette(routes=[...])
1178+
1179+
# Then wrap it with CORS middleware
1180+
starlette_app = CORSMiddleware(
1181+
starlette_app,
1182+
allow_origins=["*"], # Configure appropriately for production
1183+
allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods
1184+
expose_headers=["Mcp-Session-Id"],
1185+
)
1186+
```
1187+
1188+
This configuration is necessary because:
1189+
1190+
- The MCP streamable HTTP transport uses the `Mcp-Session-Id` header for session management
1191+
- Browsers restrict access to response headers unless explicitly exposed via CORS
1192+
- Without this configuration, browser-based clients won't be able to read the session ID from initialization responses
1193+
11631194
### Mounting to an Existing ASGI Server
11641195

11651196
By default, SSE servers are mounted at `/sse` and Streamable HTTP servers are mounted at `/mcp`. You can customize these paths using the methods described below.
11661197

11671198
For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes).
11681199

1200+
#### StreamableHTTP servers
1201+
1202+
You can mount the StreamableHTTP server to an existing ASGI server using the `streamable_http_app` method. This allows you to integrate the StreamableHTTP server with other ASGI applications.
1203+
1204+
##### Basic mounting
1205+
1206+
<!-- snippet-source examples/snippets/servers/streamable_http_basic_mounting.py -->
1207+
```python
1208+
"""
1209+
Basic example showing how to mount StreamableHTTP server in Starlette.
1210+
1211+
Run from the repository root:
1212+
uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload
1213+
"""
1214+
1215+
from starlette.applications import Starlette
1216+
from starlette.routing import Mount
1217+
1218+
from mcp.server.fastmcp import FastMCP
1219+
1220+
# Create MCP server
1221+
mcp = FastMCP("My App")
1222+
1223+
1224+
@mcp.tool()
1225+
def hello() -> str:
1226+
"""A simple hello tool"""
1227+
return "Hello from MCP!"
1228+
1229+
1230+
# Mount the StreamableHTTP server to the existing ASGI server
1231+
app = Starlette(
1232+
routes=[
1233+
Mount("/", app=mcp.streamable_http_app()),
1234+
]
1235+
)
1236+
```
1237+
1238+
_Full example: [examples/snippets/servers/streamable_http_basic_mounting.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_basic_mounting.py)_
1239+
<!-- /snippet-source -->
1240+
1241+
##### Host-based routing
1242+
1243+
<!-- snippet-source examples/snippets/servers/streamable_http_host_mounting.py -->
1244+
```python
1245+
"""
1246+
Example showing how to mount StreamableHTTP server using Host-based routing.
1247+
1248+
Run from the repository root:
1249+
uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload
1250+
"""
1251+
1252+
from starlette.applications import Starlette
1253+
from starlette.routing import Host
1254+
1255+
from mcp.server.fastmcp import FastMCP
1256+
1257+
# Create MCP server
1258+
mcp = FastMCP("MCP Host App")
1259+
1260+
1261+
@mcp.tool()
1262+
def domain_info() -> str:
1263+
"""Get domain-specific information"""
1264+
return "This is served from mcp.acme.corp"
1265+
1266+
1267+
# Mount using Host-based routing
1268+
app = Starlette(
1269+
routes=[
1270+
Host("mcp.acme.corp", app=mcp.streamable_http_app()),
1271+
]
1272+
)
1273+
```
1274+
1275+
_Full example: [examples/snippets/servers/streamable_http_host_mounting.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_host_mounting.py)_
1276+
<!-- /snippet-source -->
1277+
1278+
##### Multiple servers with path configuration
1279+
1280+
<!-- snippet-source examples/snippets/servers/streamable_http_multiple_servers.py -->
1281+
```python
1282+
"""
1283+
Example showing how to mount multiple StreamableHTTP servers with path configuration.
1284+
1285+
Run from the repository root:
1286+
uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload
1287+
"""
1288+
1289+
from starlette.applications import Starlette
1290+
from starlette.routing import Mount
1291+
1292+
from mcp.server.fastmcp import FastMCP
1293+
1294+
# Create multiple MCP servers
1295+
api_mcp = FastMCP("API Server")
1296+
chat_mcp = FastMCP("Chat Server")
1297+
1298+
1299+
@api_mcp.tool()
1300+
def api_status() -> str:
1301+
"""Get API status"""
1302+
return "API is running"
1303+
1304+
1305+
@chat_mcp.tool()
1306+
def send_message(message: str) -> str:
1307+
"""Send a chat message"""
1308+
return f"Message sent: {message}"
1309+
1310+
1311+
# Configure servers to mount at the root of each path
1312+
# This means endpoints will be at /api and /chat instead of /api/mcp and /chat/mcp
1313+
api_mcp.settings.streamable_http_path = "/"
1314+
chat_mcp.settings.streamable_http_path = "/"
1315+
1316+
# Mount the servers
1317+
app = Starlette(
1318+
routes=[
1319+
Mount("/api", app=api_mcp.streamable_http_app()),
1320+
Mount("/chat", app=chat_mcp.streamable_http_app()),
1321+
]
1322+
)
1323+
```
1324+
1325+
_Full example: [examples/snippets/servers/streamable_http_multiple_servers.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_multiple_servers.py)_
1326+
<!-- /snippet-source -->
1327+
1328+
##### Path configuration at initialization
1329+
1330+
<!-- snippet-source examples/snippets/servers/streamable_http_path_config.py -->
1331+
```python
1332+
"""
1333+
Example showing path configuration during FastMCP initialization.
1334+
1335+
Run from the repository root:
1336+
uvicorn examples.snippets.servers.streamable_http_path_config:app --reload
1337+
"""
1338+
1339+
from starlette.applications import Starlette
1340+
from starlette.routing import Mount
1341+
1342+
from mcp.server.fastmcp import FastMCP
1343+
1344+
# Configure streamable_http_path during initialization
1345+
# This server will mount at the root of wherever it's mounted
1346+
mcp_at_root = FastMCP("My Server", streamable_http_path="/")
1347+
1348+
1349+
@mcp_at_root.tool()
1350+
def process_data(data: str) -> str:
1351+
"""Process some data"""
1352+
return f"Processed: {data}"
1353+
1354+
1355+
# Mount at /process - endpoints will be at /process instead of /process/mcp
1356+
app = Starlette(
1357+
routes=[
1358+
Mount("/process", app=mcp_at_root.streamable_http_app()),
1359+
]
1360+
)
1361+
```
1362+
1363+
_Full example: [examples/snippets/servers/streamable_http_path_config.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_path_config.py)_
1364+
<!-- /snippet-source -->
1365+
11691366
#### SSE servers
11701367

11711368
> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).

examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from mcp.server.lowlevel import Server
1010
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
1111
from starlette.applications import Starlette
12+
from starlette.middleware.cors import CORSMiddleware
1213
from starlette.routing import Mount
1314
from starlette.types import Receive, Scope, Send
1415

@@ -123,6 +124,15 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]:
123124
lifespan=lifespan,
124125
)
125126

127+
# Wrap ASGI application with CORS middleware to expose Mcp-Session-Id header
128+
# for browser-based clients (ensures 500 errors get proper CORS headers)
129+
starlette_app = CORSMiddleware(
130+
starlette_app,
131+
allow_origins=["*"], # Allow all origins - adjust as needed for production
132+
allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods
133+
expose_headers=["Mcp-Session-Id"],
134+
)
135+
126136
import uvicorn
127137

128138
uvicorn.run(starlette_app, host="127.0.0.1", port=port)

examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
1111
from pydantic import AnyUrl
1212
from starlette.applications import Starlette
13+
from starlette.middleware.cors import CORSMiddleware
1314
from starlette.routing import Mount
1415
from starlette.types import Receive, Scope, Send
1516

@@ -148,6 +149,15 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]:
148149
lifespan=lifespan,
149150
)
150151

152+
# Wrap ASGI application with CORS middleware to expose Mcp-Session-Id header
153+
# for browser-based clients (ensures 500 errors get proper CORS headers)
154+
starlette_app = CORSMiddleware(
155+
starlette_app,
156+
allow_origins=["*"], # Allow all origins - adjust as needed for production
157+
allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods
158+
expose_headers=["Mcp-Session-Id"],
159+
)
160+
151161
import uvicorn
152162

153163
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""
2+
Basic example showing how to mount StreamableHTTP server in Starlette.
3+
4+
Run from the repository root:
5+
uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload
6+
"""
7+
8+
from starlette.applications import Starlette
9+
from starlette.routing import Mount
10+
11+
from mcp.server.fastmcp import FastMCP
12+
13+
# Create MCP server
14+
mcp = FastMCP("My App")
15+
16+
17+
@mcp.tool()
18+
def hello() -> str:
19+
"""A simple hello tool"""
20+
return "Hello from MCP!"
21+
22+
23+
# Mount the StreamableHTTP server to the existing ASGI server
24+
app = Starlette(
25+
routes=[
26+
Mount("/", app=mcp.streamable_http_app()),
27+
]
28+
)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""
2+
Example showing how to mount StreamableHTTP server using Host-based routing.
3+
4+
Run from the repository root:
5+
uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload
6+
"""
7+
8+
from starlette.applications import Starlette
9+
from starlette.routing import Host
10+
11+
from mcp.server.fastmcp import FastMCP
12+
13+
# Create MCP server
14+
mcp = FastMCP("MCP Host App")
15+
16+
17+
@mcp.tool()
18+
def domain_info() -> str:
19+
"""Get domain-specific information"""
20+
return "This is served from mcp.acme.corp"
21+
22+
23+
# Mount using Host-based routing
24+
app = Starlette(
25+
routes=[
26+
Host("mcp.acme.corp", app=mcp.streamable_http_app()),
27+
]
28+
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""
2+
Example showing how to mount multiple StreamableHTTP servers with path configuration.
3+
4+
Run from the repository root:
5+
uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload
6+
"""
7+
8+
from starlette.applications import Starlette
9+
from starlette.routing import Mount
10+
11+
from mcp.server.fastmcp import FastMCP
12+
13+
# Create multiple MCP servers
14+
api_mcp = FastMCP("API Server")
15+
chat_mcp = FastMCP("Chat Server")
16+
17+
18+
@api_mcp.tool()
19+
def api_status() -> str:
20+
"""Get API status"""
21+
return "API is running"
22+
23+
24+
@chat_mcp.tool()
25+
def send_message(message: str) -> str:
26+
"""Send a chat message"""
27+
return f"Message sent: {message}"
28+
29+
30+
# Configure servers to mount at the root of each path
31+
# This means endpoints will be at /api and /chat instead of /api/mcp and /chat/mcp
32+
api_mcp.settings.streamable_http_path = "/"
33+
chat_mcp.settings.streamable_http_path = "/"
34+
35+
# Mount the servers
36+
app = Starlette(
37+
routes=[
38+
Mount("/api", app=api_mcp.streamable_http_app()),
39+
Mount("/chat", app=chat_mcp.streamable_http_app()),
40+
]
41+
)

0 commit comments

Comments
 (0)