Skip to content

Commit 8f065c0

Browse files
authored
Merge branch 'main' into localden/tasks-session
2 parents 113f35c + 7ba41dc commit 8f065c0

39 files changed

+667
-491
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()`

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

scripts/test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
set -ex
44

5+
uv run --frozen coverage erase
56
uv run --frozen coverage run -m pytest -n auto $@
67
uv run --frozen coverage combine
78
uv run --frozen coverage report

src/mcp/client/auth/oauth2.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,9 @@ def prepare_token_auth(
205205
headers["Authorization"] = f"Basic {encoded_credentials}"
206206
# Don't include client_secret in body for basic auth
207207
data = {k: v for k, v in data.items() if k != "client_secret"}
208-
elif auth_method == "client_secret_post" and self.client_info.client_secret:
209-
# Include client_secret in request body
208+
elif auth_method == "client_secret_post" and self.client_info.client_id and self.client_info.client_secret:
209+
# Include client_id and client_secret in request body (RFC 6749 §2.3.1)
210+
data["client_id"] = self.client_info.client_id
210211
data["client_secret"] = self.client_info.client_secret
211212
# For auth_method == "none", don't add any client_secret
212213

src/mcp/server/lowlevel/server.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ async def main():
3636

3737
from __future__ import annotations
3838

39-
import contextvars
4039
import logging
4140
import warnings
4241
from collections.abc import AsyncIterator, Awaitable, Callable
@@ -74,8 +73,6 @@ async def main():
7473

7574
LifespanResultT = TypeVar("LifespanResultT", default=Any)
7675

77-
request_ctx: contextvars.ContextVar[ServerRequestContext[Any]] = contextvars.ContextVar("request_ctx")
78-
7976

8077
class NotificationOptions:
8178
def __init__(self, prompts_changed: bool = False, resources_changed: bool = False, tools_changed: bool = False):
@@ -476,11 +473,7 @@ async def _handle_request(
476473
close_sse_stream=close_sse_stream_cb,
477474
close_standalone_sse_stream=close_standalone_sse_stream_cb,
478475
)
479-
token = request_ctx.set(ctx)
480-
try:
481-
response = await handler(ctx, req.params)
482-
finally:
483-
request_ctx.reset(token)
476+
response = await handler(ctx, req.params)
484477
except MCPError as err:
485478
response = err.error
486479
except anyio.get_cancelled_exc_class():

src/mcp/server/mcpserver/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from mcp.types import Icon
44

5-
from .server import Context, MCPServer
5+
from .context import Context
6+
from .server import MCPServer
67
from .utilities.types import Audio, Image
78

89
__all__ = ["MCPServer", "Context", "Image", "Audio", "Icon"]

0 commit comments

Comments
 (0)