Skip to content

Commit b9fc7b4

Browse files
authored
Merge branch 'main' into fix-hang-github-mcp
2 parents d460614 + abfb482 commit b9fc7b4

File tree

76 files changed

+1309
-1170
lines changed

Some content is hidden

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

76 files changed

+1309
-1170
lines changed

.github/workflows/shared.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ jobs:
7070
- name: Run pytest with coverage
7171
shell: bash
7272
run: |
73+
uv run --frozen --no-sync coverage erase
7374
uv run --frozen --no-sync coverage run -m pytest -n auto
7475
uv run --frozen --no-sync coverage combine
7576
uv run --frozen --no-sync coverage report

.github/workflows/weekly-lockfile-update.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jobs:
3232
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7
3333
with:
3434
commit-message: "chore: update uv.lock with latest dependencies"
35+
sign-commits: true
3536
title: "chore: weekly dependency update"
3637
body-path: pr_body.md
3738
branch: weekly-lockfile-update

CLAUDE.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,19 @@ This document contains critical information about working with this codebase. Fo
2828
- Bug fixes require regression tests
2929
- IMPORTANT: The `tests/client/test_client.py` is the most well designed test file. Follow its patterns.
3030
- IMPORTANT: Be minimal, and focus on E2E tests: Use the `mcp.client.Client` whenever possible.
31-
- IMPORTANT: Before pushing, verify 100% branch coverage on changed files by running
32-
`uv run --frozen pytest -x` (coverage is configured in `pyproject.toml` with `fail_under = 100`
33-
and `branch = true`). If any branch is uncovered, add a test for it before pushing.
31+
- Coverage: CI requires 100% (`fail_under = 100`, `branch = true`).
32+
- Full check: `./scripts/test` (~20s, matches CI exactly)
33+
- Targeted check while iterating:
34+
35+
```bash
36+
uv run --frozen coverage erase
37+
uv run --frozen coverage run -m pytest tests/path/test_foo.py
38+
uv run --frozen coverage combine
39+
uv run --frozen coverage report --include='src/mcp/path/foo.py' --fail-under=0
40+
```
41+
42+
Partial runs can't hit 100% (coverage tracks `tests/` too), so `--fail-under=0`
43+
and `--include` scope the report to what you actually changed.
3444
- Avoid `anyio.sleep()` with a fixed duration to wait for async operations. Instead:
3545
- Use `anyio.Event` — set it in the callback/handler, `await event.wait()` in the test
3646
- For stream messages, use `await stream.receive()` instead of `sleep()` + `receive_nowait()`

README.v2.md

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -346,13 +346,12 @@ Tools can optionally receive a Context object by including a parameter with the
346346
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
347347
```python
348348
from mcp.server.mcpserver import Context, MCPServer
349-
from mcp.server.session import ServerSession
350349

351350
mcp = MCPServer(name="Progress Example")
352351

353352

354353
@mcp.tool()
355-
async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str:
354+
async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
356355
"""Execute a task with progress updates."""
357356
await ctx.info(f"Starting: {task_name}")
358357

@@ -694,13 +693,12 @@ The Context object provides the following capabilities:
694693
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
695694
```python
696695
from mcp.server.mcpserver import Context, MCPServer
697-
from mcp.server.session import ServerSession
698696

699697
mcp = MCPServer(name="Progress Example")
700698

701699

702700
@mcp.tool()
703-
async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str:
701+
async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
704702
"""Execute a task with progress updates."""
705703
await ctx.info(f"Starting: {task_name}")
706704

@@ -826,7 +824,6 @@ import uuid
826824
from pydantic import BaseModel, Field
827825

828826
from mcp.server.mcpserver import Context, MCPServer
829-
from mcp.server.session import ServerSession
830827
from mcp.shared.exceptions import UrlElicitationRequiredError
831828
from mcp.types import ElicitRequestURLParams
832829

@@ -844,7 +841,7 @@ class BookingPreferences(BaseModel):
844841

845842

