Skip to content

Commit 8bcf9e9

Browse files
committed
refactor(examples): migrate all HTTP examples to streamable_http_app()
Per review from pcarleton on #2291: - simple-streamablehttp, simple-streamablehttp-stateless: use app.streamable_http_app() with CORS wrapped around the returned Starlette app. Removes manual StreamableHTTPSessionManager wiring. - simple-tool, simple-prompt, simple-resource, simple-pagination: replace --transport sse (legacy) with --transport streamable-http using app.streamable_http_app(). READMEs updated to match. - docs/experimental/tasks-server.md: use server.streamable_http_app() instead of manual wiring. All 9 examples now get DNS rebinding protection via the auto-enable in streamable_http_app() — zero explicit TransportSecuritySettings needed. Verified live: 45/45 probes pass (421 for bad Host, 403 for bad Origin).
1 parent 2ea2b19 commit 8bcf9e9

File tree

11 files changed

+34
-242
lines changed

11 files changed

+34
-242
lines changed

docs/experimental/tasks-server.md

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -408,17 +408,10 @@ For custom error messages, call `task.fail()` before raising.
408408
For web applications, use the Streamable HTTP transport:
409409

410410
```python
411-
from collections.abc import AsyncIterator
412-
from contextlib import asynccontextmanager
413-
414411
import uvicorn
415-
from starlette.applications import Starlette
416-
from starlette.routing import Mount
417412

418413
from mcp.server import Server
419414
from mcp.server.experimental.task_context import ServerTaskContext
420-
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
421-
from mcp.server.transport_security import TransportSecuritySettings
422415
from mcp.types import (
423416
CallToolResult, CreateTaskResult, TextContent, Tool, ToolExecution, TASK_REQUIRED,
424417
)
@@ -463,28 +456,8 @@ async def handle_tool(name: str, arguments: dict) -> CallToolResult | CreateTask
463456
return CallToolResult(content=[TextContent(type="text", text=f"Unknown: {name}")], isError=True)
464457

465458

466-
def create_app():
467-
session_manager = StreamableHTTPSessionManager(
468-
app=server,
469-
security_settings=TransportSecuritySettings(
470-
allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*"],
471-
allowed_origins=["http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"],
472-
),
473-
)
474-
475-
@asynccontextmanager
476-
async def lifespan(app: Starlette) -> AsyncIterator[None]:
477-
async with session_manager.run():
478-
yield
479-
480-
return Starlette(
481-
routes=[Mount("/mcp", app=session_manager.handle_request)],
482-
lifespan=lifespan,
483-
)
484-
485-
486459
if __name__ == "__main__":
487-
uvicorn.run(create_app(), host="127.0.0.1", port=8000)
460+
uvicorn.run(server.streamable_http_app(), host="127.0.0.1", port=8000)
488461
```
489462

490463
## Testing Task Servers

examples/servers/simple-pagination/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ A simple MCP server demonstrating pagination for tools, resources, and prompts u
44

55
## Usage
66

7-
Start the server using either stdio (default) or SSE transport:
7+
Start the server using either stdio (default) or Streamable HTTP transport:
88

99
```bash
1010
# Using stdio transport (default)
1111
uv run mcp-simple-pagination
1212

13-
# Using SSE transport on custom port
14-
uv run mcp-simple-pagination --transport sse --port 8000
13+
# Using Streamable HTTP transport on custom port
14+
uv run mcp-simple-pagination --transport streamable-http --port 8000
1515
```
1616

1717
The server exposes:

examples/servers/simple-pagination/mcp_simple_pagination/server.py

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import click
1111
from mcp import types
1212
from mcp.server import Server, ServerRequestContext
13-
from starlette.requests import Request
1413

1514
T = TypeVar("T")
1615

@@ -143,10 +142,10 @@ async def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRe
143142

144143

145144
@click.command()
146-
@click.option("--port", default=8000, help="Port to listen on for SSE")
145+
@click.option("--port", default=8000, help="Port to listen on for HTTP")
147146
@click.option(
148147
"--transport",
149-
type=click.Choice(["stdio", "sse"]),
148+
type=click.Choice(["stdio", "streamable-http"]),
150149
default="stdio",
151150
help="Transport type",
152151
)
@@ -161,37 +160,10 @@ def main(port: int, transport: str) -> int:
161160
on_get_prompt=handle_get_prompt,
162161
)
163162

