From 69f2ab93254d4c80ebc70de5bb5e59c8da8d4bef Mon Sep 17 00:00:00 2001 From: Ranjit Odedra Date: Sat, 18 Oct 2025 01:01:38 -0400 Subject: [PATCH 1/2] Fix: Add missing lifespan parameter to StreamableHTTP mounting examples Fixes #1484 - Add contextlib import and lifespan function to all mounting examples - Add stateless_http=True parameter to FastMCP constructors - Add lifespan=lifespan parameter to Starlette apps - Update README.md documentation with correct examples - Fixes RuntimeError: Task group is not initialized. Make sure to use run() Resolves the issue where mounting StreamableHTTP servers to existing ASGI applications would fail with RuntimeError due to missing session manager initialization. --- README.md | 54 +++++++++++++++---- .../servers/streamable_http_basic_mounting.py | 15 +++++- .../servers/streamable_http_host_mounting.py | 15 +++++- .../streamable_http_multiple_servers.py | 20 +++++-- .../servers/streamable_http_path_config.py | 15 +++++- 5 files changed, 101 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a0daf5c6d..33db2eb7d 100644 --- a/README.md +++ b/README.md @@ -1261,6 +1261,7 @@ Basic example showing how to mount StreamableHTTP server in Starlette. Run from the repository root: uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload """ +import contextlib from starlette.applications import Starlette from starlette.routing import Mount @@ -1268,7 +1269,7 @@ from starlette.routing import Mount from mcp.server.fastmcp import FastMCP # Create MCP server -mcp = FastMCP("My App") +mcp = FastMCP("My App", stateless_http=True) @mcp.tool() @@ -1276,12 +1277,19 @@ def hello() -> str: """A simple hello tool""" return "Hello from MCP!" +# Create lifespan context manager to initialize the session manager +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + """Context manager for managing MCP session manager lifecycle.""" + async with mcp.session_manager.run(): + yield # Mount the StreamableHTTP server to the existing ASGI server app = Starlette( routes=[ Mount("/", app=mcp.streamable_http_app()), - ] + ], + lifespan=lifespan, ) ``` @@ -1298,6 +1306,7 @@ Example showing how to mount StreamableHTTP server using Host-based routing. Run from the repository root: uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload """ +import contextlib from starlette.applications import Starlette from starlette.routing import Host @@ -1305,7 +1314,7 @@ from starlette.routing import Host from mcp.server.fastmcp import FastMCP # Create MCP server -mcp = FastMCP("MCP Host App") +mcp = FastMCP("MCP Host App", stateless_http=True) @mcp.tool() @@ -1313,12 +1322,19 @@ def domain_info() -> str: """Get domain-specific information""" return "This is served from mcp.acme.corp" +# Create lifespan context manager to initialize the session manager +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + """Context manager for managing MCP session manager lifecycle.""" + async with mcp.session_manager.run(): + yield # Mount using Host-based routing app = Starlette( routes=[ Host("mcp.acme.corp", app=mcp.streamable_http_app()), - ] + ], + lifespan=lifespan, ) ``` @@ -1335,6 +1351,7 @@ Example showing how to mount multiple StreamableHTTP servers with path configura Run from the repository root: uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload """ +import contextlib from starlette.applications import Starlette from starlette.routing import Mount @@ -1342,8 +1359,8 @@ from starlette.routing import Mount from mcp.server.fastmcp import FastMCP # Create multiple MCP servers -api_mcp = FastMCP("API Server") -chat_mcp = FastMCP("Chat Server") +api_mcp = FastMCP("API Server", stateless_http=True) +chat_mcp = FastMCP("Chat Server", stateless_http=True) @api_mcp.tool() @@ -1363,12 +1380,22 @@ def send_message(message: str) -> str: api_mcp.settings.streamable_http_path = "/" chat_mcp.settings.streamable_http_path = "/" +# Create lifespan context manager to initialize both session managers +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + """Context manager for managing multiple MCP session managers.""" + async with contextlib.AsyncExitStack() as stack: + await stack.enter_async_context(api_mcp.session_manager.run()) + await stack.enter_async_context(chat_mcp.session_manager.run()) + yield + # Mount the servers app = Starlette( routes=[ Mount("/api", app=api_mcp.streamable_http_app()), Mount("/chat", app=chat_mcp.streamable_http_app()), - ] + ], + lifespan=lifespan, ) ``` @@ -1385,6 +1412,7 @@ Example showing path configuration during FastMCP initialization. Run from the repository root: uvicorn examples.snippets.servers.streamable_http_path_config:app --reload """ +import contextlib from starlette.applications import Starlette from starlette.routing import Mount @@ -1393,7 +1421,7 @@ from mcp.server.fastmcp import FastMCP # Configure streamable_http_path during initialization # This server will mount at the root of wherever it's mounted -mcp_at_root = FastMCP("My Server", streamable_http_path="/") +mcp_at_root = FastMCP("My Server", streamable_http_path="/", stateless_http=True) @mcp_at_root.tool() @@ -1402,11 +1430,19 @@ def process_data(data: str) -> str: return f"Processed: {data}" +# Create lifespan context manager to initialize the session manager +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + """Context manager for managing MCP session manager lifecycle.""" + async with mcp_at_root.session_manager.run(): + yield + # Mount at /process - endpoints will be at /process instead of /process/mcp app = Starlette( routes=[ Mount("/process", app=mcp_at_root.streamable_http_app()), - ] + ], + lifespan=lifespan, ) ``` diff --git a/examples/snippets/servers/streamable_http_basic_mounting.py b/examples/snippets/servers/streamable_http_basic_mounting.py index abcc0e572..6486d7bb7 100644 --- a/examples/snippets/servers/streamable_http_basic_mounting.py +++ b/examples/snippets/servers/streamable_http_basic_mounting.py @@ -5,13 +5,15 @@ uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload """ +import contextlib + from starlette.applications import Starlette from starlette.routing import Mount from mcp.server.fastmcp import FastMCP # Create MCP server -mcp = FastMCP("My App") +mcp = FastMCP("My App", stateless_http=True) @mcp.tool() @@ -20,9 +22,18 @@ def hello() -> str: return "Hello from MCP!" +# Create lifespan context manager to initialize the session manager +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + """Context manager for managing MCP session manager lifecycle.""" + async with mcp.session_manager.run(): + yield + + # Mount the StreamableHTTP server to the existing ASGI server app = Starlette( routes=[ Mount("/", app=mcp.streamable_http_app()), - ] + ], + lifespan=lifespan, ) diff --git a/examples/snippets/servers/streamable_http_host_mounting.py b/examples/snippets/servers/streamable_http_host_mounting.py index d48558cc8..84d9c1e05 100644 --- a/examples/snippets/servers/streamable_http_host_mounting.py +++ b/examples/snippets/servers/streamable_http_host_mounting.py @@ -5,13 +5,15 @@ uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload """ +import contextlib + from starlette.applications import Starlette from starlette.routing import Host from mcp.server.fastmcp import FastMCP # Create MCP server -mcp = FastMCP("MCP Host App") +mcp = FastMCP("MCP Host App", stateless_http=True) @mcp.tool() @@ -20,9 +22,18 @@ def domain_info() -> str: return "This is served from mcp.acme.corp" +# Create lifespan context manager to initialize the session manager +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + """Context manager for managing MCP session manager lifecycle.""" + async with mcp.session_manager.run(): + yield + + # Mount using Host-based routing app = Starlette( routes=[ Host("mcp.acme.corp", app=mcp.streamable_http_app()), - ] + ], + lifespan=lifespan, ) diff --git a/examples/snippets/servers/streamable_http_multiple_servers.py b/examples/snippets/servers/streamable_http_multiple_servers.py index df347b7b3..4726ad82d 100644 --- a/examples/snippets/servers/streamable_http_multiple_servers.py +++ b/examples/snippets/servers/streamable_http_multiple_servers.py @@ -5,14 +5,16 @@ uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload """ +import contextlib + from starlette.applications import Starlette from starlette.routing import Mount from mcp.server.fastmcp import FastMCP # Create multiple MCP servers -api_mcp = FastMCP("API Server") -chat_mcp = FastMCP("Chat Server") +api_mcp = FastMCP("API Server", stateless_http=True) +chat_mcp = FastMCP("Chat Server", stateless_http=True) @api_mcp.tool() @@ -32,10 +34,22 @@ def send_message(message: str) -> str: api_mcp.settings.streamable_http_path = "/" chat_mcp.settings.streamable_http_path = "/" + +# Create lifespan context manager to initialize both session managers +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + """Context manager for managing multiple MCP session managers.""" + async with contextlib.AsyncExitStack() as stack: + await stack.enter_async_context(api_mcp.session_manager.run()) + await stack.enter_async_context(chat_mcp.session_manager.run()) + yield + + # Mount the servers app = Starlette( routes=[ Mount("/api", app=api_mcp.streamable_http_app()), Mount("/chat", app=chat_mcp.streamable_http_app()), - ] + ], + lifespan=lifespan, ) diff --git a/examples/snippets/servers/streamable_http_path_config.py b/examples/snippets/servers/streamable_http_path_config.py index 71228423e..fc431e3f4 100644 --- a/examples/snippets/servers/streamable_http_path_config.py +++ b/examples/snippets/servers/streamable_http_path_config.py @@ -5,6 +5,8 @@ uvicorn examples.snippets.servers.streamable_http_path_config:app --reload """ +import contextlib + from starlette.applications import Starlette from starlette.routing import Mount @@ -12,7 +14,7 @@ # Configure streamable_http_path during initialization # This server will mount at the root of wherever it's mounted -mcp_at_root = FastMCP("My Server", streamable_http_path="/") +mcp_at_root = FastMCP("My Server", streamable_http_path="/", stateless_http=True) @mcp_at_root.tool() @@ -21,9 +23,18 @@ def process_data(data: str) -> str: return f"Processed: {data}" +# Create lifespan context manager to initialize the session manager +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + """Context manager for managing MCP session manager lifecycle.""" + async with mcp_at_root.session_manager.run(): + yield + + # Mount at /process - endpoints will be at /process instead of /process/mcp app = Starlette( routes=[ Mount("/process", app=mcp_at_root.streamable_http_app()), - ] + ], + lifespan=lifespan, ) From 6c08e684e7bc65c2dc593a6a02fdfd6d50c8bd36 Mon Sep 17 00:00:00 2001 From: Ranjit Odedra Date: Sat, 18 Oct 2025 01:16:38 -0400 Subject: [PATCH 2/2] Update README snippets to match example files Run scripts/update_readme_snippets.py to sync documentation with code changes. --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 33db2eb7d..81c839c10 100644 --- a/README.md +++ b/README.md @@ -1261,6 +1261,7 @@ Basic example showing how to mount StreamableHTTP server in Starlette. Run from the repository root: uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload """ + import contextlib from starlette.applications import Starlette @@ -1277,6 +1278,7 @@ def hello() -> str: """A simple hello tool""" return "Hello from MCP!" + # Create lifespan context manager to initialize the session manager @contextlib.asynccontextmanager async def lifespan(app: Starlette): @@ -1284,6 +1286,7 @@ async def lifespan(app: Starlette): async with mcp.session_manager.run(): yield + # Mount the StreamableHTTP server to the existing ASGI server app = Starlette( routes=[ @@ -1306,6 +1309,7 @@ Example showing how to mount StreamableHTTP server using Host-based routing. Run from the repository root: uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload """ + import contextlib from starlette.applications import Starlette @@ -1322,6 +1326,7 @@ def domain_info() -> str: """Get domain-specific information""" return "This is served from mcp.acme.corp" + # Create lifespan context manager to initialize the session manager @contextlib.asynccontextmanager async def lifespan(app: Starlette): @@ -1329,6 +1334,7 @@ async def lifespan(app: Starlette): async with mcp.session_manager.run(): yield + # Mount using Host-based routing app = Starlette( routes=[ @@ -1351,6 +1357,7 @@ Example showing how to mount multiple StreamableHTTP servers with path configura Run from the repository root: uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload """ + import contextlib from starlette.applications import Starlette @@ -1380,6 +1387,7 @@ def send_message(message: str) -> str: api_mcp.settings.streamable_http_path = "/" chat_mcp.settings.streamable_http_path = "/" + # Create lifespan context manager to initialize both session managers @contextlib.asynccontextmanager async def lifespan(app: Starlette): @@ -1389,6 +1397,7 @@ async def lifespan(app: Starlette): await stack.enter_async_context(chat_mcp.session_manager.run()) yield + # Mount the servers app = Starlette( routes=[ @@ -1412,6 +1421,7 @@ Example showing path configuration during FastMCP initialization. Run from the repository root: uvicorn examples.snippets.servers.streamable_http_path_config:app --reload """ + import contextlib from starlette.applications import Starlette @@ -1437,6 +1447,7 @@ async def lifespan(app: Starlette): async with mcp_at_root.session_manager.run(): yield + # Mount at /process - endpoints will be at /process instead of /process/mcp app = Starlette( routes=[