Skip to content

Commit ede25d3

Browse files
authored
Merge branch 'main' into pcarleton/auth-confused-deputy
2 parents d549868 + ed25167 commit ede25d3

File tree

19 files changed

+1235
-254
lines changed

19 files changed

+1235
-254
lines changed

README.md

+135-3
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ The Model Context Protocol allows applications to provide context for LLMs in a
6666

6767
- Build MCP clients that can connect to any MCP server
6868
- Create MCP servers that expose resources, prompts and tools
69-
- Use standard transports like stdio and SSE
69+
- Use standard transports like stdio, SSE, and Streamable HTTP
7070
- Handle all MCP protocol messages and lifecycle events
7171

7272
## Installation
@@ -334,7 +334,7 @@ mcp = FastMCP("My App",
334334
)
335335
```
336336

337-
See [OAuthServerProvider](mcp/server/auth/provider.py) for more details.
337+
See [OAuthServerProvider](src/mcp/server/auth/provider.py) for more details.
338338

339339
## Running Your Server
340340

@@ -387,8 +387,81 @@ python server.py
387387
mcp run server.py
388388
```
389389

390+
### Streamable HTTP Transport
391+
392+
> **Note**: Streamable HTTP transport is superseding SSE transport for production deployments.
393+
394+
```python
395+
from mcp.server.fastmcp import FastMCP
396+
397+
# Stateful server (maintains session state)
398+
mcp = FastMCP("StatefulServer")
399+
400+
# Stateless server (no session persistence)
401+
mcp = FastMCP("StatelessServer", stateless_http=True)
402+
403+
# Run server with streamable_http transport
404+
mcp.run(transport="streamable-http")
405+
```
406+
407+
You can mount multiple FastMCP servers in a FastAPI application:
408+
409+
```python
410+
# echo.py
411+
from mcp.server.fastmcp import FastMCP
412+
413+
mcp = FastMCP(name="EchoServer", stateless_http=True)
414+
415+
416+
@mcp.tool(description="A simple echo tool")
417+
def echo(message: str) -> str:
418+
return f"Echo: {message}"
419+
```
420+
421+
```python
422+
# math.py
423+
from mcp.server.fastmcp import FastMCP
424+
425+
mcp = FastMCP(name="MathServer", stateless_http=True)
426+
427+
428+
@mcp.tool(description="A simple add tool")
429+
def add_two(n: int) -> str:
430+
return n + 2
431+
```
432+
433+
```python
434+
# main.py
435+
from fastapi import FastAPI
436+
from mcp.echo import echo
437+
from mcp.math import math
438+
439+
440+
app = FastAPI()
441+
442+
# Use the session manager's lifespan
443+
app = FastAPI(lifespan=lambda app: echo.mcp.session_manager.run())
444+
app.mount("/echo", echo.mcp.streamable_http_app())
445+
app.mount("/math", math.mcp.streamable_http_app())
446+
```
447+
448+
For low level server with Streamable HTTP implementations, see:
449+
- Stateful server: [`examples/servers/simple-streamablehttp/`](examples/servers/simple-streamablehttp/)
450+
- Stateless server: [`examples/servers/simple-streamablehttp-stateless/`](examples/servers/simple-streamablehttp-stateless/)
451+
452+
453+
454+
The streamable HTTP transport supports:
455+
- Stateful and stateless operation modes
456+
- Resumability with event stores
457+
- JSON or SSE response formats
458+
- Better scalability for multi-node deployments
459+
460+
390461
### Mounting to an Existing ASGI Server
391462

463+
> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).
464+
392465
You can mount the SSE server to an existing ASGI server using the `sse_app` method. This allows you to integrate the SSE server with other ASGI applications.
393466

394467
```python
@@ -410,6 +483,43 @@ app = Starlette(
410483
app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app()))
411484
```
412485