164-
if transport == "sse":
165-
from mcp.server.sse import SseServerTransport
166-
from mcp.server.transport_security import TransportSecuritySettings
167-
from starlette.applications import Starlette
168-
from starlette.responses import Response
169-
from starlette.routing import Mount, Route
170-
171-
sse = SseServerTransport(
172-
"/messages/",
173-
security_settings=TransportSecuritySettings(
174-
allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*"],
175-
allowed_origins=["http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"],
176-
),
177-
)
178-
179-
async def handle_sse(request: Request):
180-
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
181-
await app.run(streams[0], streams[1], app.create_initialization_options())
182-
return Response()
183-
184-
starlette_app = Starlette(
185-
debug=True,
186-
routes=[
187-
Route("/sse", endpoint=handle_sse, methods=["GET"]),
188-
Mount("/messages/", app=sse.handle_post_message),
189-
],
190-
)
191-
163+
if transport == "streamable-http":
192164
import uvicorn
193165

194-
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
166+
uvicorn.run(app.streamable_http_app(), host="127.0.0.1", port=port)
195167
else:
196168
from mcp.server.stdio import stdio_server
197169

examples/servers/simple-prompt/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ A simple MCP server that exposes a customizable prompt template with optional co
44

55
## Usage
66

7-
Start the server using either stdio (default) or SSE transport:
7+
Start the server using either stdio (default) or Streamable HTTP transport:
88

99
```bash
1010
# Using stdio transport (default)
1111
uv run mcp-simple-prompt
1212

13-
# Using SSE transport on custom port
14-
uv run mcp-simple-prompt --transport sse --port 8000
13+
# Using Streamable HTTP transport on custom port
14+
uv run mcp-simple-prompt --transport streamable-http --port 8000
1515
```
1616

1717
The server exposes a prompt named "simple" that accepts two optional arguments:

examples/servers/simple-prompt/mcp_simple_prompt/server.py

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import click
33
from mcp import types
44
from mcp.server import Server, ServerRequestContext
5-
from starlette.requests import Request
65

76

87
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
6968

7069

7170
@click.command()
72-
@click.option("--port", default=8000, help="Port to listen on for SSE")
71+
@click.option("--port", default=8000, help="Port to listen on for HTTP")
7372
@click.option(
7473
"--transport",
75-
type=click.Choice(["stdio", "sse"]),
74+
type=click.Choice(["stdio", "streamable-http"]),
7675
default="stdio",
7776
help="Transport type",
7877
)
@@ -83,37 +82,10 @@ def main(port: int, transport: str) -> int:
8382
on_get_prompt=handle_get_prompt,
8483
)
8584

86-
if transport == "sse":
87-
from mcp.server.sse import SseServerTransport
88-
from mcp.server.transport_security import TransportSecuritySettings
89-
from starlette.applications import Starlette
90-
from starlette.responses import Response
91-
from starlette.routing import Mount, Route
92-
93-
sse = SseServerTransport(
94-
"/messages/",
95-
security_settings=TransportSecuritySettings(
96-
allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*"],
97-
allowed_origins=["http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"],
98-
),
99-
)
100-
101-
async def handle_sse(request: Request):
102-
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
103-
await app.run(streams[0], streams[1], app.create_initialization_options())
104-
return Response()
105-
106-
starlette_app = Starlette(
107-
debug=True,
108-
routes=[
109-
Route("/sse", endpoint=handle_sse),
110-
Mount("/messages/", app=sse.handle_post_message),
111-
],
112-
)
113-
85+
if transport == "streamable-http":
11486
import uvicorn
11587

116-
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
88+
uvicorn.run(app.streamable_http_app(), host="127.0.0.1", port=port)
11789
else:
11890
from mcp.server.stdio import stdio_server
11991

examples/servers/simple-resource/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ A simple MCP server that exposes sample text files as resources.
44

55
## Usage
66

7-
Start the server using either stdio (default) or SSE transport:
7+
Start the server using either stdio (default) or Streamable HTTP transport:
88

