Skip to content

Commit dcbe6e8

Browse files
committed
Sweep: route report_progress through DispatchContext.progress; bump LATEST_PROTOCOL_VERSION; orphan cleanup
- Context.report_progress now delegates to DispatchContext.progress() via ServerSession.report_progress (was: token-gated send_notification, which only worked under JSONRPCDispatcher). Progress now reaches the client on the in-process modern path; 4 progress-notification xfails flip to pass. ServerSession's request_outbound is typed DispatchContext (it always was one at runtime). - LATEST_PROTOCOL_VERSION bumped to '2026-07-28' (the newest revision the SDK supports). Handshake-outcome assertions and mock-InitializeResult fixtures switched to HANDSHAKE_PROTOCOL_VERSIONS[-1]. migration.md entry. - ServerMessageMetadata.protocol_version deleted (no readers, no writers). - ClientSession.send_progress_notification and Client.send_progress_notification deprecated (client-to-server progress is server-to-client only at 2026-07-28). - Mcp-Name TODO re-anchored on _make_modern_stamp.
1 parent 46c0742 commit dcbe6e8

22 files changed

Lines changed: 182 additions & 146 deletions

docs/migration.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,8 @@ async def my_tool(ctx: Context[MyLifespanState]) -> str: ...
776776

777777
`SUPPORTED_PROTOCOL_VERSIONS` is deprecated — it's now the union of `HANDSHAKE_PROTOCOL_VERSIONS` (initialize-handshake versions) and `MODERN_PROTOCOL_VERSIONS` (per-request-envelope versions). If you were using it to mean "versions the initialize handshake accepts", switch to `HANDSHAKE_PROTOCOL_VERSIONS`.
778778

779+
`LATEST_PROTOCOL_VERSION` now reflects the newest protocol revision the SDK supports (`2026-07-28`). Code that used it to mean "the version `.initialize()` offers" should switch to `HANDSHAKE_PROTOCOL_VERSIONS[-1]`.
780+
779781
### `ProgressContext` and `progress()` context manager removed
780782

781783
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.
@@ -1301,6 +1303,12 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
13011303

13021304
No migration is required during the deprecation window. New code should avoid building on these features, since they may be removed in a future spec version.
13031305

1306+
### Client-to-server progress deprecated (2026-07-28)
1307+
1308+
The 2026-07-28 spec restricts `notifications/progress` to the server-to-client direction only — `ProgressNotification` is no longer in `ClientNotification`. `Client.send_progress_notification()` and `ClientSession.send_progress_notification()` now carry `typing_extensions.deprecated` and emit `mcp.MCPDeprecationWarning` at runtime. They continue to work against servers negotiating 2025-11-25 or earlier.
1309+
1310+
On the server side, prefer the new dispatcher-agnostic `ServerSession.report_progress(progress, total, message)` (and `Context.report_progress()` on `MCPServer`) over the raw `ServerSession.send_progress_notification(progress_token, …)`. `report_progress` encapsulates the "no-op when the caller did not request progress" rule and works on every dispatcher; the raw token-taking form remains for handlers that read `_meta.progressToken` directly.
1311+
13041312
## Bug Fixes
13051313

13061314
### OAuth metadata URLs no longer gain a trailing slash

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@ filterwarnings = [
217217
# them internally (e.g. `ctx.debug` -> `log` -> `send_log_message`), so the
218218
# advisory warning is silenced. Tests asserting it opt back in with pytest.warns.
219219
"ignore:.*is deprecated as of 2026-07-28 \\(SEP-2577\\).:mcp.MCPDeprecationWarning",
220+
# 2026-07-28 restricts progress to server->client; the client send path is
221+
# advisory-deprecated and a handful of tests still exercise it.
222+
"ignore:Client-to-server progress is deprecated as of 2026-07-28.*:mcp.MCPDeprecationWarning",
220223
]
221224

222225
[tool.markdown.lint]

src/mcp/client/client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,10 @@ async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> EmptyResu
239239
"""Send a ping request to the server."""
240240
return await self.session.send_ping(meta=meta)
241241

