Skip to content

Commit 1da25ec

Browse files
committed
refactor: rename Dispatcher.call to send_request, replace RequestSender with Outbound
The design doc's `send_request = call` alias only makes the concrete class satisfy RequestSender, not the abstract Dispatcher Protocol — so any consumer typed against `Dispatcher[TT]` (Connection, ServerRunner) couldn't pass it to something expecting a RequestSender without a cast or hand-written bridge. RequestSender was also half a contract: every implementor (Dispatcher, DispatchContext, Connection, Context) has `notify` too, and PeerMixin needs both for its typed sugar (elicit/sample are requests, log is a notification). Outbound(Protocol) declares both methods; Dispatcher and DispatchContext extend it. PeerMixin will wrap an Outbound. One verb everywhere, no aliases, no extra Protocols. - Dispatcher.call -> send_request - OnCall -> OnRequest, on_call -> on_request - RequestSender -> Outbound (now also declares notify) - Dispatcher(Outbound, Protocol[TT]), DispatchContext(Outbound, Protocol[TT])
1 parent 5540d80 commit 1da25ec

File tree

3 files changed

+115
-138
lines changed

3 files changed

+115
-138
lines changed

src/mcp/shared/direct_dispatcher.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""In-memory `Dispatcher` that wires two peers together with no transport.
22
3-
`DirectDispatcher` is the simplest possible `Dispatcher` implementation: a call
4-
on one side directly invokes the other side's `on_call`. There is no
3+
`DirectDispatcher` is the simplest possible `Dispatcher` implementation: a
4+
request on one side directly invokes the other side's `on_request`. There is no
55
serialization, no JSON-RPC framing, and no streams. It exists to:
66
77
* prove the `Dispatcher` Protocol is implementable without JSON-RPC
@@ -21,7 +21,7 @@
2121

2222
import anyio
2323

24-
from mcp.shared.dispatcher import CallOptions, OnCall, OnNotify, ProgressFnT
24+
from mcp.shared.dispatcher import CallOptions, OnNotify, OnRequest, ProgressFnT
2525
from mcp.shared.exceptions import MCPError, NoBackChannelError
2626
from mcp.shared.transport_context import TransportContext
2727
from mcp.types import INTERNAL_ERROR, REQUEST_TIMEOUT
@@ -31,20 +31,20 @@
3131
DIRECT_TRANSPORT_KIND = "direct"
3232

3333

34-
_Call = Callable[[str, Mapping[str, Any] | None, CallOptions | None], Awaitable[dict[str, Any]]]
34+
_Request = Callable[[str, Mapping[str, Any] | None, CallOptions | None], Awaitable[dict[str, Any]]]
3535
_Notify = Callable[[str, Mapping[str, Any] | None], Awaitable[None]]
3636

3737

