Skip to content

Commit 07527fa

Browse files
committed
Merge branch 'main' into ihrpr/elicitation-prototype
2 parents 547d516 + 29c69e6 commit 07527fa

File tree

85 files changed

+9302
-1241
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+9302
-1241
lines changed

.github/workflows/publish-docs-manually.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
uses: astral-sh/setup-uv@v3
2020
with:
2121
enable-cache: true
22+
version: 0.7.2
2223

2324
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
2425
- uses: actions/cache@v4

.github/workflows/publish-pypi.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
uses: astral-sh/setup-uv@v3
1717
with:
1818
enable-cache: true
19+
version: 0.7.2
1920

2021
- name: Set up Python 3.12
2122
run: uv python install 3.12
@@ -67,6 +68,7 @@ jobs:
6768
uses: astral-sh/setup-uv@v3
6869
with:
6970
enable-cache: true
71+
version: 0.7.2
7072

7173
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
7274
- uses: actions/cache@v4

.github/workflows/shared.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ jobs:
1313
uses: astral-sh/setup-uv@v3
1414
with:
1515
enable-cache: true
16+
version: 0.7.2
1617

1718
- name: Install the project
1819
run: uv sync --frozen --all-extras --dev --python 3.12
@@ -29,6 +30,7 @@ jobs:
2930
uses: astral-sh/setup-uv@v3
3031
with:
3132
enable-cache: true
33+
version: 0.7.2
3234

3335
- name: Install the project
3436
run: uv sync --frozen --all-extras --dev --python 3.12
@@ -37,10 +39,11 @@ jobs:
3739
run: uv run --no-sync pyright
3840

3941
test:
40-
runs-on: ubuntu-latest
42+
runs-on: ${{ matrix.os }}
4143
strategy:
4244
matrix:
4345
python-version: ["3.10", "3.11", "3.12", "3.13"]
46+
os: [ubuntu-latest, windows-latest]
4447

4548
steps:
4649
- uses: actions/checkout@v4
@@ -49,9 +52,11 @@ jobs:
4952
uses: astral-sh/setup-uv@v3
5053
with:
5154
enable-cache: true
55+
version: 0.7.2
5256

5357
- name: Install the project
5458
run: uv sync --frozen --all-extras --dev --python ${{ matrix.python-version }}
5559

5660
- name: Run pytest
5761
run: uv run --no-sync pytest
62+
continue-on-error: true

README.md

Lines changed: 236 additions & 21 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
@@ -160,7 +160,7 @@ from dataclasses import dataclass
160160

161161
from fake_database import Database # Replace with your actual DB type
162162

163-
from mcp.server.fastmcp import Context, FastMCP
163+
from mcp.server.fastmcp import FastMCP
164164

165165
# Create a named server
166166
mcp = FastMCP("My App")
@@ -192,9 +192,10 @@ mcp = FastMCP("My App", lifespan=app_lifespan)
192192