99
```bash
1010
# Using stdio transport (default)
1111
uv run mcp-simple-resource
1212

13-
# Using SSE transport on custom port
14-
uv run mcp-simple-resource --transport sse --port 8000
13+
# Using Streamable HTTP transport on custom port
14+
uv run mcp-simple-resource --transport streamable-http --port 8000
1515
```
1616

1717
The server exposes some basic text file resources that can be read by clients.

examples/servers/simple-resource/mcp_simple_resource/server.py

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import click
55
from mcp import types
66
from mcp.server import Server, ServerRequestContext
7-
from starlette.requests import Request
87

98
SAMPLE_RESOURCES = {
109
"greeting": {
@@ -62,10 +61,10 @@ async def handle_read_resource(
6261

6362

6463
@click.command()
65-
@click.option("--port", default=8000, help="Port to listen on for SSE")
64+
@click.option("--port", default=8000, help="Port to listen on for HTTP")
6665
@click.option(
6766
"--transport",
68-
type=click.Choice(["stdio", "sse"]),
67+
type=click.Choice(["stdio", "streamable-http"]),
6968
default="stdio",
7069
help="Transport type",
7170
)
@@ -76,37 +75,10 @@ def main(port: int, transport: str) -> int:
7675
on_read_resource=handle_read_resource,
7776
)
7877

79-
if transport == "sse":
80-
from mcp.server.sse import SseServerTransport
81-
from mcp.server.transport_security import TransportSecuritySettings
82-
from starlette.applications import Starlette
83-
from starlette.responses import Response
84-
from starlette.routing import Mount, Route
85-
86-
sse = SseServerTransport(
87-
"/messages/",
88-
security_settings=TransportSecuritySettings(
89-
allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*"],
90-
allowed_origins=["http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"],
91-
),
92-
)
93-
94-
async def handle_sse(request: Request):
95-
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
96-
await app.run(streams[0], streams[1], app.create_initialization_options())
97-
return Response()
98-
99-
starlette_app = Starlette(
100-
debug=True,
101-
routes=[
102-
Route("/sse", endpoint=handle_sse, methods=["GET"]),
103-
Mount("/messages/", app=sse.handle_post_message),
104-
],
105-
)
106-
78+
if transport == "streamable-http":
10779
import uvicorn
10880

109-
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
81+
uvicorn.run(app.streamable_http_app(), host="127.0.0.1", port=port)
11082
else:
11183
from mcp.server.stdio import stdio_server
11284

examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
1-
import contextlib
21
import logging
3-
from collections.abc import AsyncIterator
42

53
import anyio
64
import click
75
import uvicorn
86
from mcp import types
97
from mcp.server import Server, ServerRequestContext
10-
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
11-
from mcp.server.transport_security import TransportSecuritySettings
12-
from starlette.applications import Starlette
138
from starlette.middleware.cors import CORSMiddleware
14-
from starlette.routing import Mount
15-
from starlette.types import Receive, Scope, Send
169

1710
logger = logging.getLogger(__name__)
1811

@@ -105,36 +98,10 @@ def main(
10598
on_call_tool=handle_call_tool,
10699
)
107100

108-
# Create the session manager with true stateless mode
109-
session_manager = StreamableHTTPSessionManager(
110-
app=app,
111-
event_store=None,
101+
starlette_app = app.streamable_http_app(
102+
stateless_http=True,
112103
json_response=json_response,
113-
stateless=True,
114-
security_settings=TransportSecuritySettings(
115-
allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*"],
116-
allowed_origins=["http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"],
117-
),
118-
)
119-
120-
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
121-
await session_manager.handle_request(scope, receive, send)
122-
123-
@contextlib.asynccontextmanager
124-
async def lifespan(app: Starlette) -> AsyncIterator[None]:
125-
"""Context manager for session manager."""
126-
async with session_manager.run():
127-
logger.info("Application started with StreamableHTTP session manager!")
128-
try:
129-
yield
130-
finally:
131-
logger.info("Application shutting down...")
132-
133-
# Create an ASGI application using the transport
134-
starlette_app = Starlette(
135104
debug=True,
136-
routes=[Mount("/mcp", app=handle_streamable_http)],
137-
lifespan=lifespan,
138105
)
139106

140107
# Wrap ASGI application with CORS middleware to expose Mcp-Session-Id header

0 commit comments

Comments
 (0)