From 28aaa02cbc504e411bc6ae0d38871623a778e4cc Mon Sep 17 00:00:00 2001 From: Jerome Date: Mon, 30 Jun 2025 16:41:29 +0100 Subject: [PATCH 1/4] feat: Add CORS configuration for browser-based MCP clients - Add CORSMiddleware to streamable HTTP example servers - Configure minimal CORS with Mcp-Session-Id exposed - Add CORS documentation section to README This enables browser-based clients to connect to MCP servers by properly exposing the Mcp-Session-Id header required for session management. Reported-by: Jerome --- README.md | 21 +++++++++++++++++++ .../server.py | 9 ++++++++ .../mcp_simple_streamablehttp/server.py | 9 ++++++++ 3 files changed, 39 insertions(+) diff --git a/README.md b/README.md index f2b0012315..863f1cb2c3 100644 --- a/README.md +++ b/README.md @@ -713,6 +713,27 @@ 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.middleware.cors import CORSMiddleware + +# Add CORS middleware to your Starlette app +app.add_middleware( + CORSMiddleware, + 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 > **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http). 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 68f3ac6a6e..df0cb39d49 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 @@ -8,6 +8,7 @@ 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 @@ -131,6 +132,14 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]: ], lifespan=lifespan, ) + + # Add CORS middleware to expose Mcp-Session-Id header for browser-based clients + starlette_app.add_middleware( + CORSMiddleware, + 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 diff --git a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py index 9c25cc5696..125001d4c3 100644 --- a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py +++ b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py @@ -9,6 +9,7 @@ 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 @@ -159,6 +160,14 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]: ], lifespan=lifespan, ) + + # Add CORS middleware to expose Mcp-Session-Id header for browser-based clients + starlette_app.add_middleware( + CORSMiddleware, + 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 From 7b67d78642e8637d490fb431a71bf6881972de42 Mon Sep 17 00:00:00 2001 From: Jerome Date: Mon, 7 Jul 2025 13:53:03 +0100 Subject: [PATCH 2/4] Update README.md Co-authored-by: Marcelo Trylesinski --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 863f1cb2c3..962b83084e 100644 --- a/README.md +++ b/README.md @@ -721,8 +721,8 @@ If you'd like your server to be accessible by browser-based MCP clients, you'll from starlette.middleware.cors import CORSMiddleware # Add CORS middleware to your Starlette app -app.add_middleware( - CORSMiddleware, +app = CORSMiddleware( + app, allow_origins=["*"], # Configure appropriately for production allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods expose_headers=["Mcp-Session-Id"], From 992df5972d1397b796cdb2921f5d912c4b13b7fd Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 21 Aug 2025 14:04:22 +0100 Subject: [PATCH 3/4] Fix CORS middleware implementation to wrap entire application - Change from add_middleware() to CORSMiddleware wrapper pattern - Ensures 500 errors get proper CORS headers for browser clients - Update both streamable HTTP example servers - Fix README documentation to show complete example Reported-by: Jerome --- README.md | 10 +++++++--- .../mcp_simple_streamablehttp_stateless/server.py | 7 ++++--- .../mcp_simple_streamablehttp/server.py | 7 ++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 962b83084e..a76c43de9e 100644 --- a/README.md +++ b/README.md @@ -718,11 +718,15 @@ The streamable HTTP transport supports: 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 -# Add CORS middleware to your Starlette app -app = CORSMiddleware( - app, +# 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"], 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 df0cb39d49..2345a0b90f 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 @@ -133,9 +133,10 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]: lifespan=lifespan, ) - # Add CORS middleware to expose Mcp-Session-Id header for browser-based clients - starlette_app.add_middleware( - CORSMiddleware, + # 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"], diff --git a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py index 125001d4c3..2ab9217961 100644 --- a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py +++ b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py @@ -161,9 +161,10 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]: lifespan=lifespan, ) - # Add CORS middleware to expose Mcp-Session-Id header for browser-based clients - starlette_app.add_middleware( - CORSMiddleware, + # 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"], From bf7ab5f2df8952e03631d08f9f69ec14b1e4b06f Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 21 Aug 2025 14:11:14 +0100 Subject: [PATCH 4/4] Apply ruff formatting --- README.md | 1 + .../mcp_simple_streamablehttp_stateless/server.py | 2 +- .../simple-streamablehttp/mcp_simple_streamablehttp/server.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a76c43de9e..074b59a7f7 100644 --- a/README.md +++ b/README.md @@ -734,6 +734,7 @@ starlette_app = CORSMiddleware( ``` 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 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 2345a0b90f..ef1d7b88a2 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 @@ -132,7 +132,7 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]: ], 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( diff --git a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py index 2ab9217961..33f83d8056 100644 --- a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py +++ b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py @@ -160,7 +160,7 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]: ], 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(