486+
When mounting multiple MCP servers under different paths, you can configure the mount path in several ways:
487+
488+
```python
489+
from starlette.applications import Starlette
490+
from starlette.routing import Mount
491+
from mcp.server.fastmcp import FastMCP
492+
493+
# Create multiple MCP servers
494+
github_mcp = FastMCP("GitHub API")
495+
browser_mcp = FastMCP("Browser")
496+
curl_mcp = FastMCP("Curl")
497+
search_mcp = FastMCP("Search")
498+
499+
# Method 1: Configure mount paths via settings (recommended for persistent configuration)
500+
github_mcp.settings.mount_path = "/github"
501+
browser_mcp.settings.mount_path = "/browser"
502+
503+
# Method 2: Pass mount path directly to sse_app (preferred for ad-hoc mounting)
504+
# This approach doesn't modify the server's settings permanently
505+
506+
# Create Starlette app with multiple mounted servers
507+
app = Starlette(
508+
routes=[
509+
# Using settings-based configuration
510+
Mount("/github", app=github_mcp.sse_app()),
511+
Mount("/browser", app=browser_mcp.sse_app()),
512+
# Using direct mount path parameter
513+
Mount("/curl", app=curl_mcp.sse_app("/curl")),
514+
Mount("/search", app=search_mcp.sse_app("/search")),
515+
]
516+
)
517+
518+
# Method 3: For direct execution, you can also pass the mount path to run()
519+
if __name__ == "__main__":
520+
search_mcp.run(transport="sse", mount_path="/search")
521+
```
522+
413523
For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes).
414524

415525
## Examples
@@ -584,7 +694,7 @@ if __name__ == "__main__":
584694

585695
### Writing MCP Clients
586696

587-
The SDK provides a high-level client interface for connecting to MCP servers:
697+
The SDK provides a high-level client interface for connecting to MCP servers using various [transports](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports):
588698

589699
```python
590700
from mcp import ClientSession, StdioServerParameters, types
@@ -648,6 +758,28 @@ if __name__ == "__main__":
648758
asyncio.run(run())
649759
```
650760

761+
Clients can also connect using [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http):
762+
763+
```python
764+
from mcp.client.streamable_http import streamablehttp_client
765+
from mcp import ClientSession
766+
767+
768+
async def main():
769+
# Connect to a streamable HTTP server
770+
async with streamablehttp_client("example/mcp") as (
771+
read_stream,
772+
write_stream,
773+
_,
774+
):
775+
# Create a session using the client streams
776+
async with ClientSession(read_stream, write_stream) as session:
777+
# Initialize the connection
778+
await session.initialize()
779+
# Call a tool
780+
tool_result = await session.call_tool("echo", {"message": "hello"})
781+
```
782+
651783
### MCP Primitives
652784

653785
The MCP protocol defines three core primitives that servers can implement:

examples/servers/simple-auth/mcp_simple_auth/server.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import Any
77

88
import click
9-
import httpx
9+
1010
from pydantic import AnyHttpUrl
1111
from pydantic_settings import BaseSettings, SettingsConfigDict
1212
from starlette.exceptions import HTTPException
@@ -24,8 +24,9 @@
2424
)
2525
from mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions
2626
from mcp.server.fastmcp.server import FastMCP
27-
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
2827
from urllib.parse import urlencode
28+
from mcp.shared._httpx_utils import create_mcp_http_client
29+
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
2930

3031
logger = logging.getLogger(__name__)
3132

