Skip to content

Commit 87fdbc4

Browse files
committed
review: address trivial-directive threads + t02/t15/t19
t01: direct_dispatcher locals left/right -> client/server t03: migration.md drop "(workaround)", add_request_handler is the supported path t06+07: Server.middleware comment rewritten + TODO (was stale post-swap) t09: delete stale ServerRegistry section header t11: drop Connection.client_info/.client_capabilities properties (no readers; v1 only exposed client_params; check_capability reads through) t12: send_raw_request docstrings point at CallOptions keys t14+26: drop dump_params from peer.__all__ t20+02: resumption_token/on_resumption_token annotated client-side/SHTTP-only, and as 2025-11-25-and-earlier (resumption removed in next protocol rev) t24: _route_notification docstring (correlation rationale) t15+19: drop unneeded `from __future__ import annotations` from server/context.py and server/session.py (no cycle, no forward refs) t05+08 answered (no code change): HandlerEntry.handler Any is required storage erasure; correlation enforced at add_request_handler.
1 parent d243445 commit 87fdbc4

11 files changed

Lines changed: 46 additions & 50 deletions

File tree

docs/migration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ async def my_tool(x: int, ctx: Context) -> str:
428428

429429
The internal layers (`ToolManager.call_tool`, `Tool.run`, `Prompt.render`, `ResourceTemplate.create_resource`, etc.) now require `context` as a positional argument.
430430

431-
### Registering lowlevel handlers on `MCPServer` (workaround)
431+
### Registering lowlevel handlers from `MCPServer`
432432

433433
`MCPServer` does not expose public APIs for `subscribe_resource`, `unsubscribe_resource`, or `set_logging_level` handlers. In v1, the workaround was to reach into the private lowlevel server and use its decorator methods:
434434

