Skip to content

Commit 41d4ac7

Browse files
Merge branch 'main' into fastmpc-logging-progress-example
2 parents 66a7048 + c3717e7 commit 41d4ac7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1779
-510
lines changed

.gitattribute

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Generated
2+
uv.lock linguist-generated=true

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ coverage.xml
5252
*.py,cover
5353
.hypothesis/
5454
.pytest_cache/
55+
.ruff_cache/
5556
cover/
5657

5758
# Translations
@@ -162,9 +163,12 @@ cython_debug/
162163
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
163164
# and can be added to the global gitignore or merged into this file. For a more nuclear
164165
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
165-
#.idea/
166+
.idea/
166167

167168
# vscode
168169
.vscode/
169170
.windsurfrules
170171
**/CLAUDE.local.md
172+
173+
# claude code
174+
.claude/

README.md

Lines changed: 210 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,32 @@
3131
- [Prompts](#prompts)
3232
- [Images](#images)
3333
- [Context](#context)
34+
- [Getting Context in Functions](#getting-context-in-functions)
35+
- [Context Properties and Methods](#context-properties-and-methods)
3436
- [Completions](#completions)
3537
- [Elicitation](#elicitation)
3638
- [Sampling](#sampling)
3739
- [Logging and Notifications](#logging-and-notifications)
3840
- [Authentication](#authentication)
3941
- [FastMCP Properties](#fastmcp-properties)
40-
- [Session Properties](#session-properties-and-methods)
42+
- [Session Properties and Methods](#session-properties-and-methods)
4143
- [Request Context Properties](#request-context-properties)
4244
- [Running Your Server](#running-your-server)
4345
- [Development Mode](#development-mode)
4446
- [Claude Desktop Integration](#claude-desktop-integration)
4547
- [Direct Execution](#direct-execution)
4648
- [Streamable HTTP Transport](#streamable-http-transport)
49+
- [CORS Configuration for Browser-Based Clients](#cors-configuration-for-browser-based-clients)
4750
- [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server)
51+
- [StreamableHTTP servers](#streamablehttp-servers)
52+
- [Basic mounting](#basic-mounting)
53+
- [Host-based routing](#host-based-routing)
54+
- [Multiple servers with path configuration](#multiple-servers-with-path-configuration)
55+
- [Path configuration at initialization](#path-configuration-at-initialization)
56+
- [SSE servers](#sse-servers)
4857
- [Advanced Usage](#advanced-usage)
4958
- [Low-Level Server](#low-level-server)
59+
- [Structured Output Support](#structured-output-support)
5060
- [Writing MCP Clients](#writing-mcp-clients)
5161
- [Client Display Utilities](#client-display-utilities)
5262
- [OAuth Authentication for Clients](#oauth-authentication-for-clients)
@@ -400,7 +410,7 @@ def get_weather(city: str) -> WeatherData:
400410
"""Get weather for a city - returns structured data."""
401411
# Simulated weather data
402412
return WeatherData(
403-
temperature=72.5,
413+
temperature=22.5,
404414
humidity=45.0,
405415
condition="sunny",
406416
wind_speed=5.2,
@@ -1143,6 +1153,11 @@ app = Starlette(
11431153
],
11441154
lifespan=lifespan,
11451155
)
1156+
1157+
# Note: Clients connect to http://localhost:8000/echo/mcp and http://localhost:8000/math/mcp
1158+
# To mount at the root of each path (e.g., /echo instead of /echo/mcp):
1159+
# echo_mcp.settings.streamable_http_path = "/"
1160+
# math_mcp.settings.streamable_http_path = "/"
11461161
```
11471162

11481163
_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 +1175,204 @@ The streamable HTTP transport supports:
11601175
- JSON or SSE response formats
11611176
- Better scalability for multi-node deployments
11621177

1178+
#### CORS Configuration for Browser-Based Clients
1179+
1180+
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:
1181+
1182+
```python
1183+
from starlette.applications import Starlette
1184+
from starlette.middleware.cors import CORSMiddleware
1185+
1186+
# Create your Starlette app first
1187+
starlette_app = Starlette(routes=[...])
1188+
1189+
# Then wrap it with CORS middleware
1190+
starlette_app = CORSMiddleware(
1191+
starlette_app,
1192+
allow_origins=["*"], # Configure appropriately for production
1193+
allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods
1194+
expose_headers=["Mcp-Session-Id"],
1195+
)
1196+
```
1197+
1198+
This configuration is necessary because:
1199+
1200+
- The MCP streamable HTTP transport uses the `Mcp-Session-Id` header for session management
1201+
- Browsers restrict access to response headers unless explicitly exposed via CORS
1202+
- Without this configuration, browser-based clients won't be able to read the session ID from initialization responses
1203+
11631204
### Mounting to an Existing ASGI Server
11641205

11651206
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.
11661207

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

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

11711378
> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).
@@ -1940,6 +2147,7 @@ MCP servers declare capabilities during initialization:
19402147

19412148
## Documentation
19422149

2150+
- [API Reference](https://modelcontextprotocol.github.io/python-sdk/api/)
19432151
- [Model Context Protocol documentation](https://modelcontextprotocol.io)
19442152
- [Model Context Protocol specification](https://spec.modelcontextprotocol.io)
19452153
- [Officially supported servers](https://github.com/modelcontextprotocol/servers)

examples/servers/simple-resource/mcp_simple_resource/server.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import click
33
import mcp.types as types
44
from mcp.server.lowlevel import Server
5+
from mcp.server.lowlevel.helper_types import ReadResourceContents
56
from pydantic import AnyUrl, FileUrl
67
from starlette.requests import Request
78

@@ -46,15 +47,15 @@ async def list_resources() -> list[types.Resource]:
4647
]
4748

4849
@app.read_resource()
49-
async def read_resource(uri: AnyUrl) -> str | bytes:
50+
async def read_resource(uri: AnyUrl):
5051
if uri.path is None:
5152
raise ValueError(f"Invalid resource path: {uri}")
5253
name = uri.path.replace(".txt", "").lstrip("/")
5354

5455
if name not in SAMPLE_RESOURCES:
5556
raise ValueError(f"Unknown resource: {uri}")
5657

57-
return SAMPLE_RESOURCES[name]["content"]
58+
return [ReadResourceContents(content=SAMPLE_RESOURCES[name]["content"], mime_type="text/plain")]
5859

5960
if transport == "sse":
6061
from mcp.server.sse import SseServerTransport

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+
)

0 commit comments

Comments
 (0)