@@ -117,6 +118,7 @@ async def authorize(
117118
consent_url = f"{self.settings.server_url}consent?{urlencode(consent_params)}"
118119
return consent_url
119120

121+
120122
async def handle_github_callback(self, code: str, state: str) -> str:
121123
"""Handle GitHub OAuth callback."""
122124
state_data = self.state_mapping.get(state)
@@ -131,7 +133,7 @@ async def handle_github_callback(self, code: str, state: str) -> str:
131133
client_id = state_data["client_id"]
132134

133135
# Exchange code for token with GitHub
134-
async with httpx.AsyncClient() as client:
136+
async with create_mcp_http_client() as client:
135137
response = await client.post(
136138
self.settings.github_token_url,
137139
data={
@@ -482,7 +484,6 @@ def _format_scopes(self, scopes: str) -> str:
482484

483485

484486

485-
486487
def create_simple_mcp_server(settings: ServerSettings) -> FastMCP:
487488
"""Create a simple FastMCP server with GitHub OAuth."""
488489
oauth_provider = SimpleGitHubOAuthProvider(settings)
@@ -514,6 +515,7 @@ def create_simple_mcp_server(settings: ServerSettings) -> FastMCP:
514515
async def example_consent_handler(request: Request) -> Response:
515516
return await consent_handler.handle(request)
516517

518+
517519
@app.custom_route("/github/callback", methods=["GET"])
518520
async def github_callback_handler(request: Request) -> Response:
519521
"""Handle GitHub OAuth callback."""
@@ -560,7 +562,7 @@ async def get_user_profile() -> dict[str, Any]:
560562
"""
561563
github_token = get_github_token()
562564

563-
async with httpx.AsyncClient() as client:
565+
async with create_mcp_http_client() as client:
564566
response = await client.get(
565567
"https://api.github.com/user",
566568
headers={

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

+25-52
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,17 @@
11
import contextlib
22
import logging
3+
from collections.abc import AsyncIterator
34

45
import anyio
56
import click
67
import mcp.types as types
78
from mcp.server.lowlevel import Server
8-
from mcp.server.streamableHttp import (
9-
StreamableHTTPServerTransport,
10-
)
9+
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
1110
from starlette.applications import Starlette
1211
from starlette.routing import Mount
12+
from starlette.types import Receive, Scope, Send
1313

1414
logger = logging.getLogger(__name__)
15-
# Global task group that will be initialized in the lifespan
16-
task_group = None
17-
18-
19-
@contextlib.asynccontextmanager
20-
async def lifespan(app):
21-
"""Application lifespan context manager for managing task group."""
22-
global task_group
23-
24-
async with anyio.create_task_group() as tg:
25-
task_group = tg
26-
logger.info("Application started, task group initialized!")
27-
try:
28-
yield
29-
finally:
30-
logger.info("Application shutting down, cleaning up resources...")
31-
if task_group:
32-
tg.cancel_scope.cancel()
33-
task_group = None
34-
logger.info("Resources cleaned up successfully.")
3515

3616

3717
@click.command()
@@ -122,35 +102,28 @@ async def list_tools() -> list[types.Tool]:
122102
)
123103
]
124104

125-
# ASGI handler for stateless HTTP connections
126-
async def handle_streamable_http(scope, receive, send):
127-
logger.debug("Creating new transport")
128-
# Use lock to prevent race conditions when creating new sessions
129-
http_transport = StreamableHTTPServerTransport(
130-
mcp_session_id=None,
131-
is_json_response_enabled=json_response,
132-
)
133-
async with http_transport.connect() as streams:
134-
read_stream, write_stream = streams
135-
136-
if not task_group:
137-
raise RuntimeError("Task group is not initialized")
138-
139-
async def run_server():
140-
await app.run(
141-
read_stream,
142-
write_stream,
143-
app.create_initialization_options(),
144-
# Runs in standalone mode for stateless deployments
145-
# where clients perform initialization with any node
146-
standalone_mode=True,
147-
)
148-
149-
# Start server task
150-
task_group.start_soon(run_server)
151-
152-
# Handle the HTTP request and return the response
153-
await http_transport.handle_request(scope, receive, send)
105+
# Create the session manager with true stateless mode
106+
session_manager = StreamableHTTPSessionManager(
107+
app=app,
108+
event_store=None,
109+
json_response=json_response,
110+
stateless=True,
111+
)
112+
113+
async def handle_streamable_http(
114+
scope: Scope, receive: Receive, send: Send
115+
) -> None:
116+
await session_manager.handle_request(scope, receive, send)
117+
118+
@contextlib.asynccontextmanager
119+
async def lifespan(app: Starlette) -> AsyncIterator[None]:
120+
"""Context manager for session manager."""
121+
async with session_manager.run():
122+
logger.info("Application started with StreamableHTTP session manager!")
123+
try:
124+
yield
125+
finally:
126+
logger.info("Application shutting down...")
154127

155128
# Create an ASGI application using the transport
156129
starlette_app = Starlette(

0 commit comments

Comments
 (0)