3838
@dataclass
3939
class _DirectDispatchContext:
40-
"""`DispatchContext` for an inbound call on a `DirectDispatcher`.
40+
"""`DispatchContext` for an inbound request on a `DirectDispatcher`.
4141
4242
The back-channel callables target the *originating* side, so a handler's
43-
`send_request` reaches the peer that made the inbound call.
43+
`send_request` reaches the peer that made the inbound request.
4444
"""
4545

4646
transport: TransportContext
47-
_back_call: _Call
47+
_back_request: _Request
4848
_back_notify: _Notify
4949
_on_progress: ProgressFnT | None = None
5050
cancel_requested: anyio.Event = field(default_factory=anyio.Event)
@@ -60,7 +60,7 @@ async def send_request(
6060
) -> dict[str, Any]:
6161
if not self.transport.can_send_request:
6262
raise NoBackChannelError(method)
63-
return await self._back_call(method, params, opts)
63+
return await self._back_request(method, params, opts)
6464

6565
async def progress(self, progress: float, total: float | None = None, message: str | None = None) -> None:
6666
if self._on_progress is not None:
@@ -71,38 +71,38 @@ class DirectDispatcher:
7171
"""A `Dispatcher` that calls a peer's handlers directly, in-process.
7272
7373
Two instances are wired together with `create_direct_dispatcher_pair`; each
74-
holds a reference to the other. `call` on one awaits the peer's `on_call`.
75-
`run` parks until `close` is called.
74+
holds a reference to the other. `send_request` on one awaits the peer's
75+
`on_request`. `run` parks until `close` is called.
7676
"""
7777

7878
def __init__(self, transport_ctx: TransportContext):
7979
self._transport_ctx = transport_ctx
8080
self._peer: DirectDispatcher | None = None
81-
self._on_call: OnCall | None = None
81+
self._on_request: OnRequest | None = None
8282
self._on_notify: OnNotify | None = None
8383
self._ready = anyio.Event()
8484
self._closed = anyio.Event()
8585

8686
def connect_to(self, peer: DirectDispatcher) -> None:
8787
self._peer = peer
8888

89-
async def call(
89+
async def send_request(
9090
self,
9191
method: str,
9292
params: Mapping[str, Any] | None,
9393
opts: CallOptions | None = None,
9494
) -> dict[str, Any]:
9595
if self._peer is None:
9696
raise RuntimeError("DirectDispatcher has no peer; use create_direct_dispatcher_pair()")
97-
return await self._peer._dispatch_call(method, params, opts)
97+
return await self._peer._dispatch_request(method, params, opts)
9898

9999
async def notify(self, method: str, params: Mapping[str, Any] | None) -> None:
100100
if self._peer is None:
101101
raise RuntimeError("DirectDispatcher has no peer; use create_direct_dispatcher_pair()")
102102
await self._peer._dispatch_notify(method, params)
103103

104-
async def run(self, on_call: OnCall, on_notify: OnNotify) -> None:
105-
self._on_call = on_call
104+
async def run(self, on_request: OnRequest, on_notify: OnNotify) -> None:
105+
self._on_request = on_request
106106
self._on_notify = on_notify
107107
self._ready.set()
108108
await self._closed.wait()
@@ -115,25 +115,25 @@ def _make_context(self, on_progress: ProgressFnT | None = None) -> _DirectDispat
115115
peer = self._peer
116116
return _DirectDispatchContext(
117117
transport=self._transport_ctx,
118-
_back_call=lambda m, p, o: peer._dispatch_call(m, p, o),
118+
_back_request=lambda m, p, o: peer._dispatch_request(m, p, o),
119119
_back_notify=lambda m, p: peer._dispatch_notify(m, p),
120120
_on_progress=on_progress,
121121
)
122122

123-
async def _dispatch_call(
123+
async def _dispatch_request(
124124
self,
125125
method: str,
126126
params: Mapping[str, Any] | None,
127127
opts: CallOptions | None,
128128
) -> dict[str, Any]:
129129
await self._ready.wait()
130-
assert self._on_call is not None
130+
assert self._on_request is not None
131131
opts = opts or {}
132132
dctx = self._make_context(on_progress=opts.get("on_progress"))
133133
try:
134134
with anyio.fail_after(opts.get("timeout")):
135135
try:
136-
return await self._on_call(dctx, method, params)
136+
return await self._on_request(dctx, method, params)
137137
except MCPError:
138138
raise
139139
except Exception as e:

src/mcp/shared/dispatcher.py

Lines changed: 39 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
33
A Dispatcher turns a duplex message channel into two things:
44
5-
* an outbound API: ``call(method, params)`` and ``notify(method, params)``
6-
* an inbound pump: ``run(on_call, on_notify)`` that drives the receive loop and
7-
invokes the supplied handlers for each incoming request/notification
5+
* an outbound API: ``send_request(method, params)`` and ``notify(method, params)``
6+
* an inbound pump: ``run(on_request, on_notify)`` that drives the receive loop
7+
and invokes the supplied handlers for each incoming request/notification
88
99
It is deliberately *not* MCP-aware. Method names are strings, params and
1010
results are ``dict[str, Any]``. The MCP type layer (request/result models,
@@ -28,23 +28,23 @@
2828
"DispatchContext",
2929
"DispatchMiddleware",
3030
"Dispatcher",
31-
"OnCall",
3231
"OnNotify",
32+
"OnRequest",
33+
"Outbound",
3334
"ProgressFnT",
34-
"RequestSender",
3535
]
3636

3737
TransportT_co = TypeVar("TransportT_co", bound=TransportContext, covariant=True)
3838

3939

4040
class ProgressFnT(Protocol):
41-
"""Callback invoked when a progress notification arrives for a pending call."""
41+
"""Callback invoked when a progress notification arrives for a pending request."""
4242

4343
async def __call__(self, progress: float, total: float | None, message: str | None) -> None: ...
4444

4545

4646
class CallOptions(TypedDict, total=False):
47-
"""Per-call options for `RequestSender.send_request` / `Dispatcher.call`.
47+
"""Per-call options for `Outbound.send_request`.
4848
4949
All keys are optional. Dispatchers ignore keys they do not understand.
5050
"""
@@ -53,37 +53,51 @@ class CallOptions(TypedDict, total=False):
5353
"""Seconds to wait for a result before raising and sending ``notifications/cancelled``."""
5454

5555
on_progress: ProgressFnT
56-
"""Receive ``notifications/progress`` updates for this call."""
56+
"""Receive ``notifications/progress`` updates for this request."""
5757

5858
resumption_token: str
59-
"""Opaque token to resume a previously interrupted call (transport-dependent)."""
59+
"""Opaque token to resume a previously interrupted request (transport-dependent)."""
6060

6161
on_resumption_token: Callable[[str], Awaitable[None]]
6262
"""Receive a resumption token when the transport issues one."""
6363

6464