193193
# Access type-safe lifespan context in tools
194194
@mcp.tool()
195-
def query_db(ctx: Context) -> str:
195+
def query_db() -> str:
196196
"""Tool that uses initialized resources"""
197-
db = ctx.request_context.lifespan_context.db
197+
ctx = mcp.get_context()
198+
db = ctx.request_context.lifespan_context["db"]
198199
return db.query()
199200
```
200201

@@ -314,27 +315,42 @@ async def long_task(files: list[str], ctx: Context) -> str:
314315
Authentication can be used by servers that want to expose tools accessing protected resources.
315316

316317
`mcp.server.auth` implements an OAuth 2.0 server interface, which servers can use by
317-
providing an implementation of the `OAuthServerProvider` protocol.
318+
providing an implementation of the `OAuthAuthorizationServerProvider` protocol.
318319

319-
```
320-
mcp = FastMCP("My App",
321-
auth_provider=MyOAuthServerProvider(),
322-
auth=AuthSettings(
323-
issuer_url="https://myapp.com",
324-
revocation_options=RevocationOptions(
325-
enabled=True,
326-
),
327-
client_registration_options=ClientRegistrationOptions(
328-
enabled=True,
329-
valid_scopes=["myscope", "myotherscope"],
330-
default_scopes=["myscope"],
331-
),
332-
required_scopes=["myscope"],
320+
```python
321+
from mcp import FastMCP
322+
from mcp.server.auth.provider import OAuthAuthorizationServerProvider
323+
from mcp.server.auth.settings import (
324+
AuthSettings,
325+
ClientRegistrationOptions,
326+
RevocationOptions,
327+
)
328+
329+
330+
class MyOAuthServerProvider(OAuthAuthorizationServerProvider):
331+
# See an example on how to implement at `examples/servers/simple-auth`
332+
...
333+
334+
335+
mcp = FastMCP(
336+
"My App",
337+
auth_server_provider=MyOAuthServerProvider(),
338+
auth=AuthSettings(
339+
issuer_url="https://myapp.com",
340+
revocation_options=RevocationOptions(
341+
enabled=True,
342+
),
343+
client_registration_options=ClientRegistrationOptions(
344+
enabled=True,
345+
valid_scopes=["myscope", "myotherscope"],
346+
default_scopes=["myscope"],
333347
),
348+
required_scopes=["myscope"],
349+
),
334350
)
335351
```
336352

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

339355
## Running Your Server
340356

@@ -387,8 +403,92 @@ python server.py
387403
mcp run server.py
388404
```
389405

406+
Note that `mcp run` or `mcp dev` only supports server using FastMCP and not the low-level server variant.
407+
408+
### Streamable HTTP Transport
409+
410+
> **Note**: Streamable HTTP transport is superseding SSE transport for production deployments.
411+
412+
```python
413+
from mcp.server.fastmcp import FastMCP
414+
415+
# Stateful server (maintains session state)
416+
mcp = FastMCP("StatefulServer")
417+
418+
# Stateless server (no session persistence)
419+
mcp = FastMCP("StatelessServer", stateless_http=True)
420+
421+
# Stateless server (no session persistence, no sse stream with supported client)
422+
mcp = FastMCP("StatelessServer", stateless_http=True, json_response=True)
423+
424+
# Run server with streamable_http transport
425+
mcp.run(transport="streamable-http")
426+
```
427+
428+
You can mount multiple FastMCP servers in a FastAPI application:
429+
430+
```python
431+
# echo.py
432+
from mcp.server.fastmcp import FastMCP
433+
434+
mcp = FastMCP(name="EchoServer", stateless_http=True)
435+
436+
437+
@mcp.tool(description="A simple echo tool")
438+
def echo(message: str) -> str:
439+
return f"Echo: {message}"
440+
```
441+
442+
```python
443+
# math.py
444+
from mcp.server.fastmcp import FastMCP
445+
446+
mcp = FastMCP(name="MathServer", stateless_http=True)
447+
448+
449+
@mcp.tool(description="A simple add tool")
450+
def add_two(n: int) -> int:
451+
return n + 2
452+
```
453+
454+
```python
455+
# main.py
456+
import contextlib
457+
from fastapi import FastAPI
458+
from mcp.echo import echo
459+
from mcp.math import math
460+
461+
462+
# Create a combined lifespan to manage both session managers
463+
@contextlib.asynccontextmanager
464+
async def lifespan(app: FastAPI):
465+
async with contextlib.AsyncExitStack() as stack:
466+
await stack.enter_async_context(echo.mcp.session_manager.run())
467+
await stack.enter_async_context(math.mcp.session_manager.run())
468+
yield
469+
470+
471+
app = FastAPI(lifespan=lifespan)
472+
app.mount("/echo", echo.mcp.streamable_http_app())
473+
app.mount("/math", math.mcp.streamable_http_app())
474+
```
475+
476+
For low level server with Streamable HTTP implementations, see:
477+
- Stateful server: [`examples/servers/simple-streamablehttp/`](examples/servers/simple-streamablehttp/)
478+
- Stateless server: [`examples/servers/simple-streamablehttp-stateless/`](examples/servers/simple-streamablehttp-stateless/)
479+
480+
The streamable HTTP transport supports:
481+
- Stateful and stateless operation modes
482+
- Resumability with event stores
483+
- JSON or SSE response formats
484+
- Better scalability for multi-node deployments
485+
390486
### Mounting to an Existing ASGI Server
391487

488+
> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).
489+
490+
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.
491+
392492
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.
393493

394494
```python
@@ -410,6 +510,43 @@ app = Starlette(
410510
app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app()))
411511
```
412512

513+
When mounting multiple MCP servers under different paths, you can configure the mount path in several ways:
514+
515+
```python
516+
from starlette.applications import Starlette
517+
from starlette.routing import Mount
518+
from mcp.server.fastmcp import FastMCP
519+
520+
# Create multiple MCP servers
521+
github_mcp = FastMCP("GitHub API")
522+
browser_mcp = FastMCP("Browser")
523+
curl_mcp = FastMCP("Curl")
524+
search_mcp = FastMCP("Search")
525+
526+
# Method 1: Configure mount paths via settings (recommended for persistent configuration)
527+
github_mcp.settings.mount_path = "/github"
528+
browser_mcp.settings.mount_path = "/browser"
529+
530+
# Method 2: Pass mount path directly to sse_app (preferred for ad-hoc mounting)
531+
# This approach doesn't modify the server's settings permanently
532+
533+
# Create Starlette app with multiple mounted servers
534+
app = Starlette(
535+
routes=[
536+
# Using settings-based configuration
537+
Mount("/github", app=github_mcp.sse_app()),
538+
Mount("/browser", app=browser_mcp.sse_app()),
539+
# Using direct mount path parameter
540+
Mount("/curl", app=curl_mcp.sse_app("/curl")),
541+
Mount("/search", app=search_mcp.sse_app("/search")),
542+
]
543+
)
544+
545+
# Method 3: For direct execution, you can also pass the mount path to run()
546+
if __name__ == "__main__":
547+
search_mcp.run(transport="sse", mount_path="/search")
548+
```
549+
413550
For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes).
414551

415552
## Examples
@@ -582,9 +719,11 @@ if __name__ == "__main__":
582719
asyncio.run(run())
583720
```
584721

722+
Caution: The `mcp run` and `mcp dev` tool doesn't support low-level server.
723+
585724
### Writing MCP Clients
586725

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

589728
```python
590729
from mcp import ClientSession, StdioServerParameters, types
@@ -648,6 +787,82 @@ if __name__ == "__main__":
648787
asyncio.run(run())
649788
```
650789

790+
Clients can also connect using [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http):
791+
792+
```python
793+
from mcp.client.streamable_http import streamablehttp_client
794+
from mcp import ClientSession
795+
796+
797+
async def main():
798+
# Connect to a streamable HTTP server
799+
async with streamablehttp_client("example/mcp") as (
800+
read_stream,
801+
write_stream,
802+
_,
803+
):
804+
# Create a session using the client streams
805+
async with ClientSession(read_stream, write_stream) as session:
806+
# Initialize the connection
807+
await session.initialize()
808+
# Call a tool
809+
tool_result = await session.call_tool("echo", {"message": "hello"})
810+
```
811+
812+
### OAuth Authentication for Clients
813+
814+
The SDK includes [authorization support](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for connecting to protected MCP servers:
815+
816+
```python
817+
from mcp.client.auth import OAuthClientProvider, TokenStorage
818+
from mcp.client.session import ClientSession
819+
from mcp.client.streamable_http import streamablehttp_client
820+
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
821+
822+
823+
class CustomTokenStorage(TokenStorage):
824+
"""Simple in-memory token storage implementation."""
825+
826+
async def get_tokens(self) -> OAuthToken | None:
827+
pass
828+
829+
async def set_tokens(self, tokens: OAuthToken) -> None:
830+
pass
831+
832+
async def get_client_info(self) -> OAuthClientInformationFull | None:
833+
pass
834+
835+
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
836+
pass
837+
838+
839+
async def main():
840+
# Set up OAuth authentication
841+
oauth_auth = OAuthClientProvider(
842+
server_url="https://api.example.com",
843+
client_metadata=OAuthClientMetadata(
844+
client_name="My Client",
845+
redirect_uris=["http://localhost:3000/callback"],
846+
grant_types=["authorization_code", "refresh_token"],
847+
response_types=["code"],
848+
),
849+
storage=CustomTokenStorage(),
850+
redirect_handler=lambda url: print(f"Visit: {url}"),
851+
callback_handler=lambda: ("auth_code", None),
852+
)
853+
854+
# Use with streamable HTTP client
855+
async with streamablehttp_client(
856+
"https://api.example.com/mcp", auth=oauth_auth
857+
) as (read, write, _):
858+
async with ClientSession(read, write) as session:
859+
await session.initialize()
860+
# Authenticated session ready
861+
```
862+
863+
For a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/).
864+
865+
651866
### MCP Primitives
652867

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

0 commit comments

Comments
 (0)