Skip to content

Commit b87060d

Browse files
committed
feat: DispatchContext.request_id; drop RequestId arg from transport_builder
request_id is the wire-format correlation id (JSON-RPC message id; None for notifications and for dispatchers without one). Lives on DispatchContext because it's wire-format-shaped (dispatcher domain), not transport-shaped. ServerRunner._make_context will read it to populate ServerRequestContext.request_id. transport_builder no longer takes RequestId: that arg existed so the builder could put the id on a TransportContext subclass, which is now redundant with dctx.request_id. Nothing read it.
1 parent 7234f0e commit b87060d

6 files changed

Lines changed: 37 additions & 9 deletions

File tree

src/mcp/shared/direct_dispatcher.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from mcp.shared.exceptions import MCPError, NoBackChannelError
2727
from mcp.shared.message import MessageMetadata
2828
from mcp.shared.transport_context import TransportContext
29-
from mcp.types import INTERNAL_ERROR, REQUEST_TIMEOUT
29+
from mcp.types import INTERNAL_ERROR, REQUEST_TIMEOUT, RequestId
3030

3131
__all__ = ["DirectDispatcher", "create_direct_dispatcher_pair"]
3232

@@ -48,6 +48,8 @@ class _DirectDispatchContext:
4848
transport: TransportContext
4949
_back_request: _Request
5050
_back_notify: _Notify
51+
request_id: RequestId | None = None
52+
"""Always ``None``: direct dispatch has no wire-level request id."""
5153
message_metadata: MessageMetadata = None # TODO(maxisbey): remove for Context rework
5254
"""Always ``None``: in-memory dispatch attaches no transport metadata."""
5355
_on_progress: ProgressFnT | None = None

src/mcp/shared/dispatcher.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from mcp.shared.message import MessageMetadata
2626
from mcp.shared.transport_context import TransportContext
27+
from mcp.types import RequestId
2728