846843
@mcp.tool()
847-
async def book_table(date: str, time: str, party_size: int, ctx: Context[ServerSession, None]) -> str:
844+
async def book_table(date: str, time: str, party_size: int, ctx: Context) -> str:
848845
"""Book a table with date availability check.
849846
850847
This demonstrates form mode elicitation for collecting non-sensitive user input.
@@ -868,7 +865,7 @@ async def book_table(date: str, time: str, party_size: int, ctx: Context[ServerS
868865

869866

870867
@mcp.tool()
871-
async def secure_payment(amount: float, ctx: Context[ServerSession, None]) -> str:
868+
async def secure_payment(amount: float, ctx: Context) -> str:
872869
"""Process a secure payment requiring URL confirmation.
873870
874871
This demonstrates URL mode elicitation using ctx.elicit_url() for
@@ -892,7 +889,7 @@ async def secure_payment(amount: float, ctx: Context[ServerSession, None]) -> st
892889

893890

894891
@mcp.tool()
895-
async def connect_service(service_name: str, ctx: Context[ServerSession, None]) -> str:
892+
async def connect_service(service_name: str, ctx: Context) -> str:
896893
"""Connect to a third-party service requiring OAuth authorization.
897894
898895
This demonstrates the "throw error" pattern using UrlElicitationRequiredError.
@@ -933,14 +930,13 @@ Tools can interact with LLMs through sampling (generating text):
933930
<!-- snippet-source examples/snippets/servers/sampling.py -->
934931
```python
935932
from mcp.server.mcpserver import Context, MCPServer
936-
from mcp.server.session import ServerSession
937933
from mcp.types import SamplingMessage, TextContent
938934

939935
mcp = MCPServer(name="Sampling Example")
940936

941937

942938
@mcp.tool()
943-
async def generate_poem(topic: str, ctx: Context[ServerSession, None]) -> str:
939+
async def generate_poem(topic: str, ctx: Context) -> str:
944940
"""Generate a poem using LLM sampling."""
945941
prompt = f"Write a short poem about {topic}"
946942

@@ -970,13 +966,12 @@ Tools can send logs and notifications through the context:
970966
<!-- snippet-source examples/snippets/servers/notifications.py -->
971967
```python
972968
from mcp.server.mcpserver import Context, MCPServer
973-
from mcp.server.session import ServerSession
974969

975970
mcp = MCPServer(name="Notifications Example")
976971

977972

978973
@mcp.tool()
979-
async def process_data(data: str, ctx: Context[ServerSession, None]) -> str:
974+
async def process_data(data: str, ctx: Context) -> str:
980975
"""Process data with logging."""
981976
# Different log levels
982977
await ctx.debug(f"Debug: Processing '{data}'")

docs/experimental/tasks-server.md

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -408,16 +408,10 @@ For custom error messages, call `task.fail()` before raising.
408408
For web applications, use the Streamable HTTP transport:
409409

410410
```python
411-
from collections.abc import AsyncIterator
412-
from contextlib import asynccontextmanager
413-
414411
import uvicorn
415-
from starlette.applications import Starlette
416-
from starlette.routing import Mount
417412

418413
from mcp.server import Server
419414
from mcp.server.experimental.task_context import ServerTaskContext
420-
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
421415
from mcp.types import (
422416
CallToolResult, CreateTaskResult, TextContent, Tool, ToolExecution, TASK_REQUIRED,
423417
)
@@ -462,22 +456,8 @@ async def handle_tool(name: str, arguments: dict) -> CallToolResult | CreateTask
462456
return CallToolResult(content=[TextContent(type="text", text=f"Unknown: {name}")], isError=True)
463457

464458

465-
def create_app():
466-
session_manager = StreamableHTTPSessionManager(app=server)
467-
468-
@asynccontextmanager
469-
async def lifespan(app: Starlette) -> AsyncIterator[None]:
470-
async with session_manager.run():
471-
yield
472-
473-
return Starlette(
474-
routes=[Mount("/mcp", app=session_manager.handle_request)],
475-
lifespan=lifespan,
476-
)
477-
478-
479459
if __name__ == "__main__":
480-
uvicorn.run(create_app(), host="127.0.0.1", port=8000)
460+
uvicorn.run(server.streamable_http_app(), host="127.0.0.1", port=8000)
481461
```
482462

483463
## Testing Task Servers

docs/migration.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,37 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app(json_response=Tru
288288

289289
**Note:** DNS rebinding protection is automatically enabled when `host` is `127.0.0.1`, `localhost`, or `::1`. This now happens in `sse_app()` and `streamable_http_app()` instead of the constructor.
290290

291+
### `MCPServer.get_context()` removed
292+
293+
`MCPServer.get_context()` has been removed. Context is now injected by the framework and passed explicitly — there is no ambient ContextVar to read from.
294+
295+
**If you were calling `get_context()` from inside a tool/resource/prompt:** use the `ctx: Context` parameter injection instead.
296+
297+
**Before (v1):**
298+
299+
```python
300+
@mcp.tool()
301+
async def my_tool(x: int) -> str:
302+
ctx = mcp.get_context()
303+
await ctx.info("Processing...")
304+
return str(x)
305+
```
306+
307+
**After (v2):**
308+
309+
```python
310+
@mcp.tool()
311+
async def my_tool(x: int, ctx: Context) -> str:
312+
await ctx.info("Processing...")
313+
return str(x)
314+
```
315+
316+
### `MCPServer.call_tool()`, `read_resource()`, `get_prompt()` now accept a `context` parameter
317+
318+
`MCPServer.call_tool()`, `MCPServer.read_resource()`, and `MCPServer.get_prompt()` now accept an optional `context: Context | None = None` parameter. The framework passes this automatically during normal request handling. If you call these methods directly and omit `context`, a Context with no active request is constructed for you — tools that don't use `ctx` work normally, but any attempt to use `ctx.session`, `ctx.request_id`, etc. will raise.
319+
320+
The internal layers (`ToolManager.call_tool`, `Tool.run`, `Prompt.render`, `ResourceTemplate.create_resource`, etc.) now require `context` as a positional argument.
321+
291322
### Replace `RootModel` by union types with `TypeAdapter` validation
292323

293324
The following union types are no longer `RootModel` subclasses:
@@ -694,7 +725,7 @@ If you prefer the convenience of automatic wrapping, use `MCPServer` which still
694725

695726
### Lowlevel `Server`: `request_context` property removed
696727

697-
The `server.request_context` property has been removed. Request context is now passed directly to handlers as the first argument (`ctx`). The `request_ctx` module-level contextvar is now an internal implementation detail and should not be relied upon.
728+
The `server.request_context` property has been removed. Request context is now passed directly to handlers as the first argument (`ctx`). The `request_ctx` module-level contextvar has been removed entirely.
698729

699730
**Before (v1):**
700731

examples/servers/everything-server/mcp_everything_server/server.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from mcp.server import ServerRequestContext
1414
from mcp.server.mcpserver import Context, MCPServer
1515
from mcp.server.mcpserver.prompts.base import UserMessage
16-
from mcp.server.session import ServerSession
1716
from mcp.server.streamable_http import EventCallback, EventMessage, EventStore
1817
from mcp.types import (
1918
AudioContent,
@@ -142,7 +141,7 @@ def test_multiple_content_types() -> list[TextContent | ImageContent | EmbeddedR
142141

143142

144143
@mcp.tool()
145-
async def test_tool_with_logging(ctx: Context[ServerSession, None]) -> str:
144+
async def test_tool_with_logging(ctx: Context) -> str:
146145
"""Tests tool that emits log messages during execution"""
147146
await ctx.info("Tool execution started")
148147
await asyncio.sleep(0.05)
@@ -155,7 +154,7 @@ async def test_tool_with_logging(ctx: Context[ServerSession, None]) -> str:
155154

156155

157156
@mcp.tool()
158-
async def test_tool_with_progress(ctx: Context[ServerSession, None]) -> str:
157+
async def test_tool_with_progress(ctx: Context) -> str:
159158
"""Tests tool that reports progress notifications"""
160159
await ctx.report_progress(progress=0, total=100, message="Completed step 0 of 100")
161160
await asyncio.sleep(0.05)
@@ -173,7 +172,7 @@ async def test_tool_with_progress(ctx: Context[ServerSession, None]) -> str:
173172

174173

175174
@mcp.tool()
176-
async def test_sampling(prompt: str, ctx: Context[ServerSession, None]) -> str:
175+
async def test_sampling(prompt: str, ctx: Context) -> str:
177176
"""Tests server-initiated sampling (LLM completion request)"""
178177
try:
179178
# Request sampling from client
@@ -198,7 +197,7 @@ class UserResponse(BaseModel):
198197

199198

200199
@mcp.tool()
201-
async def test_elicitation(message: str, ctx: Context[ServerSession, None]) -> str:
200+
async def test_elicitation(message: str, ctx: Context) -> str:
202201
"""Tests server-initiated elicitation (user input request)"""
203202
try:
204203
# Request user input from client
@@ -230,7 +229,7 @@ class SEP1034DefaultsSchema(BaseModel):
230229

231230

232231
@mcp.tool()
233-
async def test_elicitation_sep1034_defaults(ctx: Context[ServerSession, None]) -> str:
232+
async def test_elicitation_sep1034_defaults(ctx: Context) -> str:
234233
"""Tests elicitation with default values for all primitive types (SEP-1034)"""
235234
try:
236235
# Request user input with defaults for all primitive types
@@ -289,7 +288,7 @@ class EnumSchemasTestSchema(BaseModel):
289288

290289

291290
@mcp.tool()
292-
async def test_elicitation_sep1330_enums(ctx: Context[ServerSession, None]) -> str:
291+
async def test_elicitation_sep1330_enums(ctx: Context) -> str:
293292
"""Tests elicitation with enum schema variations per SEP-1330"""
294293
try:
295294
result = await ctx.elicit(
@@ -313,7 +312,7 @@ def test_error_handling() -> str:
313312

314313

315314
@mcp.tool()
316-
async def test_reconnection(ctx: Context[ServerSession, None]) -> str:
315+
async def test_reconnection(ctx: Context) -> str:
317316
"""Tests SSE polling by closing stream mid-call (SEP-1699)"""
318317
await ctx.info("Before disconnect")
319318

examples/servers/simple-pagination/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ A simple MCP server demonstrating pagination for tools, resources, and prompts u
44

55
## Usage
66

7-
Start the server using either stdio (default) or SSE transport:
7+
Start the server using either stdio (default) or Streamable HTTP transport:
88

99
```bash
1010
# Using stdio transport (default)
1111
uv run mcp-simple-pagination
1212

13-
# Using SSE transport on custom port
14-
uv run mcp-simple-pagination --transport sse --port 8000
13+
# Using Streamable HTTP transport on custom port
14+
uv run mcp-simple-pagination --transport streamable-http --port 8000
1515
```
1616

1717
The server exposes:

examples/servers/simple-pagination/mcp_simple_pagination/server.py

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import click
1111
from mcp import types
1212
from mcp.server import Server, ServerRequestContext
13-
from starlette.requests import Request
1413

1514
T = TypeVar("T")
1615

@@ -143,10 +142,10 @@ async def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRe
143142

144143

145144
@click.command()
146-
@click.option("--port", default=8000, help="Port to listen on for SSE")
145+
@click.option("--port", default=8000, help="Port to listen on for HTTP")
147146
@click.option(
148147
"--transport",
149-
type=click.Choice(["stdio", "sse"]),
148+
type=click.Choice(["stdio", "streamable-http"]),
150149
default="stdio",
151150
help="Transport type",
152151
)
@@ -161,30 +160,10 @@ def main(port: int, transport: str) -> int:
161160
on_get_prompt=handle_get_prompt,
162161
)
163162

164-
if transport == "sse":
165-
from mcp.server.sse import SseServerTransport
166-
from starlette.applications import Starlette
167-
from starlette.responses import Response
168-
from starlette.routing import Mount, Route
169-
170-
sse = SseServerTransport("/messages/")
171-
172-
async def handle_sse(request: Request):
173-
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
174-
await app.run(streams[0], streams[1], app.create_initialization_options())
175-
return Response()
176-
177-
starlette_app = Starlette(
178-
debug=True,
179-
routes=[
180-
Route("/sse", endpoint=handle_sse, methods=["GET"]),
181-
Mount("/messages/", app=sse.handle_post_message),
182-
],
183-
)
184-
163+
if transport == "streamable-http":
185164
import uvicorn
186165

187-
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
166+
uvicorn.run(app.streamable_http_app(), host="127.0.0.1", port=port)
188167
else:
189168
from mcp.server.stdio import stdio_server
190169

examples/servers/simple-prompt/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ A simple MCP server that exposes a customizable prompt template with optional co
44

55
## Usage
66

7-
Start the server using either stdio (default) or SSE transport:
7+
Start the server using either stdio (default) or Streamable HTTP transport:
88

99
```bash
1010
# Using stdio transport (default)
1111
uv run mcp-simple-prompt
1212

13-
# Using SSE transport on custom port
14-
uv run mcp-simple-prompt --transport sse --port 8000
13+
# Using Streamable HTTP transport on custom port
14+
uv run mcp-simple-prompt --transport streamable-http --port 8000
1515
```
1616

1717
The server exposes a prompt named "simple" that accepts two optional arguments:

0 commit comments

Comments
 (0)