Skip to content

Commit 50f0d53

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/resources-with-context
2 parents 5920bd2 + 3b1b213 commit 50f0d53

File tree

92 files changed

+9671
-295
lines changed

Some content is hidden

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

92 files changed

+9671
-295
lines changed

.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

.pre-commit-config.yaml

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,29 @@ repos:
77
- id: prettier
88
types_or: [yaml, json5]
99

10-
- repo: https://github.com/astral-sh/ruff-pre-commit
11-
rev: v0.8.1
10+
- repo: local
1211
hooks:
1312
- id: ruff-format
13+
name: Ruff Format
14+
entry: uv run ruff
15+
args: [format]
16+
language: system
17+
types: [python]
18+
pass_filenames: false
1419
- id: ruff
15-
args: [--fix, --exit-non-zero-on-fix]
16-
17-
- repo: local
18-
hooks:
20+
name: Ruff
21+
entry: uv run ruff
22+
args: ["check", "--fix", "--exit-non-zero-on-fix"]
23+
types: [python]
24+
language: system
25+
pass_filenames: false
26+
- id: pyright
27+
name: pyright
28+
entry: uv run pyright
29+
args: [src]
30+
language: system
31+
types: [python]
32+
pass_filenames: false
1933
- id: uv-lock-check
2034
name: Check uv.lock is up to date
2135
entry: uv lock --check

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: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ mcp = FastMCP("My App", lifespan=app_lifespan)
194194
@mcp.tool()
195195
def query_db(ctx: Context) -> str:
196196
"""Tool that uses initialized resources"""
197-
db = ctx.request_context.lifespan_context["db"]
197+
db = ctx.request_context.lifespan_context.db
198198
return db.query()
199199
```
200200

@@ -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_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](mcp/server/auth/provider.py) for more details.
338+
312339
## Running Your Server
313340

314341
### Development Mode
@@ -385,6 +412,30 @@ app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app()))
385412

386413
For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes).
387414

415+
#### Message Dispatch Options
416+
417+
By default, the SSE server uses an in-memory message dispatch system for incoming POST messages. For production deployments or distributed scenarios, you can use Redis or implement your own message dispatch system that conforms to the `MessageDispatch` protocol:
418+
419+
```python
420+
# Using the built-in Redis message dispatch
421+
from mcp.server.fastmcp import FastMCP
422+
from mcp.server.message_queue import RedisMessageDispatch
423+
424+
# Create a Redis message dispatch
425+
redis_dispatch = RedisMessageDispatch(
426+
redis_url="redis://localhost:6379/0", prefix="mcp:pubsub:"
427+
)
428+
429+
# Pass the message dispatch instance to the server
430+
mcp = FastMCP("My App", message_queue=redis_dispatch)
431+
```
432+
433+
To use Redis, add the Redis dependency:
434+
435+
```bash
436+
uv add "mcp[redis]"
437+
```
438+
388439
## Examples
389440

390441
### Echo Server

examples/clients/simple-chatbot/mcp_simple_chatbot/main.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,10 @@ async def list_tools(self) -> list[Any]:
122122

123123
for item in tools_response:
124124
if isinstance(item, tuple) and item[0] == "tools":
125-
for tool in item[1]:
126-
tools.append(Tool(tool.name, tool.description, tool.inputSchema))
125+
tools.extend(
126+
Tool(tool.name, tool.description, tool.inputSchema)
127+
for tool in item[1]
128+
)
127129

128130
return tools
129131

@@ -282,10 +284,9 @@ def __init__(self, servers: list[Server], llm_client: LLMClient) -> None:
282284

283285
async def cleanup_servers(self) -> None:
284286
"""Clean up all servers properly."""
285-
cleanup_tasks = []
286-
for server in self.servers:
287-
cleanup_tasks.append(asyncio.create_task(server.cleanup()))
288-
287+
cleanup_tasks = [
288+
asyncio.create_task(server.cleanup()) for server in self.servers
289+
]
289290
if cleanup_tasks:
290291
try:
291292
await asyncio.gather(*cleanup_tasks, return_exceptions=True)
@@ -322,8 +323,7 @@ async def process_llm_response(self, llm_response: str) -> str:
322323
total = result["total"]
323324
percentage = (progress / total) * 100
324325
logging.info(
325-
f"Progress: {progress}/{total} "
326-
f"({percentage:.1f}%)"
326+
f"Progress: {progress}/{total} ({percentage:.1f}%)"
327327
)
328328

329329
return f"Tool execution result: {result}"

examples/servers/simple-prompt/mcp_simple_prompt/server.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,15 @@ async def get_prompt(
8888
)
8989

9090
if transport == "sse":
91+
from mcp.server.message_queue.redis import RedisMessageDispatch
9192
from mcp.server.sse import SseServerTransport
9293
from starlette.applications import Starlette
94+
from starlette.responses import Response
9395
from starlette.routing import Mount, Route
9496

95-
sse = SseServerTransport("/messages/")
97+
message_dispatch = RedisMessageDispatch("redis://localhost:6379/0")
98+
99+
sse = SseServerTransport("/messages/", message_dispatch=message_dispatch)
96100

97101
async def handle_sse(request):
98102
async with sse.connect_sse(
@@ -101,6 +105,7 @@ async def handle_sse(request):
101105
await app.run(
102106
streams[0], streams[1], app.create_initialization_options()
103107
)
108+
return Response()
104109

105110
starlette_app = Starlette(
106111
debug=True,

examples/servers/simple-resource/mcp_simple_resource/server.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ async def read_resource(uri: FileUrl) -> str | bytes:
4646
if transport == "sse":
4747
from mcp.server.sse import SseServerTransport
4848
from starlette.applications import Starlette
49+
from starlette.responses import Response
4950
from starlette.routing import Mount, Route
5051

5152
sse = SseServerTransport("/messages/")
@@ -57,11 +58,12 @@ async def handle_sse(request):
5758
await app.run(
5859
streams[0], streams[1], app.create_initialization_options()
5960
)
61+
return Response()
6062

6163
starlette_app = Starlette(
6264
debug=True,
6365
routes=[
64-
Route("/sse", endpoint=handle_sse),
66+
Route("/sse", endpoint=handle_sse, methods=["GET"]),
6567
Mount("/messages/", app=sse.handle_post_message),
6668
],
6769
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# MCP Simple StreamableHttp Stateless Server Example
2+
3+
A stateless MCP server example demonstrating the StreamableHttp transport without maintaining session state. This example is ideal for understanding how to deploy MCP servers in multi-node environments where requests can be routed to any instance.
4+
5+
## Features
6+
7+
- Uses the StreamableHTTP transport in stateless mode (mcp_session_id=None)
8+
- Each request creates a new ephemeral connection
9+
- No session state maintained between requests
10+
- Task lifecycle scoped to individual requests
11+
- Suitable for deployment in multi-node environments
12+
13+
14+
## Usage
15+
16+
Start the server:
17+
18+
```bash
19+
# Using default port 3000
20+
uv run mcp-simple-streamablehttp-stateless
21+
22+
# Using custom port
23+
uv run mcp-simple-streamablehttp-stateless --port 3000
24+
25+
# Custom logging level
26+
uv run mcp-simple-streamablehttp-stateless --log-level DEBUG
27+
28+
# Enable JSON responses instead of SSE streams
29+
uv run mcp-simple-streamablehttp-stateless --json-response
30+
```
31+
32+
The server exposes a tool named "start-notification-stream" that accepts three arguments:
33+
34+
- `interval`: Time between notifications in seconds (e.g., 1.0)
35+
- `count`: Number of notifications to send (e.g., 5)
36+
- `caller`: Identifier string for the caller
37+
38+
39+
## Client
40+
41+
You can connect to this server using an HTTP client. For now, only the TypeScript SDK has streamable HTTP client examples, or you can use [Inspector](https://github.com/modelcontextprotocol/inspector) for testing.

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

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .server import main
2+
3+
if __name__ == "__main__":
4+
main()

0 commit comments

Comments
 (0)