Skip to content

Commit b23b702

Browse files
committed
merge: resolve conflicts with upstream main
Merge upstream/main which included major refactors: - Context class moved from server.py to mcpserver/context.py (#2203) - Lowlevel Server decorators replaced with on_* kwargs (#1985) - Docstring formatting standardized with periods (#2095) - mcp.shared.progress module removed (#2080) Resolved conflicts by: - Taking upstream server.py (Context class removed) - Adding progress_callback to new context.py - Keeping progress_callback docstrings with upstream period style - Restoring RequestContext import for new tests - Updating tests to use ctx: Context injection pattern
2 parents 4effbed + 7ba41dc commit b23b702

File tree

97 files changed

+1528
-955
lines changed

Some content is hidden

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

97 files changed

+1528
-955
lines changed

.github/workflows/claude-code-review.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ jobs:
1919

2020
steps:
2121
- name: Checkout repository
22-
uses: actions/checkout@v6
22+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2323
with:
2424
fetch-depth: 1
2525

2626
- name: Run Claude Code Review
2727
id: claude-review
28-
uses: anthropics/claude-code-action@v1
28+
uses: anthropics/claude-code-action@2f8ba26a219c06cfb0f468eef8d97055fa814f97 # v1.0.53
2929
with:
3030
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
3131
plugin_marketplaces: "https://github.com/anthropics/claude-code.git"

.github/workflows/claude.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ jobs:
2727
actions: read # Required for Claude to read CI results on PRs
2828
steps:
2929
- name: Checkout repository
30-
uses: actions/checkout@v6
30+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3131
with:
3232
fetch-depth: 1
3333

3434
- name: Run Claude Code
3535
id: claude
36-
uses: anthropics/claude-code-action@v1
36+
uses: anthropics/claude-code-action@2f8ba26a219c06cfb0f468eef8d97055fa814f97 # v1.0.53
3737
with:
3838
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
3939
use_commit_signing: true

.github/workflows/publish-docs-manually.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@ jobs:
3131
3232
- run: uv sync --frozen --group docs
3333
- run: uv run --frozen --no-sync mkdocs gh-deploy --force
34+
env:
35+
ENABLE_SOCIAL_CARDS: "true"

.github/workflows/shared.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,19 @@ jobs:
2626
with:
2727
extra_args: --all-files --verbose
2828
env:
29-
SKIP: no-commit-to-branch
29+
SKIP: no-commit-to-branch,readme-v1-frozen
30+
31+
# TODO(Max): Drop this in v2.
32+
- name: Check README.md is not modified
33+
if: github.event_name == 'pull_request'
34+
run: |
35+
git fetch --no-tags --depth=1 origin "$BASE_SHA"
36+
if git diff --name-only "$BASE_SHA" -- README.md | grep -q .; then
37+
echo "::error::README.md is frozen at v1. Edit README.v2.md instead."
38+
exit 1
39+
fi
40+
env:
41+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
3042

3143
test:
3244
name: test (${{ matrix.python-version }}, ${{ matrix.dep-resolution.name }}, ${{ matrix.os }})
@@ -58,6 +70,7 @@ jobs:
5870
- name: Run pytest with coverage
5971
shell: bash
6072
run: |
73+
uv run --frozen --no-sync coverage erase
6174
uv run --frozen --no-sync coverage run -m pytest -n auto
6275
uv run --frozen --no-sync coverage combine
6376
uv run --frozen --no-sync coverage report

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ jobs:
1414
update-lockfile:
1515
runs-on: ubuntu-latest
1616
steps:
17-
- uses: actions/checkout@v6
17+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
1818

19-
- uses: astral-sh/setup-uv@v7.2.1
19+
- uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1
2020
with:
2121
version: 0.9.5
2222

@@ -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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@ 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+
- 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.
44+
- Avoid `anyio.sleep()` with a fixed duration to wait for async operations. Instead:
45+
- Use `anyio.Event` — set it in the callback/handler, `await event.wait()` in the test
46+
- For stream messages, use `await stream.receive()` instead of `sleep()` + `receive_nowait()`
47+
- Exception: `sleep()` is appropriate when testing time-based features (e.g., timeouts)
48+
- Wrap indefinite waits (`event.wait()`, `stream.receive()`) in `anyio.fail_after(5)` to prevent hangs
3149
3250
Test files mirror the source tree: `src/mcp/client/streamable_http.py` → `tests/client/test_streamable_http.py`
3351
Add tests to the existing file for that module.

SECURITY.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
# Security Policy
22

3-
Thank you for helping us keep the SDKs and systems they interact with secure.
3+
Thank you for helping keep the Model Context Protocol and its ecosystem secure.
44

55
## Reporting Security Issues
66

7-
This SDK is maintained by [Anthropic](https://www.anthropic.com/) as part of the Model Context Protocol project.
7+
If you discover a security vulnerability in this repository, please report it through
8+
the [GitHub Security Advisory process](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability)
9+
for this repository.
810

9-
The security of our systems and user data is Anthropic’s top priority. We appreciate the work of security researchers acting in good faith in identifying and reporting potential vulnerabilities.
11+
Please **do not** report security vulnerabilities through public GitHub issues, discussions,
12+
or pull requests.
1013

11-
Our security program is managed on HackerOne and we ask that any validated vulnerability in this functionality be reported through their [submission form](https://hackerone.com/anthropic-vdp/reports/new?type=team&report_type=vulnerability).
14+
## What to Include
1215

13-
## Vulnerability Disclosure Program
16+
To help us triage and respond quickly, please include:
1417

15-
Our Vulnerability Program Guidelines are defined on our [HackerOne program page](https://hackerone.com/anthropic-vdp).
18+
- A description of the vulnerability
19+
- Steps to reproduce the issue
20+
- The potential impact
21+
- Any suggested fixes (optional)

docs/migration.md

Lines changed: 64 additions & 14 deletions
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:
@@ -371,7 +402,7 @@ async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestPar
371402
server = Server("my-server", on_call_tool=handle_call_tool)
372403
```
373404

374-
### `RequestContext` and `ProgressContext` type parameters simplified
405+
### `RequestContext` type parameters simplified
375406

376407
The `RequestContext` class has been split to separate shared fields from server-specific fields. The shared `RequestContext` now only takes 1 type parameter (the session type) instead of 3.
377408

@@ -380,40 +411,59 @@ The `RequestContext` class has been split to separate shared fields from server-
380411
- Type parameters reduced from `RequestContext[SessionT, LifespanContextT, RequestT]` to `RequestContext[SessionT]`
381412
- Server-specific fields (`lifespan_context`, `experimental`, `request`, `close_sse_stream`, `close_standalone_sse_stream`) moved to new `ServerRequestContext` class in `mcp.server.context`
382413

383-
**`ProgressContext` changes:**
384-
385-
- Type parameters reduced from `ProgressContext[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT]` to `ProgressContext[SessionT]`
386-
387414
**Before (v1):**
388415

389416
```python
390417
from mcp.client.session import ClientSession
391418
from mcp.shared.context import RequestContext, LifespanContextT, RequestT
392-
from mcp.shared.progress import ProgressContext
393419

394420
# RequestContext with 3 type parameters
395421
ctx: RequestContext[ClientSession, LifespanContextT, RequestT]
396-
397-
# ProgressContext with 5 type parameters
398-
progress_ctx: ProgressContext[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT]
399422
```
400423

401424
**After (v2):**
402425

403426
```python
404427
from mcp.client.context import ClientRequestContext
405-
from mcp.client.session import ClientSession
406428
from mcp.server.context import ServerRequestContext, LifespanContextT, RequestT
407-
from mcp.shared.progress import ProgressContext
408429

409430
# For client-side context (sampling, elicitation, list_roots callbacks)
410431
ctx: ClientRequestContext
411432

412433
# For server-specific context with lifespan and request types
413434
server_ctx: ServerRequestContext[LifespanContextT, RequestT]
435+
```
436+
437+
### `ProgressContext` and `progress()` context manager removed
438+
439+
The `mcp.shared.progress` module (`ProgressContext`, `Progress`, and the `progress()` context manager) has been removed. This module had no real-world adoption — all users send progress notifications via `Context.report_progress()` or `session.send_progress_notification()` directly.
414440

415-
# ProgressContext with 1 type parameter
416-
progress_ctx: ProgressContext[ClientSession]
441+
**Before:**
442+
443+
```python
444+
from mcp.shared.progress import progress
445+
446+
with progress(ctx, total=100) as p:
447+
await p.progress(25)
448+
```
449+
450+
**After — use `Context.report_progress()` (recommended):**
451+
452+
```python
453+
@server.tool()
454+
async def my_tool(x: int, ctx: Context) -> str:
455+
await ctx.report_progress(25, 100)
456+
return "done"
457+
```
458+
459+
**After — use `session.send_progress_notification()` (low-level):**
460+
461+
```python
462+
await session.send_progress_notification(
463+
progress_token=progress_token,
464+
progress=25,
465+
total=100,
466+
)
417467
```
418468

419469
### Resource URI type changed from `AnyUrl` to `str`
@@ -675,7 +725,7 @@ If you prefer the convenience of automatic wrapping, use `MCPServer` which still
675725

676726
### Lowlevel `Server`: `request_context` property removed
677727

678-
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.
679729

680730
**Before (v1):**
681731

examples/snippets/clients/url_elicitation_client.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424

2525
import asyncio
2626
import json
27-
import subprocess
28-
import sys
2927
import webbrowser
3028
from typing import Any
3129
from urllib.parse import urlparse
@@ -56,15 +54,19 @@ async def handle_elicitation(
5654
)
5755

5856

57+
ALLOWED_SCHEMES = {"http", "https"}
58+
59+
5960
async def handle_url_elicitation(
6061
params: types.ElicitRequestParams,
6162
) -> types.ElicitResult:
6263
"""Handle URL mode elicitation - show security warning and optionally open browser.
6364
6465
This function demonstrates the security-conscious approach to URL elicitation:
65-
1. Display the full URL and domain for user inspection
66-
2. Show the server's reason for requesting this interaction
67-
3. Require explicit user consent before opening any URL
66+
1. Validate the URL scheme before prompting the user
67+
2. Display the full URL and domain for user inspection
68+
3. Show the server's reason for requesting this interaction
69+
4. Require explicit user consent before opening any URL
6870
"""
6971
# Extract URL parameters - these are available on URL mode requests
7072
url = getattr(params, "url", None)
@@ -75,6 +77,12 @@ async def handle_url_elicitation(
7577
print("Error: No URL provided in elicitation request")
7678
return types.ElicitResult(action="cancel")
7779

80+
# Reject dangerous URL schemes before prompting the user
81+
parsed = urlparse(str(url))
82+
if parsed.scheme.lower() not in ALLOWED_SCHEMES:
83+
print(f"\nRejecting URL with disallowed scheme '{parsed.scheme}': {url}")
84+
return types.ElicitResult(action="decline")
85+
7886
# Extract domain for security display
7987
domain = extract_domain(url)
8088

@@ -105,7 +113,11 @@ async def handle_url_elicitation(
105113

106114
# Open the browser
107115
print(f"\nOpening browser to: {url}")
108-
open_browser(url)
116+
try:
117+
webbrowser.open(url)
118+
except Exception as e:
119+
print(f"Failed to open browser: {e}")
120+
print(f"Please manually open: {url}")
109121

110122
print("Waiting for you to complete the interaction in your browser...")
111123
print("(The server will continue once you've finished)")
@@ -121,20 +133,6 @@ def extract_domain(url: str) -> str:
121133
return "unknown"
122134

123135

124-
def open_browser(url: str) -> None:
125-
"""Open URL in the default browser."""
126-
try:
127-
if sys.platform == "darwin":
128-
subprocess.run(["open", url], check=False)
129-
elif sys.platform == "win32":
130-
subprocess.run(["start", url], shell=True, check=False)
131-
else:
132-
webbrowser.open(url)
133-
except Exception as e:
134-
print(f"Failed to open browser: {e}")
135-
print(f"Please manually open: {url}")
136-
137-
138136
async def call_tool_with_error_handling(
139137
session: ClientSession,
140138
tool_name: str,

mkdocs.yml

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

113113
plugins:
114114
- search
115-
- social
115+
- social:
116+
enabled: !ENV [ENABLE_SOCIAL_CARDS, false]
116117
- glightbox
117118
- mkdocstrings:
118119
handlers:

0 commit comments

Comments
 (0)