diff --git a/README.md b/README.md index 5dbc4bd9d..ec87320db 100644 --- a/README.md +++ b/README.md @@ -1317,6 +1317,8 @@ 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 @@ -1332,11 +1334,20 @@ 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, ) ``` @@ -1354,6 +1365,8 @@ 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 @@ -1369,11 +1382,20 @@ 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, ) ``` @@ -1391,6 +1413,8 @@ 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 @@ -1418,12 +1442,24 @@ 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, ) ``` @@ -1441,6 +1477,8 @@ 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 @@ -1457,11 +1495,20 @@ 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..6337d50d5 100644 --- a/examples/snippets/servers/streamable_http_basic_mounting.py +++ b/examples/snippets/servers/streamable_http_basic_mounting.py @@ -5,6 +5,8 @@ uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload """ +import contextlib + from starlette.applications import Starlette from starlette.routing import Mount @@ -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..269eb5b0b 100644 --- a/examples/snippets/servers/streamable_http_host_mounting.py +++ b/examples/snippets/servers/streamable_http_host_mounting.py @@ -5,6 +5,8 @@ uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload """ +import contextlib + from starlette.applications import Starlette from starlette.routing import Host @@ -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..68d8bfc9c 100644 --- a/examples/snippets/servers/streamable_http_multiple_servers.py +++ b/examples/snippets/servers/streamable_http_multiple_servers.py @@ -5,6 +5,8 @@ uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload """ +import contextlib + from starlette.applications import Starlette from starlette.routing import Mount @@ -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..904c36ae2 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 @@ -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, ) diff --git a/examples/snippets/servers/streamable_simple_lifespan.py b/examples/snippets/servers/streamable_simple_lifespan.py new file mode 100644 index 000000000..c923cd414 --- /dev/null +++ b/examples/snippets/servers/streamable_simple_lifespan.py @@ -0,0 +1,55 @@ +""" +Example showing ASGI route mounting with lifespan context management. + +From the repository root: + cd examples/snippets/servers + uv run streamable_uvicorn_lifespan.py +""" + +import contextlib + +from starlette.applications import Starlette +from starlette.routing import Mount + +from mcp.server.fastmcp import FastMCP + +# Create MCP server +mcp = FastMCP(name="My App", stateless_http=True) + + +@mcp.tool() +def ping() -> str: + """A simple ping tool""" + return "pong" + + +# lifespan for managing the session manager +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + """Gather any session managers for startup/shutdown. + See streamable_starlette_mount.py for example of multiple mcp managers. + """ + async with mcp.session_manager.run(): + yield + + +"""Create the Starlette app and mount the MCP server. +lifespan ensures the session manager is started/stopped with the app. +session_manager references must only be made after streamable_http_app() +""" +app = Starlette( + routes=[ + # Mounted at /mcp + Mount("/", app=mcp.streamable_http_app()), + ], + lifespan=lifespan, +) + +if __name__ == "__main__": + import uvicorn + + """Attach to another ASGI server LIFO + ASGI chain: Uvicorn -> Starlette -> FastMCP + Route: http://0.0.0.0:8000/mcp + """ + uvicorn.run(app, host="0.0.0.0", port=8000)