Skip to content

Commit 92acf11

Browse files
Merge branch 'main' into fix-1664-forward-user-agent-auth-flow
2 parents 7411f44 + 92c693b commit 92acf11

File tree

96 files changed

+2319
-2145
lines changed

Some content is hidden

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

96 files changed

+2319
-2145
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: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,32 @@ 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` (~23s). Runs coverage + `strict-no-cover` on the
33+
default Python. Not identical to CI: CI also runs 3.10–3.14 × {ubuntu, windows},
34+
and some branch-coverage quirks only surface on specific matrix entries.
35+
- Targeted check while iterating (~4s, deterministic):
36+
37+
```bash
38+
uv run --frozen coverage erase
39+
uv run --frozen coverage run -m pytest tests/path/test_foo.py
40+
uv run --frozen coverage combine
41+
uv run --frozen coverage report --include='src/mcp/path/foo.py' --fail-under=0
42+
UV_FROZEN=1 uv run --frozen strict-no-cover
43+
```
44+
45+
Partial runs can't hit 100% (coverage tracks `tests/` too), so `--fail-under=0`
46+
and `--include` scope the report. `strict-no-cover` has no false positives on
47+
partial runs — if your new test executes a line marked `# pragma: no cover`,
48+
even a single-file run catches it.
49+
- Coverage pragmas:
50+
- `# pragma: no cover` — line is never executed. CI's `strict-no-cover` fails if
51+
it IS executed. When your test starts covering such a line, remove the pragma.
52+
- `# pragma: lax no cover` — excluded from coverage but not checked by
53+
`strict-no-cover`. Use for lines covered on some platforms/versions but not
54+
others.
55+
- `# pragma: no branch` — excludes branch arcs only. coverage.py misreports the
56+
`->exit` arc for nested `async with` on Python 3.11+ (worse on 3.14/Windows).
3457
- Avoid `anyio.sleep()` with a fixed duration to wait for async operations. Instead:
3558
- Use `anyio.Event`set it in the callback/handler, `await event.wait()` in the test
3659
- 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/api.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

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/hooks/gen_ref_pages.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Generate the code reference pages and navigation."""
2+
3+
from pathlib import Path
4+
5+
import mkdocs_gen_files
6+
7+
nav = mkdocs_gen_files.Nav()
8+
9+
root = Path(__file__).parent.parent.parent
10+
src = root / "src"
11+
12+
for path in sorted(src.rglob("*.py")):
13+
module_path = path.relative_to(src).with_suffix("")
14+
doc_path = path.relative_to(src).with_suffix(".md")
15+
full_doc_path = Path("api", doc_path)
16+
17+
parts = tuple(module_path.parts)
18+
19+
if parts[-1] == "__init__":
20+
parts = parts[:-1]
21+
doc_path = doc_path.with_name("index.md")
22+
full_doc_path = full_doc_path.with_name("index.md")
23+
elif parts[-1].startswith("_"):
24+
continue
25+
26+
nav[parts] = doc_path.as_posix()
27+
28+
with mkdocs_gen_files.open(full_doc_path, "w") as fd:
29+
ident = ".".join(parts)
30+
fd.write(f"::: {ident}")
31+
32+
mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root))
33+
34+
with mkdocs_gen_files.open("api/SUMMARY.md", "w") as nav_file:
35+
nav_file.writelines(nav.build_literate_nav())

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,4 @@ npx -y @modelcontextprotocol/inspector
6464

6565
## API Reference
6666

67-
Full API documentation is available in the [API Reference](api.md).
67+
Full API documentation is available in the [API Reference](api/mcp/index.md).

docs/migration.md

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,30 @@ result = await session.list_resources(params=PaginatedRequestParams(cursor="next
169169
result = await session.list_tools(params=PaginatedRequestParams(cursor="next_page_token"))
170170
```
171171

172+
### `ClientSession.get_server_capabilities()` replaced by `initialize_result` property
173+
174+
`ClientSession` now stores the full `InitializeResult` via an `initialize_result` property. This provides access to `server_info`, `capabilities`, `instructions`, and the negotiated `protocol_version` through a single property. The `get_server_capabilities()` method has been removed.
175+
176+
**Before (v1):**
177+
178+
```python
179+
capabilities = session.get_server_capabilities()
180+
# server_info, instructions, protocol_version were not stored — had to capture initialize() return value
181+
```
182+
183+
**After (v2):**
184+
185+
```python
186+
result = session.initialize_result
187+
if result is not None:
188+
capabilities = result.capabilities
189+
server_info = result.server_info
190+
instructions = result.instructions
191+
version = result.protocol_version
192+
```
193+
194+
The high-level `Client.initialize_result` returns the same `InitializeResult` but is non-nullable — initialization is guaranteed inside the context manager, so no `None` check is needed. This replaces v1's `Client.server_capabilities`; use `client.initialize_result.capabilities` instead.
195+
172196
### `McpError` renamed to `MCPError`
173197

174198
The `McpError` exception class has been renamed to `MCPError` for consistent naming with the MCP acronym style used throughout the SDK.
@@ -288,6 +312,37 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app(json_response=Tru
288312

289313
**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.
290314

315+
### `MCPServer.get_context()` removed
316+
317+
`MCPServer.get_context()` has been removed. Context is now injected by the framework and passed explicitly — there is no ambient ContextVar to read from.
318+
319+
**If you were calling `get_context()` from inside a tool/resource/prompt:** use the `ctx: Context` parameter injection instead.
320+
321+
**Before (v1):**
322+
323+
```python
324+
@mcp.tool()
325+
async def my_tool(x: int) -> str:
326+
ctx = mcp.get_context()
327+
await ctx.info("Processing...")
328+
return str(x)
329+
```
330+
331+
**After (v2):**
332+
333+
```python
334+
@mcp.tool()
335+
async def my_tool(x: int, ctx: Context) -> str:
336+
await ctx.info("Processing...")
337+
return str(x)
338+
```
339+
340+
### `MCPServer.call_tool()`, `read_resource()`, `get_prompt()` now accept a `context` parameter
341+
342+
`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.
343+
344+
The internal layers (`ToolManager.call_tool`, `Tool.run`, `Prompt.render`, `ResourceTemplate.create_resource`, etc.) now require `context` as a positional argument.
345+
291346
### Replace `RootModel` by union types with `TypeAdapter` validation
292347

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

695750
### Lowlevel `Server`: `request_context` property removed
696751

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.
752+
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.
698753

699754
**Before (v1):**
700755

@@ -828,6 +883,6 @@ The lowlevel `Server` also now exposes a `session_manager` property to access th
828883

829884
If you encounter issues during migration:
830885

831-
1. Check the [API Reference](api.md) for updated method signatures
886+
1. Check the [API Reference](api/mcp/index.md) for updated method signatures
832887
2. Review the [examples](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples) for updated usage patterns
833888
3. Open an issue on [GitHub](https://github.com/modelcontextprotocol/python-sdk/issues) if you find a bug or need further assistance

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

0 commit comments

Comments
 (0)