242+
@deprecated(
243+
"Client-to-server progress is deprecated as of 2026-07-28; progress is server-to-client only.",
244+
category=MCPDeprecationWarning,
245+
)
242246
async def send_progress_notification(
243247
self,
244248
progress_token: str | int,
@@ -247,7 +251,7 @@ async def send_progress_notification(
247251
message: str | None = None,
248252
) -> None:
249253
"""Send a progress notification to the server."""
250-
await self.session.send_progress_notification(
254+
await self.session.send_progress_notification( # pyright: ignore[reportDeprecated]
251255
progress_token=progress_token,
252256
progress=progress,
253257
total=total,

src/mcp/client/session.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def stamp(data: dict[str, Any], opts: CallOptions) -> None:
7272
headers = opts.setdefault("headers", {})
7373
headers[MCP_PROTOCOL_VERSION_HEADER] = protocol_version
7474
headers[MCP_METHOD_HEADER] = data["method"]
75+
# TODO: also emit Mcp-Name for prompts/get (params.name) and resources/read (params.uri)
7576
if data["method"] == "tools/call" and isinstance(name := params.get("name"), str):
7677
headers[MCP_NAME_HEADER] = encode_header_value(name)
7778

@@ -451,6 +452,10 @@ async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> types.Emp
451452
"""Send a ping request."""
452453
return await self.send_request(types.PingRequest(params=types.RequestParams(_meta=meta)), types.EmptyResult)
453454

455+
@deprecated(
456+
"Client-to-server progress is deprecated as of 2026-07-28; progress is server-to-client only.",
457+
category=MCPDeprecationWarning,
458+
)
454459
async def send_progress_notification(
455460
self,
456461
progress_token: str | int,

src/mcp/server/mcpserver/context.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,18 +94,7 @@ async def report_progress(self, progress: float, total: float | None = None, mes
9494
total: Optional total value (e.g., 100)
9595
message: Optional message (e.g., "Starting render...")
9696
"""
97-
progress_token = self.request_context.meta.get("progress_token") if self.request_context.meta else None
98-
99-
if progress_token is None:
100-
return
101-
102-
await self.request_context.session.send_progress_notification(
103-
progress_token=progress_token,
104-
progress=progress,
105-
total=total,
106-
message=message,
107-
related_request_id=self.request_id,
108-
)
97+
await self.request_context.session.report_progress(progress, total, message)
10998

11099
async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]:
111100
"""Read a resource by URI.

src/mcp/server/session.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from mcp import types
1515
from mcp.server.connection import Connection
1616
from mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages
17-
from mcp.shared.dispatcher import CallOptions, Outbound, ProgressFnT
17+
from mcp.shared.dispatcher import CallOptions, DispatchContext, ProgressFnT
1818
from mcp.shared.exceptions import MCPDeprecationWarning
1919
from mcp.shared.message import ServerMessageMetadata
2020
from mcp.types import methods as _methods
@@ -36,7 +36,7 @@ class ServerSession:
3636
never crosses the `Outbound` Protocol.
3737
"""
3838

39-
def __init__(self, request_outbound: Outbound, connection: Connection) -> None:
39+
def __init__(self, request_outbound: DispatchContext[Any], connection: Connection) -> None:
4040
self._request_outbound = request_outbound
4141
self._connection = connection
4242

@@ -353,6 +353,16 @@ async def send_ping(self) -> types.EmptyResult:
353353
types.EmptyResult,
354354
)
355355

356+
async def report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None:
357+
"""Report progress for the inbound request this session is scoped to.
358+
359+
A no-op when the caller did not request progress. Dispatcher-agnostic:
360+
on JSON-RPC the held `DispatchContext` emits ``notifications/progress``
361+
against the caller's token; on the in-process direct dispatcher it
362+
invokes the caller's callback directly.
363+
"""
364+
await self._request_outbound.progress(progress, total, message)
365+
356366
async def send_progress_notification(
357367
self,
358368
progress_token: str | int,

src/mcp/shared/message.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,6 @@ class ServerMessageMetadata:
3737
# transports, None for stdio). Typed as Any because the server layer is
3838
# transport-agnostic.
3939
request_context: Any = None
40-
# Per-message protocol version observed by the transport (e.g. the
41-
# validated MCP-Protocol-Version header).
42-
protocol_version: str | None = None
4340
# Callback to close SSE stream for the current request without terminating
4441
close_sse_stream: CloseSSEStreamCallback | None = None
4542
# Callback to close the standalone GET SSE stream (for unsolicited notifications)

src/mcp/types/_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
from mcp.types.jsonrpc import RequestId
2424

25-
LATEST_PROTOCOL_VERSION: Final[str] = "2025-11-25"
25+
LATEST_PROTOCOL_VERSION: Final[str] = "2026-07-28"
2626
"""The newest protocol version this SDK can negotiate.
2727
2828
See https://modelcontextprotocol.io/specification/latest.

tests/client/test_client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from mcp.client.streamable_http import streamable_http_client
1818
from mcp.server import Server, ServerRequestContext
1919
from mcp.server.mcpserver import MCPServer
20+
from mcp.shared.version import HANDSHAKE_PROTOCOL_VERSIONS
2021
from mcp.types import (
2122
CallToolResult,
2223
EmptyResult,
@@ -118,7 +119,7 @@ async def test_client_is_initialized(app: MCPServer):
118119
async def test_client_initialize_result_exposes_negotiated_protocol_version(app: MCPServer):
119120
"""The negotiated protocol version is readable after initialization."""
120121
async with Client(app) as client:
121-
assert client.initialize_result.protocol_version == types.LATEST_PROTOCOL_VERSION
122+
assert client.initialize_result.protocol_version == HANDSHAKE_PROTOCOL_VERSIONS[-1]
122123

123124

124125
async def test_client_with_simple_server(simple_server: Server):
@@ -241,7 +242,7 @@ async def handle_progress(ctx: ServerRequestContext, params: types.ProgressNotif
241242
server = Server(name="test_server", on_progress=handle_progress)
242243

243244
async with Client(server) as client:
244-
await client.send_progress_notification(progress_token="token123", progress=50.0)
245+
await client.send_progress_notification(progress_token="token123", progress=50.0) # pyright: ignore[reportDeprecated]
245246
await event.wait()
246247
assert received_from_client == snapshot({"progress_token": "token123", "progress": 50.0})
247248

tests/client/test_session.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
CONNECTION_CLOSED,
2424
INTERNAL_ERROR,
2525
INVALID_PARAMS,
26-
LATEST_PROTOCOL_VERSION,
2726
METHOD_NOT_FOUND,
2827
PROTOCOL_VERSION_META_KEY,
2928
REQUEST_TIMEOUT,
@@ -88,7 +87,7 @@ async def mock_server():
8887
assert isinstance(request, InitializeRequest)
8988

9089
result = InitializeResult(
91-
protocol_version=LATEST_PROTOCOL_VERSION,
90+
protocol_version=HANDSHAKE_PROTOCOL_VERSIONS[-1],
9291
capabilities=ServerCapabilities(
9392
logging=None,
9493
resources=None,
@@ -141,7 +140,7 @@ async def message_handler( # pragma: no cover
141140

142141
# Assert the result
143142
assert isinstance(result, InitializeResult)
144-
assert result.protocol_version == LATEST_PROTOCOL_VERSION
143+
assert result.protocol_version == HANDSHAKE_PROTOCOL_VERSIONS[-1]
145144
assert isinstance(result.capabilities, ServerCapabilities)
146145
assert result.server_info == Implementation(name="mock-server", version="0.1.0")
147146
assert result.instructions == "The server instructions."
@@ -172,7 +171,7 @@ async def mock_server():
172171
received_client_info = request.params.client_info
173172

174173
result = InitializeResult(
175-
protocol_version=LATEST_PROTOCOL_VERSION,
174+
protocol_version=HANDSHAKE_PROTOCOL_VERSIONS[-1],
176175
capabilities=ServerCapabilities(),
177176
server_info=Implementation(name="mock-server", version="0.1.0"),
178177
)
@@ -229,7 +228,7 @@ async def mock_server():
229228
received_client_info = request.params.client_info
230229

231230
result = InitializeResult(
232-
protocol_version=LATEST_PROTOCOL_VERSION,
231+
protocol_version=HANDSHAKE_PROTOCOL_VERSIONS[-1],
233232
capabilities=ServerCapabilities(),
234233
server_info=Implementation(name="mock-server", version="0.1.0"),
235234
)
@@ -278,8 +277,8 @@ async def mock_server():
278277
)
279278
assert isinstance(request, InitializeRequest)
280279

281-
# Verify client sent the latest protocol version
282-
assert request.params.protocol_version == LATEST_PROTOCOL_VERSION
280+
# Verify client offers the newest handshake protocol version
281+
assert request.params.protocol_version == HANDSHAKE_PROTOCOL_VERSIONS[-1]
283282

284283
# Server responds with a supported older version
285284
result = InitializeResult(
@@ -387,7 +386,7 @@ async def mock_server():
387386
received_capabilities = request.params.capabilities
388387

389388
result = InitializeResult(
390-
protocol_version=LATEST_PROTOCOL_VERSION,
389+
protocol_version=HANDSHAKE_PROTOCOL_VERSIONS[-1],
391390
capabilities=ServerCapabilities(),
392391
server_info=Implementation(name="mock-server", version="0.1.0"),
393392
)
@@ -458,7 +457,7 @@ async def mock_server():
458457
received_capabilities = request.params.capabilities
459458

460459
result = InitializeResult(
461-
protocol_version=LATEST_PROTOCOL_VERSION,
460+
protocol_version=HANDSHAKE_PROTOCOL_VERSIONS[-1],
462461
capabilities=ServerCapabilities(),
463462
server_info=Implementation(name="mock-server", version="0.1.0"),
464463
)
@@ -537,7 +536,7 @@ async def mock_server():
537536
received_capabilities = request.params.capabilities
538537

539538
result = InitializeResult(
540-
protocol_version=LATEST_PROTOCOL_VERSION,
539+
protocol_version=HANDSHAKE_PROTOCOL_VERSIONS[-1],
541540
capabilities=ServerCapabilities(),
542541
server_info=Implementation(name="mock-server", version="0.1.0"),
543542
)
@@ -605,7 +604,7 @@ async def mock_server():
605604
assert isinstance(request, InitializeRequest)
606605

607606
result = InitializeResult(
608-
protocol_version=LATEST_PROTOCOL_VERSION,
607+
protocol_version=HANDSHAKE_PROTOCOL_VERSIONS[-1],
609608
capabilities=expected_capabilities,
610609
server_info=expected_server_info,
611610
instructions=expected_instructions,
@@ -644,7 +643,7 @@ async def mock_server():
644643
assert result.server_info == expected_server_info
645644
assert result.capabilities == expected_capabilities
646645
assert result.instructions == expected_instructions
647-
assert result.protocol_version == LATEST_PROTOCOL_VERSION
646+
assert result.protocol_version == HANDSHAKE_PROTOCOL_VERSIONS[-1]
648647

649648

650649
@pytest.mark.anyio
@@ -667,7 +666,7 @@ async def mock_server():
667666
assert isinstance(request, InitializeRequest)
668667

669668
result = InitializeResult(
670-
protocol_version=LATEST_PROTOCOL_VERSION,
669+
protocol_version=HANDSHAKE_PROTOCOL_VERSIONS[-1],
671670
capabilities=ServerCapabilities(),
672671
server_info=Implementation(name="mock-server", version="0.1.0"),
673672
)
@@ -1348,7 +1347,7 @@ async def send_raw_request(
13481347
self.calls.append((method, opts or {}))
13491348
if method == "initialize":
13501349
return InitializeResult(
1351-
protocol_version=LATEST_PROTOCOL_VERSION,
1350+
protocol_version=HANDSHAKE_PROTOCOL_VERSIONS[-1],
13521351
capabilities=ServerCapabilities(),
13531352
server_info=Implementation(name="mock-server", version="0.1.0"),
13541353
).model_dump(by_alias=True, mode="json", exclude_none=True)

0 commit comments

Comments
 (0)