6565
@runtime_checkable
66-
class RequestSender(Protocol):
67-
"""Anything that can send a request and await its result.
66+
class Outbound(Protocol):
67+
"""Anything that can send requests and notifications to the peer.
6868
69-
`DispatchContext` satisfies this; `PeerMixin` (and `Connection`/`Peer`) wrap
70-
a `RequestSender` to provide typed request methods.
69+
Both `Dispatcher` (top-level outbound) and `DispatchContext` (back-channel
70+
during an inbound request) extend this. `PeerMixin` wraps an `Outbound` to
71+
provide typed MCP request/notification methods.
7172
"""
7273

7374
async def send_request(
7475
self,
7576
method: str,
7677
params: Mapping[str, Any] | None,
7778
opts: CallOptions | None = None,
78-
) -> dict[str, Any]: ...
79+
) -> dict[str, Any]:
80+
"""Send a request and await its result.
81+
82+
Raises:
83+
MCPError: If the peer responded with an error, or the handler
84+
raised. Implementations normalize all handler exceptions to
85+
`MCPError` so callers see a single exception type.
86+
"""
87+
...
7988

89+
async def notify(self, method: str, params: Mapping[str, Any] | None) -> None:
90+
"""Send a fire-and-forget notification."""
91+
...
8092

81-
class DispatchContext(Protocol[TransportT_co]):
82-
"""Per-request context handed to ``on_call`` / ``on_notify``.
93+
94+
class DispatchContext(Outbound, Protocol[TransportT_co]):
95+
"""Per-request context handed to ``on_request`` / ``on_notify``.
8396
8497
Carries the transport metadata for the inbound message and provides the
8598
back-channel for sending requests/notifications to the peer while handling
86-
it.
99+
it. `send_request` raises `NoBackChannelError` if
100+
``transport.can_send_request`` is ``False``.
87101
"""
88102

89103
@property
@@ -96,23 +110,6 @@ def cancel_requested(self) -> anyio.Event:
96110
"""Set when the peer sends ``notifications/cancelled`` for this request."""
97111
...
98112

99-
async def notify(self, method: str, params: Mapping[str, Any] | None) -> None:
100-
"""Send a notification to the peer."""
101-
...
102-
103-
async def send_request(
104-
self,
105-
method: str,
106-
params: Mapping[str, Any] | None,
107-
opts: CallOptions | None = None,
108-
) -> dict[str, Any]:
109-
"""Send a request to the peer on the back-channel and await its result.
110-
111-
Raises:
112-
NoBackChannelError: if ``transport.can_send_request`` is ``False``.
113-
"""
114-
...
115-
116113
async def progress(self, progress: float, total: float | None = None, message: str | None = None) -> None:
117114
"""Report progress for the inbound request, if the peer supplied a progress token.
118115
@@ -121,47 +118,28 @@ async def progress(self, progress: float, total: float | None = None, message: s
121118
...
122119

123120

124-
OnCall = Callable[[DispatchContext[TransportContext], str, Mapping[str, Any] | None], Awaitable[dict[str, Any]]]
121+
OnRequest = Callable[[DispatchContext[TransportContext], str, Mapping[str, Any] | None], Awaitable[dict[str, Any]]]
125122
"""Handler for inbound requests: ``(ctx, method, params) -> result``. Raise ``MCPError`` to send an error response."""
126123

127124
OnNotify = Callable[[DispatchContext[TransportContext], str, Mapping[str, Any] | None], Awaitable[None]]
128125
"""Handler for inbound notifications: ``(ctx, method, params)``."""
129126

130-
DispatchMiddleware = Callable[[OnCall], OnCall]
131-
"""Wraps an ``OnCall`` to produce another ``OnCall``. Applied outermost-first."""
127+
DispatchMiddleware = Callable[[OnRequest], OnRequest]
128+
"""Wraps an ``OnRequest`` to produce another ``OnRequest``. Applied outermost-first."""
132129

133130

134-
class Dispatcher(Protocol[TransportT_co]):
131+
class Dispatcher(Outbound, Protocol[TransportT_co]):
135132
"""A duplex request/notification channel with call-return semantics.
136133
137-
Implementations own correlation of outbound calls to inbound results, the
134+
Implementations own correlation of outbound requests to inbound results, the
138135
receive loop, per-request concurrency, and cancellation/progress wiring.
139136
"""
140137

141-
async def call(
142-
self,
143-
method: str,
144-
params: Mapping[str, Any] | None,
145-
opts: CallOptions | None = None,
146-
) -> dict[str, Any]:
147-
"""Send a request and await its result.
148-
149-
Raises:
150-
MCPError: If the peer responded with an error, or the handler
151-
raised. Implementations normalize all handler exceptions to
152-
`MCPError` so callers see a single exception type.
153-
"""
154-
...
155-
156-
async def notify(self, method: str, params: Mapping[str, Any] | None) -> None:
157-
"""Send a fire-and-forget notification."""
158-
...
159-
160-
async def run(self, on_call: OnCall, on_notify: OnNotify) -> None:
138+
async def run(self, on_request: OnRequest, on_notify: OnNotify) -> None:
161139
"""Drive the receive loop until the underlying channel closes.
162140
163-
Each inbound request is dispatched to ``on_call`` in its own task; the
164-
returned dict (or raised ``MCPError``) is sent back as the response.
141+
Each inbound request is dispatched to ``on_request`` in its own task;
142+
the returned dict (or raised ``MCPError``) is sent back as the response.
165143
Inbound notifications go to ``on_notify``.
166144
"""
167145
...

0 commit comments

Comments
 (0)