diff --git a/README.md b/README.md index 2693359d..8142c99c 100644 --- a/README.md +++ b/README.md @@ -1165,6 +1165,32 @@ The streamable HTTP transport supports: - JSON or SSE response formats - Better scalability for multi-node deployments +#### CORS Configuration for Browser-Based Clients + +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: + +```python +from starlette.applications import Starlette +from starlette.middleware.cors import CORSMiddleware + +# Create your Starlette app first +starlette_app = Starlette(routes=[...]) + +# Then wrap it with CORS middleware +starlette_app = CORSMiddleware( + starlette_app, + allow_origins=["*"], # Configure appropriately for production + allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods + expose_headers=["Mcp-Session-Id"], +) +``` + +This configuration is necessary because: + +- The MCP streamable HTTP transport uses the `Mcp-Session-Id` header for session management +- Browsers restrict access to response headers unless explicitly exposed via CORS +- Without this configuration, browser-based clients won't be able to read the session ID from initialization responses + ### Mounting to an Existing ASGI Server 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. diff --git a/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py b/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py index 3071e823..f1b3987d 100644 --- a/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py +++ b/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py @@ -9,6 +9,7 @@ import mcp.types as types from mcp.server.lowlevel import Server from mcp.server.streamable_http_manager import StreamableHTTPSessionManager from starlette.applications import Starlette +from starlette.middleware.cors import CORSMiddleware from starlette.routing import Mount from starlette.types import Receive, Scope, Send @@ -123,6 +124,15 @@ def main( lifespan=lifespan, ) + # Wrap ASGI application with CORS middleware to expose Mcp-Session-Id header + # for browser-based clients (ensures 500 errors get proper CORS headers) + starlette_app = CORSMiddleware( + starlette_app, + allow_origins=["*"], # Allow all origins - adjust as needed for production + allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods + expose_headers=["Mcp-Session-Id"], + ) + import uvicorn uvicorn.run(starlette_app, host="127.0.0.1", port=port) diff --git a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py index cf9200ce..4b2604b9 100644 --- a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py +++ b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py @@ -10,6 +10,7 @@ from mcp.server.lowlevel import Server from mcp.server.streamable_http_manager import StreamableHTTPSessionManager from pydantic import AnyUrl from starlette.applications import Starlette +from starlette.middleware.cors import CORSMiddleware from starlette.routing import Mount from starlette.types import Receive, Scope, Send @@ -148,6 +149,15 @@ def main( lifespan=lifespan, ) + # Wrap ASGI application with CORS middleware to expose Mcp-Session-Id header + # for browser-based clients (ensures 500 errors get proper CORS headers) + starlette_app = CORSMiddleware( + starlette_app, + allow_origins=["*"], # Allow all origins - adjust as needed for production + allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods + expose_headers=["Mcp-Session-Id"], + ) + import uvicorn uvicorn.run(starlette_app, host="127.0.0.1", port=port)