@@ -442,7 +442,7 @@ async def handle_set_logging_level(level: str) -> None:
442442
mcp._mcp_server.subscribe_resource()(handle_subscribe) # pyright: ignore[reportPrivateUsage]
443443
```
444444

445-
In v2, the lowlevel `Server` no longer has decorator methods (handlers are constructor-only), so the equivalent workaround is `add_request_handler`:
445+
In v2, the lowlevel `Server` supports arbitrary request handlers directly via `add_request_handler` (the decorator methods are gone; handlers are otherwise constructor-only). From `MCPServer`, access it via `_lowlevel_server`:
446446

447447
**After (v2):**
448448

src/mcp/server/connection.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from mcp.shared.dispatcher import CallOptions, Outbound
2525
from mcp.shared.exceptions import NoBackChannelError
2626
from mcp.shared.peer import Meta, dump_params
27-
from mcp.types import ClientCapabilities, Implementation, InitializeRequestParams, LoggingLevel
27+
from mcp.types import ClientCapabilities, InitializeRequestParams, LoggingLevel
2828

2929
__all__ = ["Connection"]
3030

@@ -72,16 +72,6 @@ def __init__(self, outbound: Outbound, *, has_standalone_channel: bool, session_
7272
raised by callbacks are logged and swallowed; they never propagate
7373
out of `ServerRunner.run()`."""
7474

75-
@property
76-
def client_info(self) -> Implementation | None:
77-
"""The client's `Implementation` from `initialize`; `None` before initialization."""
78-
return self.client_params.client_info if self.client_params is not None else None
79-
80-
@property
81-
def client_capabilities(self) -> ClientCapabilities | None:
82-
"""The client's `ClientCapabilities` from `initialize`; `None` before initialization."""
83-
return self.client_params.capabilities if self.client_params is not None else None
84-
8575
async def send_raw_request(
8676
self,
8777
method: str,
@@ -92,7 +82,8 @@ async def send_raw_request(
9282
9383
Low-level `Outbound` channel. Prefer the typed `send_request` (from
9484
`TypedServerRequestMixin`) or the convenience methods below; use this
95-
directly only for off-spec messages.
85+
directly only for off-spec messages. `opts` carries per-call `timeout`
86+
/ `on_progress` / resumption hints; see `CallOptions`.
9687
9788
Raises:
9889
MCPError: The peer responded with an error.
@@ -151,9 +142,9 @@ def check_capability(self, capability: ClientCapabilities) -> bool:
151142
"""
152143
# TODO: redesign - mirrors v1 ServerSession.check_client_capability
153144
# verbatim for parity.
154-
if self.client_capabilities is None:
145+
if self.client_params is None:
155146
return False
156-
have = self.client_capabilities
147+
have = self.client_params.capabilities
157148
if capability.roots is not None:
158149
if have.roots is None:
159150
return False

src/mcp/server/context.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from __future__ import annotations
2-
31
from collections.abc import Awaitable, Callable, Mapping
42
from dataclasses import dataclass
53
from typing import Any, Generic, Protocol

src/mcp/server/lowlevel/server.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,11 @@ def __init__(
215215
self._request_handlers: dict[str, HandlerEntry[LifespanResultT]] = {}
216216
self._notification_handlers: dict[str, HandlerEntry[LifespanResultT]] = {}
217217
self._session_manager: StreamableHTTPSessionManager | None = None
218-
# Context-tier middleware consumed by `ServerRunner`. Additive; the
219-
# existing `run()` path ignores it.
218+
# Context-tier middleware: wraps each request handler with
219+
# `(ctx, method, params, call_next)`. Applied in `ServerRunner._on_request`.
220+
# TODO(maxisbey): provisional - signature and semantics change with the
221+
# Context/middleware rework (covariant `Context[L]`, outbound seam) before
222+
# v2 final.
220223
self.middleware: list[ServerMiddleware[LifespanResultT]] = []
221224
logger.debug("Initializing server %r", name)
222225

@@ -272,8 +275,6 @@ def add_notification_handler(
272275
"""
273276
self._notification_handlers[method] = HandlerEntry(params_type, handler)
274277

275-
# --- ServerRegistry protocol (consumed by ServerRunner) ------------------
276-
277278
def get_request_handler(self, method: str) -> HandlerEntry[LifespanResultT] | None:
278279
"""Return the registered entry for a request method, or `None`."""
279280
return self._request_handlers.get(method)

src/mcp/server/session.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,18 @@
99
used to live here are now owned by `JSONRPCDispatcher` and `ServerRunner`.
1010
"""
1111

12-
from __future__ import annotations
13-
14-
from typing import TYPE_CHECKING, Any, TypeVar, overload
12+
from typing import Any, TypeVar, overload
1513

1614
from pydantic import AnyUrl, BaseModel
1715

1816
from mcp import types
17+
from mcp.server.connection import Connection
1918
from mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages
2019
from mcp.shared.dispatcher import CallOptions, ProgressFnT
2120
from mcp.shared.exceptions import StatelessModeNotSupported
2221
from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher
2322
from mcp.shared.message import MessageMetadata, ServerMessageMetadata
2423

25-
if TYPE_CHECKING:
26-
from mcp.server.connection import Connection
27-
2824
__all__ = ["ServerSession"]
2925

3026
ResultT = TypeVar("ResultT", bound=BaseModel)

src/mcp/shared/direct_dispatcher.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -186,12 +186,12 @@ def create_direct_dispatcher_pair(
186186
headers: Sets `TransportContext.headers` on both sides.
187187
188188
Returns:
189-
A `(left, right)` pair. Conventionally `left` is the client side
190-
and `right` is the server side, but the wiring is symmetric.
189+
A `(client, server)` pair. The wiring is symmetric, so the roles
190+
are conventional only.
191191
"""
192192
ctx = TransportContext(kind=DIRECT_TRANSPORT_KIND, can_send_request=can_send_request, headers=headers)
193-
left = DirectDispatcher(ctx)
194-
right = DirectDispatcher(ctx)
195-
left.connect_to(right)
196-
right.connect_to(left)
197-
return left, right
193+
client = DirectDispatcher(ctx)
194+
server = DirectDispatcher(ctx)
195+
client.connect_to(server)
196+
server.connect_to(client)
197+
return client, server

src/mcp/shared/dispatcher.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,20 @@ class CallOptions(TypedDict, total=False):
5959
"""Receive `notifications/progress` updates for this request."""
6060

6161
resumption_token: str
62-
"""Opaque token to resume a previously interrupted request (transport-dependent)."""
62+
"""Opaque token to resume a previously interrupted request.
63+
64+
Client-side, streamable-HTTP only. Ignored by server dispatchers and other
65+
transports. Supports protocol version 2025-11-25 and earlier; SSE-stream
66+
resumption is removed in the next protocol revision.
67+
"""
6368

6469
on_resumption_token: Callable[[str], Awaitable[None]]
65-
"""Receive a resumption token when the transport issues one."""
70+
"""Receive a resumption token when the transport issues one for this request.
71+
72+
Client-side, streamable-HTTP only. Ignored by server dispatchers and other
73+
transports. Supports protocol version 2025-11-25 and earlier; SSE-stream
74+
resumption is removed in the next protocol revision.
75+
"""
6676

6777

6878
@runtime_checkable
@@ -83,6 +93,9 @@ async def send_raw_request(
8393
) -> dict[str, Any]:
8494
"""Send a request and await its raw result dict.
8595
96+
`opts` carries per-call `timeout` / `on_progress` / resumption hints;
97+
see `CallOptions`.
98+
8699
Raises:
87100
MCPError: If the peer responded with an error, or the handler
88101
raised. Implementations normalize all handler exceptions to

src/mcp/shared/jsonrpc_dispatcher.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,13 @@ def _dispatch_notification(
504504
on_notify: OnNotify,
505505
sender_ctx: contextvars.Context | None,
506506
) -> None:
507+
"""Route one inbound notification.
508+
509+
`notifications/cancelled` and `notifications/progress` are intercepted
510+
here because they correlate against JSON-RPC request IDs - the
511+
`_in_flight` / `_pending` tables this layer owns - so no higher layer
512+
can act on them. See the module docstring for the design rationale.
513+
"""
507514
if msg.method == "notifications/cancelled":
508515
match msg.params:
509516
case {"requestId": str() | int() as rid} if (in_flight := self._in_flight.get(rid)) is not None:

src/mcp/shared/peer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
ToolChoice,
3333
)
3434

35-
__all__ = ["Meta", "Peer", "PeerMixin", "dump_params"]
35+
__all__ = ["Meta", "Peer", "PeerMixin"]
3636

3737
Meta = dict[str, Any]
3838
"""Type alias for the `_meta` field carried on request/notification params."""

tests/server/test_connection.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -235,16 +235,6 @@ def test_check_capability_per_field_branches(have: ClientCapabilities, want: Cli
235235
assert conn.check_capability(want) is expected
236236

237237

238-
def test_connection_client_info_and_capabilities_derive_from_client_params():
239-
conn = Connection(StubOutbound(), has_standalone_channel=True)
240-
assert conn.client_info is None
241-
assert conn.client_capabilities is None
242-
caps = ClientCapabilities(sampling=SamplingCapability())
243-
conn.client_params = _client_params(caps)
244-
assert conn.client_info is not None and conn.client_info.name == "t"
245-
assert conn.client_capabilities == caps
246-
247-
248238
def test_connection_check_capability_true_when_client_declares_it():
249239
conn = Connection(StubOutbound(), has_standalone_channel=True)
250240
conn.client_params = _client_params(

0 commit comments

Comments
 (0)