Skip to content

Commit d33c87a

Browse files
committed
Merge branch 'main' into ihrpr/inlcude-examples-in-pyright
2 parents 65956a8 + b8f7b02 commit d33c87a

File tree

37 files changed

+2516
-291
lines changed

37 files changed

+2516
-291
lines changed

.github/workflows/shared.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ jobs:
3737
run: uv run --no-sync pyright
3838

3939
test:
40-
runs-on: ubuntu-latest
40+
runs-on: ${{ matrix.os }}
4141
strategy:
4242
matrix:
4343
python-version: ["3.10", "3.11", "3.12", "3.13"]
44+
os: [ubuntu-latest, windows-latest]
4445

4546
steps:
4647
- uses: actions/checkout@v4
@@ -55,3 +56,4 @@ jobs:
5556

5657
- name: Run pytest
5758
run: uv run --no-sync pytest
59+
continue-on-error: true

README.md

Lines changed: 105 additions & 4 deletions
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
@@ -318,7 +318,7 @@ providing an implementation of the `OAuthServerProvider` protocol.
318318

319319
```
320320
mcp = FastMCP("My App",
321-
auth_provider=MyOAuthServerProvider(),
321+
auth_server_provider=MyOAuthServerProvider(),
322322
auth=AuthSettings(
323323
issuer_url="https://myapp.com",
324324
revocation_options=RevocationOptions(
@@ -387,8 +387,85 @@ python server.py
387387
mcp run server.py
388388
```
389389

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

465+
> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).
466+
467+
By default, SSE servers are mounted at `/sse` and Streamable HTTP servers are mounted at `/mcp`. You can customize these paths using the methods described below.
468+
392469
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.
393470

394471
```python
@@ -544,7 +621,7 @@ server = Server("example-server", lifespan=server_lifespan)
544621
# Access lifespan context in handlers
545622
@server.call_tool()
546623
async def query_db(name: str, arguments: dict) -> list:
547-
ctx = server.request_context
624+
ctx = server.get_context()
548625
db = ctx.lifespan_context["db"]
549626
return await db.query(arguments["query"])
550627
```
@@ -619,9 +696,11 @@ if __name__ == "__main__":
619696
asyncio.run(run())
620697
```
621698

699+
Caution: The `mcp run` and `mcp dev` tool doesn't support low-level server.
700+
622701
### Writing MCP Clients
623702

624-
The SDK provides a high-level client interface for connecting to MCP servers:
703+
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):
625704

626705
```python
627706
from mcp import ClientSession, StdioServerParameters, types
@@ -685,6 +764,28 @@ if __name__ == "__main__":
685764
asyncio.run(run())
686765
```
687766

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

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

examples/clients/simple-chatbot/README.MD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ This example demonstrates how to integrate the Model Context Protocol (MCP) into
2525
```plaintext
2626
LLM_API_KEY=your_api_key_here
2727
```
28+
**Note:** The current implementation is configured to use the Groq API endpoint (`https://api.groq.com/openai/v1/chat/completions`) with the `llama-3.2-90b-vision-preview` model. If you plan to use a different LLM provider, you'll need to modify the `LLMClient` class in `main.py` to use the appropriate endpoint URL and model parameters.
2829

2930
3. **Configure servers:**
3031

examples/servers/simple-auth/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,31 @@ uv run mcp-simple-auth
4444

4545
The server will start on `http://localhost:8000`.
4646

47+
### Transport Options
48+
49+
This server supports multiple transport protocols that can run on the same port:
50+
51+
#### SSE (Server-Sent Events) - Default
52+
```bash
53+
uv run mcp-simple-auth
54+
# or explicitly:
55+
uv run mcp-simple-auth --transport sse
56+
```
57+
58+
SSE transport provides endpoint:
59+
- `/sse`
60+
61+
#### Streamable HTTP
62+
```bash
63+
uv run mcp-simple-auth --transport streamable-http
64+
```
65+
66+
Streamable HTTP transport provides endpoint:
67+
- `/mcp`
68+
69+
70+
This ensures backward compatibility without needing multiple server instances. When using SSE transport (`--transport sse`), only the `/sse` endpoint is available.
71+
4772
## Available Tool
4873

4974
### get_user_profile
@@ -61,5 +86,6 @@ If the server fails to start, check:
6186
1. Environment variables `MCP_GITHUB_GITHUB_CLIENT_ID` and `MCP_GITHUB_GITHUB_CLIENT_SECRET` are set
6287
2. The GitHub OAuth app callback URL matches `http://localhost:8000/github/callback`
6388
3. No other service is using port 8000
89+
4. The transport specified is valid (`sse` or `streamable-http`)
6490

6591
You can use [Inspector](https://github.com/modelcontextprotocol/inspector) to test Auth

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
import logging
44
import secrets
55
import time
6-
from typing import Any
6+
from typing import Any, Literal
77

88
import click
9-
import httpx
109
from pydantic import AnyHttpUrl
1110
from pydantic_settings import BaseSettings, SettingsConfigDict
1211
from starlette.exceptions import HTTPException
@@ -24,6 +23,7 @@
2423
)
2524
from mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions
2625
from mcp.server.fastmcp.server import FastMCP
26+
from mcp.shared._httpx_utils import create_mcp_http_client
2727
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
2828

2929
logger = logging.getLogger(__name__)
@@ -123,7 +123,7 @@ async def handle_github_callback(self, code: str, state: str) -> str:
123123
client_id = state_data["client_id"]
124124

125125
# Exchange code for token with GitHub
126-
async with httpx.AsyncClient() as client:
126+
async with create_mcp_http_client() as client:
127127
response = await client.post(
128128
self.settings.github_token_url,
129129
data={
@@ -325,7 +325,7 @@ async def get_user_profile() -> dict[str, Any]:
325325
"""
326326
github_token = get_github_token()
327327

328-
async with httpx.AsyncClient() as client:
328+
async with create_mcp_http_client() as client:
329329
response = await client.get(
330330
"https://api.github.com/user",
331331
headers={
@@ -347,7 +347,13 @@ async def get_user_profile() -> dict[str, Any]:
347347
@click.command()
348348
@click.option("--port", default=8000, help="Port to listen on")
349349
@click.option("--host", default="localhost", help="Host to bind to")
350-
def main(port: int, host: str) -> int:
350+
@click.option(
351+
"--transport",
352+
default="sse",
353+
type=click.Choice(["sse", "streamable-http"]),
354+
help="Transport protocol to use ('sse' or 'streamable-http')",
355+
)
356+
def main(port: int, host: str, transport: Literal["sse", "streamable-http"]) -> int:
351357
"""Run the simple GitHub MCP server."""
352358
logging.basicConfig(level=logging.INFO)
353359

@@ -364,5 +370,6 @@ def main(port: int, host: str) -> int:
364370
return 1
365371

366372
mcp_server = create_simple_mcp_server(settings)
367-
mcp_server.run(transport="sse")
373+
logger.info(f"Starting server with {transport} transport")
374+
mcp_server.run(transport=transport)
368375
return 0

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

Lines changed: 25 additions & 52 deletions
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.streamable_http 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-
stateless=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)