refactor(examples): migrate all HTTP examples to streamable_http_app() (#2291)

This commit is contained in:
Max Isbey 2026-03-16 11:37:01 +00:00 • committed by GitHub
commit abfb482246
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 43 additions and 271 deletions

View file

@ -408,16 +408,10 @@ For custom error messages, call `task.fail()` before raising.
For web applications, use the Streamable HTTP transport:
```python
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
import uvicorn
from starlette.applications import Starlette
from starlette.routing import Mount
from mcp.server import Server
from mcp.server.experimental.task_context import ServerTaskContext
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from mcp.types import (
CallToolResult, CreateTaskResult, TextContent, Tool, ToolExecution, TASK_REQUIRED,
)
@ -462,22 +456,8 @@ async def handle_tool(name: str, arguments: dict) -> CallToolResult | CreateTask
return CallToolResult(content=[TextContent(type="text", text=f"Unknown: {name}")], isError=True)
def create_app():
session_manager = StreamableHTTPSessionManager(app=server)
@asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[None]:
async with session_manager.run():
yield
return Starlette(
routes=[Mount("/mcp", app=session_manager.handle_request)],
lifespan=lifespan,
)
if __name__ == "__main__":
uvicorn.run(create_app(), host="127.0.0.1", port=8000)
uvicorn.run(server.streamable_http_app(), host="127.0.0.1", port=8000)
```
## Testing Task Servers

View file

@ -4,14 +4,14 @@ A simple MCP server demonstrating pagination for tools, resources, and prompts u
## Usage
Start the server using either stdio (default) or SSE transport:
Start the server using either stdio (default) or Streamable HTTP transport:
```bash
# Using stdio transport (default)
uv run mcp-simple-pagination
# Using SSE transport on custom port
uv run mcp-simple-pagination --transport sse --port 8000
# Using Streamable HTTP transport on custom port
uv run mcp-simple-pagination --transport streamable-http --port 8000
```
The server exposes:

View file

@ -10,7 +10,6 @@ import anyio
import click
from mcp import types
from mcp.server import Server, ServerRequestContext
from starlette.requests import Request
T = TypeVar("T")
@ -143,10 +142,10 @@ async def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRe
@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")
@click.option("--port", default=8000, help="Port to listen on for HTTP")
@click.option(
"--transport",
type=click.Choice(["stdio", "sse"]),
type=click.Choice(["stdio", "streamable-http"]),
default="stdio",
help="Transport type",
)
@ -161,30 +160,10 @@ def main(port: int, transport: str) -> int:
on_get_prompt=handle_get_prompt,
)
if transport == "sse":
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Mount, Route
sse = SseServerTransport("/messages/")
async def handle_sse(request: Request):
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
await app.run(streams[0], streams[1], app.create_initialization_options())
return Response()
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse, methods=["GET"]),
Mount("/messages/", app=sse.handle_post_message),
],
)
if transport == "streamable-http":
import uvicorn
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
uvicorn.run(app.streamable_http_app(), host="127.0.0.1", port=port)
else:
from mcp.server.stdio import stdio_server

View file

@ -4,14 +4,14 @@ A simple MCP server that exposes a customizable prompt template with optional co
## Usage
Start the server using either stdio (default) or SSE transport:
Start the server using either stdio (default) or Streamable HTTP transport:
```bash
# Using stdio transport (default)
uv run mcp-simple-prompt
# Using SSE transport on custom port
uv run mcp-simple-prompt --transport sse --port 8000
# Using Streamable HTTP transport on custom port
uv run mcp-simple-prompt --transport streamable-http --port 8000
```
The server exposes a prompt named "simple" that accepts two optional arguments:

View file

@ -2,7 +2,6 @@ import anyio
import click
from mcp import types
from mcp.server import Server, ServerRequestContext
from starlette.requests import Request
def create_messages(context: str | None = None, topic: str | None = None) -> list[types.PromptMessage]:
@ -69,10 +68,10 @@ async def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRe
@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")
@click.option("--port", default=8000, help="Port to listen on for HTTP")
@click.option(
"--transport",
type=click.Choice(["stdio", "sse"]),
type=click.Choice(["stdio", "streamable-http"]),
default="stdio",
help="Transport type",
)
@ -83,30 +82,10 @@ def main(port: int, transport: str) -> int:
on_get_prompt=handle_get_prompt,
)
if transport == "sse":
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Mount, Route
sse = SseServerTransport("/messages/")
async def handle_sse(request: Request):
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
await app.run(streams[0], streams[1], app.create_initialization_options())
return Response()
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse),
Mount("/messages/", app=sse.handle_post_message),
],
)
if transport == "streamable-http":
import uvicorn
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
uvicorn.run(app.streamable_http_app(), host="127.0.0.1", port=port)
else:
from mcp.server.stdio import stdio_server

View file

