Skip to content
55 changes: 51 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
)
```

Expand All @@ -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

Expand All @@ -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,
)
```

Expand All @@ -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

Expand Down Expand Up @@ -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,
)
```

Expand All @@ -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

Expand All @@ -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,
)
```

Expand Down
13 changes: 12 additions & 1 deletion examples/snippets/servers/streamable_http_basic_mounting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
)
13 changes: 12 additions & 1 deletion examples/snippets/servers/streamable_http_host_mounting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
)
16 changes: 15 additions & 1 deletion examples/snippets/servers/streamable_http_multiple_servers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
)
13 changes: 12 additions & 1 deletion examples/snippets/servers/streamable_http_path_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
)
55 changes: 55 additions & 0 deletions examples/snippets/servers/streamable_simple_lifespan.py
Original file line number Diff line number Diff line change
@@ -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)