diff --git a/docs/experimental/tasks-server.md b/docs/experimental/tasks-server.md index 761dc5de5..b350ee3bb 100644 --- a/docs/experimental/tasks-server.md +++ b/docs/experimental/tasks-server.md @@ -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 diff --git a/examples/servers/simple-pagination/README.md b/examples/servers/simple-pagination/README.md index e732b8efb..4cab40fd3 100644 --- a/examples/servers/simple-pagination/README.md +++ b/examples/servers/simple-pagination/README.md @@ -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: diff --git a/examples/servers/simple-pagination/mcp_simple_pagination/server.py b/examples/servers/simple-pagination/mcp_simple_pagination/server.py index bac27a0f1..c94f2ac3d 100644 --- a/examples/servers/simple-pagination/mcp_simple_pagination/server.py +++ b/examples/servers/simple-pagination/mcp_simple_pagination/server.py @@ -10,7 +10,6 @@ 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 diff --git a/examples/servers/simple-prompt/README.md b/examples/servers/simple-prompt/README.md index 48e796e19..c837da876 100644 --- a/examples/servers/simple-prompt/README.md +++ b/examples/servers/simple-prompt/README.md @@ -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: diff --git a/examples/servers/simple-prompt/mcp_simple_prompt/server.py b/examples/servers/simple-prompt/mcp_simple_prompt/server.py index 6cf99d4b6..74b71b3f3 100644 --- a/examples/servers/simple-prompt/mcp_simple_prompt/server.py +++ b/examples/servers/simple-prompt/mcp_simple_prompt/server.py @@ -2,7 +2,6 @@ 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 diff --git a/examples/servers/simple-resource/README.md b/examples/servers/simple-resource/README.md index df674e91e..7fb2ab7cd 100644 --- a/examples/servers/simple-resource/README.md +++ b/examples/servers/simple-resource/README.md @@ -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. diff --git a/examples/servers/simple-resource/mcp_simple_resource/server.py b/examples/servers/simple-resource/mcp_simple_resource/server.py index b9b6a1d96..8d1105414 100644 --- a/examples/servers/simple-resource/mcp_simple_resource/server.py +++ b/examples/servers/simple-resource/mcp_simple_resource/server.py @@ -4,7 +4,6 @@ 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 diff --git a/examples/servers/simple-streamablehttp-stateless/README.md b/examples/servers/simple-streamablehttp-stateless/README.md index b87250b35..a254f88d1 100644 --- a/examples/servers/simple-streamablehttp-stateless/README.md +++ b/examples/servers/simple-streamablehttp-stateless/README.md @@ -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 diff --git a/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py b/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py index cb4a6503c..e2b8d2ef2 100644 --- a/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py +++ b/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py @@ -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"], ) diff --git a/examples/servers/simple-streamablehttp/README.md b/examples/servers/simple-streamablehttp/README.md index 983636717..3eed3320e 100644 --- a/examples/servers/simple-streamablehttp/README.md +++ b/examples/servers/simple-streamablehttp/README.md @@ -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 diff --git a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py index 2f2a53b1b..ec9761d1b 100644 --- a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py +++ b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py @@ -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 diff --git a/examples/servers/simple-task-interactive/mcp_simple_task_interactive/server.py b/examples/servers/simple-task-interactive/mcp_simple_task_interactive/server.py index 6938b6552..bc06e1208 100644 --- a/examples/servers/simple-task-interactive/mcp_simple_task_interactive/server.py +++ b/examples/servers/simple-task-interactive/mcp_simple_task_interactive/server.py @@ -6,8 +6,6 @@ - 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 @@ 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 @@ async def handle_call_tool( 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 diff --git a/examples/servers/simple-task/mcp_simple_task/server.py b/examples/servers/simple-task/mcp_simple_task/server.py index 50ae3ca9a..7583cd8f0 100644 --- a/examples/servers/simple-task/mcp_simple_task/server.py +++ b/examples/servers/simple-task/mcp_simple_task/server.py @@ -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 @@ async def work(task: ServerTaskContext) -> types.CallToolResult: @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) diff --git a/examples/servers/simple-tool/README.md b/examples/servers/simple-tool/README.md index 06020b4b0..7d3759f9d 100644 --- a/examples/servers/simple-tool/README.md +++ b/examples/servers/simple-tool/README.md @@ -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: diff --git a/examples/servers/simple-tool/mcp_simple_tool/server.py b/examples/servers/simple-tool/mcp_simple_tool/server.py index 9fe71e5b7..226058b95 100644 --- a/examples/servers/simple-tool/mcp_simple_tool/server.py +++ b/examples/servers/simple-tool/mcp_simple_tool/server.py @@ -3,7 +3,6 @@ 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 diff --git a/examples/servers/sse-polling-demo/mcp_sse_polling_demo/server.py b/examples/servers/sse-polling-demo/mcp_sse_polling_demo/server.py index c8178c35a..14bc174c4 100644 --- a/examples/servers/sse-polling-demo/mcp_sse_polling_demo/server.py +++ b/examples/servers/sse-polling-demo/mcp_sse_polling_demo/server.py @@ -12,18 +12,13 @@ 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