@ -4,14 +4,14 @@ A simple MCP server that exposes sample text files as resources.
## Usage
Start the server using either stdio (default) or SSE transport:
Start the server using either stdio (default) or Streamable HTTP transport:
```bash
# Using stdio transport (default)
uv run mcp-simple-resource
# Using SSE transport on custom port
uv run mcp-simple-resource --transport sse --port 8000
# Using Streamable HTTP transport on custom port
uv run mcp-simple-resource --transport streamable-http --port 8000
```
The server exposes some basic text file resources that can be read by clients.

View file

@ -4,7 +4,6 @@ import anyio
import click
from mcp import types
from mcp.server import Server, ServerRequestContext
from starlette.requests import Request
SAMPLE_RESOURCES = {
"greeting": {
@ -62,10 +61,10 @@ async def handle_read_resource(
@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")
@click.option("--port", default=8000, help="Port to listen on for HTTP")
@click.option(
"--transport",
type=click.Choice(["stdio", "sse"]),
type=click.Choice(["stdio", "streamable-http"]),
default="stdio",
help="Transport type",
)
@ -76,30 +75,10 @@ def main(port: int, transport: str) -> int:
on_read_resource=handle_read_resource,
)
if transport == "sse":
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Mount, Route
sse = SseServerTransport("/messages/")
async def handle_sse(request: Request):
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
await app.run(streams[0], streams[1], app.create_initialization_options())
return Response()
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse, methods=["GET"]),
Mount("/messages/", app=sse.handle_post_message),
],
)
if transport == "streamable-http":
import uvicorn
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
uvicorn.run(app.streamable_http_app(), host="127.0.0.1", port=port)
else:
from mcp.server.stdio import stdio_server

View file

@ -7,7 +7,6 @@ A stateless MCP server example demonstrating the StreamableHttp transport withou
- Uses the StreamableHTTP transport in stateless mode (mcp_session_id=None)
- Each request creates a new ephemeral connection
- No session state maintained between requests
- Task lifecycle scoped to individual requests
- Suitable for deployment in multi-node environments
## Usage

View file

