Skip to content

Commit c26f835

Browse files
Merge branch 'main' into stephanlensky/fix-async-callable-objects
2 parents 92d9426 + d187643 commit c26f835

File tree

117 files changed

+15268
-372
lines changed

Some content is hidden

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

117 files changed

+15268
-372
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

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,5 @@ cython_debug/
166166

167167
# vscode
168168
.vscode/
169+
.windsurfrules
169170
**/CLAUDE.local.md

CLAUDE.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ This document contains critical information about working with this codebase. Fo
1919
- Line length: 88 chars maximum
2020

2121
3. Testing Requirements
22-
- Framework: `uv run pytest`
22+
- Framework: `uv run --frozen pytest`
2323
- Async testing: use anyio, not asyncio
2424
- Coverage: test edge cases and errors
2525
- New features require tests
@@ -54,9 +54,9 @@ This document contains critical information about working with this codebase. Fo
5454
## Code Formatting
5555

5656
1. Ruff
57-
- Format: `uv run ruff format .`
58-
- Check: `uv run ruff check .`
59-
- Fix: `uv run ruff check . --fix`
57+
- Format: `uv run --frozen ruff format .`
58+
- Check: `uv run --frozen ruff check .`
59+
- Fix: `uv run --frozen ruff check . --fix`
6060
- Critical issues:
6161
- Line length (88 chars)
6262
- Import sorting (I001)
@@ -67,7 +67,7 @@ This document contains critical information about working with this codebase. Fo
6767
- Imports: split into multiple lines
6868

6969
2. Type Checking
70-
- Tool: `uv run pyright`
70+
- Tool: `uv run --frozen pyright`
7171
- Requirements:
7272
- Explicit None checks for Optional
7373
- Type narrowing for strings
@@ -104,6 +104,10 @@ This document contains critical information about working with this codebase. Fo
104104
- Add None checks
105105
- Narrow string types
106106
- Match existing patterns
107+
- Pytest:
108+
- If the tests aren't finding the anyio pytest mark, try adding PYTEST_DISABLE_PLUGIN_AUTOLOAD=""
109+
to the start of the pytest run command eg:
110+
`PYTEST_DISABLE_PLUGIN_AUTOLOAD="" uv run --frozen pytest`
107111

108112
3. Best Practices
109113
- Check git status before commits

README.md

Lines changed: 232 additions & 3 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
@@ -309,6 +309,33 @@ async def long_task(files: list[str], ctx: Context) -> str:
309309
return "Processing complete"
310310
```
311311

312+
### Authentication
313+
314+
Authentication can be used by servers that want to expose tools accessing protected resources.
315+
316+
`mcp.server.auth` implements an OAuth 2.0 server interface, which servers can use by
317+
providing an implementation of the `OAuthServerProvider` protocol.
318+
319+
```
320+
mcp = FastMCP("My App",
321+
auth_server_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"],
333+
),
334+
)
335+
```
336+
337+
See [OAuthServerProvider](src/mcp/server/auth/provider.py) for more details.
338+
312339
## Running Your Server
313340

314341
### Development Mode
@@ -360,8 +387,95 @@ python server.py
360387
mcp run server.py
361388
```
362389

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

475+
> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).
476+
477+
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.
478+
365479
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.
366480

367481
```python
@@ -383,6 +497,43 @@ app = Starlette(
383497
app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app()))
384498
```
385499

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

388539
## Examples
@@ -480,7 +631,7 @@ server = Server("example-server", lifespan=server_lifespan)
480631
# Access lifespan context in handlers
481632
@server.call_tool()
482633
async def query_db(name: str, arguments: dict) -> list:
483-
ctx = server.request_context
634+
ctx = server.get_context()
484635
db = ctx.lifespan_context["db"]
485636
return await db.query(arguments["query"])
486637
```
@@ -555,9 +706,11 @@ if __name__ == "__main__":
555706
asyncio.run(run())
556707
```
557708

709+
Caution: The `mcp run` and `mcp dev` tool doesn't support low-level server.
710+
558711
### Writing MCP Clients
559712

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

562715
```python
563716
from mcp import ClientSession, StdioServerParameters, types
@@ -621,6 +774,82 @@ if __name__ == "__main__":
621774
asyncio.run(run())
622775
```
623776

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

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

0 commit comments

Comments
 (0)