2829
__all__ = [
2930
"CallOptions",
@@ -108,6 +109,16 @@ def transport(self) -> TransportT_co:
108109
"""Transport-specific metadata for this inbound message."""
109110
...
110111

112+
@property
113+
def request_id(self) -> RequestId | None:
114+
"""The id of the inbound request, or ``None`` for a notification.
115+
116+
For JSON-RPC this is the wire ``id`` field. Handlers thread it through
117+
as ``related_request_id`` on outbound notifications so HTTP transports
118+
can route them onto the originating request's response stream.
119+
"""
120+
...
121+
111122
@property
112123
def message_metadata(self) -> MessageMetadata:
113124
"""The metadata the transport attached to this inbound message, if any.

src/mcp/shared/jsonrpc_dispatcher.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ class _JSONRPCDispatchContext(Generic[TransportT]):
127127
_closed: bool = False
128128
cancel_requested: anyio.Event = field(default_factory=anyio.Event)
129129

130+
@property
131+
def request_id(self) -> RequestId | None:
132+
return self._request_id
133+
130134
@property
131135
def can_send_request(self) -> bool:
132136
return self.transport.can_send_request and not self._closed
@@ -158,7 +162,7 @@ def close(self) -> None:
158162
self._closed = True
159163

160164

161-
def _default_transport_builder(_request_id: RequestId | None, _meta: MessageMetadata) -> TransportContext:
165+
def _default_transport_builder(_meta: MessageMetadata) -> TransportContext:
162166
return TransportContext(kind="jsonrpc", can_send_request=True)
163167

164168

@@ -199,7 +203,7 @@ def __init__(
199203
read_stream: ReadStream[SessionMessage | Exception],
200204
write_stream: WriteStream[SessionMessage],
201205
*,
202-
transport_builder: Callable[[RequestId | None, MessageMetadata], TransportT],
206+
transport_builder: Callable[[MessageMetadata], TransportT],
203207
peer_cancel_mode: PeerCancelMode = "interrupt",
204208
raise_handler_exceptions: bool = False,
205209
) -> None: ...
@@ -208,7 +212,7 @@ def __init__(
208212
read_stream: ReadStream[SessionMessage | Exception],
209213
write_stream: WriteStream[SessionMessage],
210214
*,
211-
transport_builder: Callable[[RequestId | None, MessageMetadata], TransportT] | None = None,
215+
transport_builder: Callable[[MessageMetadata], TransportT] | None = None,
212216
peer_cancel_mode: PeerCancelMode = "interrupt",
213217
raise_handler_exceptions: bool = False,
214218
) -> None:
@@ -218,7 +222,7 @@ def __init__(
218222
# `TransportT` is `TransportContext`, so the default is type-correct;
219223
# pyright can't see across overloads, hence the cast.
220224
self._transport_builder = cast(
221-
"Callable[[RequestId | None, MessageMetadata], TransportT]",
225+
"Callable[[MessageMetadata], TransportT]",
222226
transport_builder or _default_transport_builder,
223227
)
224228
self._peer_cancel_mode: PeerCancelMode = peer_cancel_mode
@@ -400,7 +404,7 @@ def _dispatch_request(
400404
pass
401405
case _:
402406
progress_token = None
403-
transport_ctx = self._transport_builder(req.id, metadata)
407+
transport_ctx = self._transport_builder(metadata)
404408
dctx = _JSONRPCDispatchContext(
405409
transport=transport_ctx,
406410
_dispatcher=self,
@@ -446,7 +450,7 @@ def _dispatch_notification(
446450
case _:
447451
pass
448452
# fall through: progress is also teed to on_notify
449-
transport_ctx = self._transport_builder(None, metadata)
453+
transport_ctx = self._transport_builder(metadata)
450454
dctx = _JSONRPCDispatchContext(
451455
transport=transport_ctx, _dispatcher=self, _request_id=None, message_metadata=metadata
452456
)

tests/shared/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def jsonrpc_pair(*, can_send_request: bool = True) -> DispatcherTriple:
3535
c2s_send, c2s_recv = anyio.create_memory_object_stream[SessionMessage | Exception](32)
3636
s2c_send, s2c_recv = anyio.create_memory_object_stream[SessionMessage | Exception](32)
3737

38-
def builder(_rid: object, _meta: object) -> TransportContext:
38+
def builder(_meta: object) -> TransportContext:
3939
return TransportContext(kind="jsonrpc", can_send_request=can_send_request)
4040

4141
client: JSONRPCDispatcher[TransportContext] = JSONRPCDispatcher(s2c_recv, c2s_send, transport_builder=builder)

tests/shared/test_dispatcher.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,17 @@ async def test_ctx_message_metadata_is_none_when_transport_attaches_nothing(pair
206206
assert srec.contexts[0].message_metadata is None
207207

208208

209+
@pytest.mark.anyio
210+
async def test_ctx_request_id_exposes_inbound_id(pair_factory: PairFactory):
211+
"""JSON-RPC carries the wire id through; direct dispatch has none."""
212+
async with running_pair(pair_factory) as (client, _server, _crec, srec):
213+
with anyio.fail_after(5):
214+
await client.send_raw_request("tools/call", None)
215+
await client.send_raw_request("tools/call", None)
216+
a, b = (ctx.request_id for ctx in srec.contexts)
217+
assert (a is None and b is None) or (isinstance(a, int) and isinstance(b, int) and a != b)
218+
219+
209220
@pytest.mark.anyio
210221
async def test_direct_send_raw_request_wraps_non_mcperror_exception_as_internal_error_with_cause():
211222
"""DirectDispatcher-specific: the original exception is chained via __cause__."""

tests/shared/test_jsonrpc_dispatcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ async def test_raise_handler_exceptions_true_propagates_out_of_run():
203203
c2s_send, c2s_recv = anyio.create_memory_object_stream[SessionMessage | Exception](32)
204204
s2c_send, s2c_recv = anyio.create_memory_object_stream[SessionMessage | Exception](32)
205205

206-
def builder(_rid: object, _meta: object) -> TransportContext:
206+
def builder(_meta: object) -> TransportContext:
207207
return TransportContext(kind="jsonrpc", can_send_request=True)
208208

209209
server: JSONRPCDispatcher[TransportContext] = JSONRPCDispatcher(

0 commit comments

Comments
 (0)