@ -1,17 +1,11 @@
import contextlib
import logging
from collections.abc import AsyncIterator
import anyio
import click
import uvicorn
from mcp import types
from mcp.server import Server, ServerRequestContext
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
logger = logging.getLogger(__name__)
@ -104,39 +98,17 @@ def main(
on_call_tool=handle_call_tool,
)
# Create the session manager with true stateless mode
session_manager = StreamableHTTPSessionManager(
app=app,
event_store=None,
starlette_app = app.streamable_http_app(
stateless_http=True,
json_response=json_response,
stateless=True,
)
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
await session_manager.handle_request(scope, receive, send)
@contextlib.asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[None]:
"""Context manager for session manager."""
async with session_manager.run():
logger.info("Application started with StreamableHTTP session manager!")
try:
yield
finally:
logger.info("Application shutting down...")
# Create an ASGI application using the transport
starlette_app = Starlette(
debug=True,
routes=[Mount("/mcp", app=handle_streamable_http)],
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_origins=["*"], # Note: streamable_http_app() enforces localhost-only Origin by default
allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods
expose_headers=["Mcp-Session-Id"],
)

View file

@ -6,9 +6,7 @@ A simple MCP server example demonstrating the StreamableHttp transport, which en
- Uses the StreamableHTTP transport for server-client communication
- Supports REST API operations (POST, GET, DELETE) for `/mcp` endpoint
- Task management with anyio task groups
- Ability to send multiple notifications over time to the client
- Proper resource cleanup and lifespan management
- Resumability support via InMemoryEventStore
## Usage

View file

@ -1,16 +1,11 @@
import contextlib
import logging
from collections.abc import AsyncIterator
import anyio
import click
import uvicorn
from mcp import types
from mcp.server import Server, ServerRequestContext
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
from .event_store import InMemoryEventStore
@ -127,47 +122,21 @@ def main(
# For production, use a persistent storage solution.
event_store = InMemoryEventStore()
# Create the session manager with our app and event store
session_manager = StreamableHTTPSessionManager(
app=app,
event_store=event_store, # Enable resumability
starlette_app = app.streamable_http_app(
event_store=event_store,
json_response=json_response,
)
# ASGI handler for streamable HTTP connections
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
await session_manager.handle_request(scope, receive, send)
@contextlib.asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[None]:
"""Context manager for managing session manager lifecycle."""
async with session_manager.run():
logger.info("Application started with StreamableHTTP session manager!")
try:
yield
finally:
logger.info("Application shutting down...")
# Create an ASGI application using the transport
starlette_app = Starlette(
debug=True,
routes=[
Mount("/mcp", app=handle_streamable_http),
],
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_origins=["*"], # Note: streamable_http_app() enforces localhost-only Origin by default
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)
return 0

View file

@ -6,8 +6,6 @@ This example shows the simplified task API where:
- ServerTaskContext.elicit() and ServerTaskContext.create_message() queue requests properly
"""
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any
import click
@ -15,9 +13,6 @@ import uvicorn
from mcp import types
from mcp.server import Server, ServerRequestContext
from mcp.server.experimental.task_context import ServerTaskContext
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.routing import Mount
async def handle_list_tools(
@ -134,23 +129,10 @@ server = Server(
server.experimental.enable_tasks()
def create_app(session_manager: StreamableHTTPSessionManager) -> Starlette:
@asynccontextmanager
async def app_lifespan(app: Starlette) -> AsyncIterator[None]:
async with session_manager.run():
yield
return Starlette(
routes=[Mount("/mcp", app=session_manager.handle_request)],
lifespan=app_lifespan,
)
@click.command()
@click.option("--port", default=8000, help="Port to listen on")
def main(port: int) -> int:
session_manager = StreamableHTTPSessionManager(app=server)
starlette_app = create_app(session_manager)
starlette_app = server.streamable_http_app()
print(f"Starting server on http://localhost:{port}/mcp")
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
return 0

View file

@ -1,17 +1,11 @@
"""Simple task server demonstrating MCP tasks over streamable HTTP."""
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
import anyio
import click
import uvicorn
from mcp import types
from mcp.server import Server, ServerRequestContext
from mcp.server.experimental.task_context import ServerTaskContext
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.routing import Mount
async def handle_list_tools(
@ -69,17 +63,7 @@ server.experimental.enable_tasks()
@click.command()
@click.option("--port", default=8000, help="Port to listen on")
def main(port: int) -> int:
session_manager = StreamableHTTPSessionManager(app=server)
@asynccontextmanager
async def app_lifespan(app: Starlette) -> AsyncIterator[None]:
async with session_manager.run():
yield
starlette_app = Starlette(
routes=[Mount("/mcp", app=session_manager.handle_request)],
lifespan=app_lifespan,
)
starlette_app = server.streamable_http_app()
print(f"Starting server on http://localhost:{port}/mcp")
uvicorn.run(starlette_app, host="127.0.0.1", port=port)

View file

@ -3,14 +3,14 @@ A simple MCP server that exposes a website fetching tool.
## Usage
Start the server using either stdio (default) or SSE transport:
Start the server using either stdio (default) or Streamable HTTP transport:
```bash
# Using stdio transport (default)
uv run mcp-simple-tool
# Using SSE transport on custom port
uv run mcp-simple-tool --transport sse --port 8000
# Using Streamable HTTP transport on custom port
uv run mcp-simple-tool --transport streamable-http --port 8000
```
The server exposes a tool named "fetch" that accepts one required argument:

View file

@ -3,7 +3,6 @@ import click
from mcp import types
from mcp.server import Server, ServerRequestContext
from mcp.shared._httpx_utils import create_mcp_http_client
from starlette.requests import Request
async def fetch_website(
@ -51,10 +50,10 @@ async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequ
@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")
@click.option("--port", default=8000, help="Port to listen on for HTTP")
@click.option(
"--transport",
type=click.Choice(["stdio", "sse"]),
type=click.Choice(["stdio", "streamable-http"]),
default="stdio",
help="Transport type",
)
@ -65,30 +64,10 @@ def main(port: int, transport: str) -> int:
on_call_tool=handle_call_tool,
)
if transport == "sse":
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Mount, Route
sse = SseServerTransport("/messages/")
async def handle_sse(request: Request):
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
await app.run(streams[0], streams[1], app.create_initialization_options())
return Response()
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse, methods=["GET"]),
Mount("/messages/", app=sse.handle_post_message),
],
)
if transport == "streamable-http":
import uvicorn
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
uvicorn.run(app.streamable_http_app(), host="127.0.0.1", port=port)
else:
from mcp.server.stdio import stdio_server

View file

@ -12,18 +12,13 @@ Run with:
uv run mcp-sse-polling-demo --port 3000
"""
import contextlib
import logging
from collections.abc import AsyncIterator
import anyio
import click
import uvicorn
from mcp import types
from mcp.server import Server, ServerRequestContext
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.routing import Mount
from starlette.types import Receive, Scope, Send
from .event_store import InMemoryEventStore
@ -149,37 +144,14 @@ def main(port: int, log_level: str, retry_interval: int) -> int:
on_call_tool=handle_call_tool,
)
# Create event store for resumability
event_store = InMemoryEventStore()
# Create session manager with event store and retry interval
session_manager = StreamableHTTPSessionManager(
app=app,
event_store=event_store,
starlette_app = app.streamable_http_app(
event_store=InMemoryEventStore(),
retry_interval=retry_interval,
)
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
await session_manager.handle_request(scope, receive, send)
@contextlib.asynccontextmanager
async def lifespan(starlette_app: Starlette) -> AsyncIterator[None]:
async with session_manager.run():
logger.info(f"SSE Polling Demo server started on port {port}")
logger.info("Try: POST /mcp with tools/call for 'process_batch'")
yield
logger.info("Server shutting down...")
starlette_app = Starlette(
debug=True,
routes=[
Mount("/mcp", app=handle_streamable_http),
],
lifespan=lifespan,
)
import uvicorn
logger.info(f"SSE Polling Demo server starting on port {port}")
logger.info("Try: POST /mcp with tools/call for 'process_batch'")
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
return 0