feat: add Dispatcher Protocol and DirectDispatcher#2452
Draft
feat: add Dispatcher Protocol and DirectDispatcher#2452
Conversation
Introduces the Dispatcher abstraction that decouples MCP request/response handling from JSON-RPC framing. A Dispatcher exposes call/notify for outbound messages and run(on_call, on_notify) for inbound dispatch, with no knowledge of MCP types or wire encoding. - shared/dispatcher.py: Dispatcher, DispatchContext, RequestSender Protocols; CallOptions, OnCall/OnNotify, ProgressFnT, DispatchMiddleware - shared/transport_context.py: TransportContext base dataclass - shared/direct_dispatcher.py: in-memory Dispatcher impl that wires two peers with no transport; serves as a fast test substrate and second-impl proof - shared/exceptions.py: NoBackChannelError(MCPError) for transports without a server-to-client request channel - types: REQUEST_CANCELLED SDK error code The JSON-RPC implementation and ServerRunner that consume this Protocol land in follow-up PRs.
- tests: replace unreachable 'return {}' with 'raise NotImplementedError'
(already in coverage exclude_also) and collapse send_request+return into
one statement
- dispatcher: RequestSender docstring no longer claims Dispatcher satisfies it
(Dispatcher exposes call(), not send_request())
…er 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])
maxisbey
commented
Apr 16, 2026
| server.close() | ||
|
|
||
|
|
||
| @pytest.mark.anyio |
Contributor
Author
There was a problem hiding this comment.
mark this once at the top of the file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First in a stack of PRs reworking the SDK internals to decouple MCP request handling from JSON-RPC framing. This PR lands only the abstraction and an in-memory implementation; nothing is wired into
BaseSession/Serveryet.Motivation and Context
The current
BaseSessionhard-couples three concerns: JSON-RPC framing, request/response correlation, and MCP semantics. That makes alternative transports (gRPC, in-process) impossible without smuggling JSON through them, and makes the receive loop hard to follow and test.The
DispatcherProtocol is the call/return boundary:send_request(method, params) -> dict,notify(method, params), andrun(on_request, on_notify)to drive the inbound side. Method names are strings, params/results are dicts. MCP types live above it; wire encoding lives below it.Outboundis a two-method base Protocol (send_request+notify) that bothDispatcherandDispatchContextextend. It names the surface thatPeerMixin(a later PR) will wrap to provide typed MCP methods — one Protocol covers both top-level outbound and the per-request back-channel, with no aliases or adapter classes.DirectDispatcheris an in-memory implementation that wires two peers with no transport. It serves as the second-impl proof for the Protocol and as a fast test substrate for the layers that will sit above the dispatcher in later PRs.Files
shared/dispatcher.py—Outbound,Dispatcher,DispatchContextProtocols;CallOptions,OnRequest/OnNotify,ProgressFnT,DispatchMiddlewareshared/transport_context.py—TransportContextbase dataclassshared/direct_dispatcher.py— in-memoryDispatcherimplshared/exceptions.py—NoBackChannelError(MCPError)for transports without a server-to-client request channeltypes/—REQUEST_CANCELLEDSDK error codeHow Has This Been Tested?
tests/shared/test_dispatcher.py— 13 behavioral tests covering thesend_requestcontract (round-trip, MCPError passthrough, exception normalization, timeout),notify, theDispatchContextback-channel (send_request,notify,progress,NoBackChannelError), and therun/closelifecycle. 100% coverage on the new modules.Breaking Changes
None. New code only; nothing existing is touched beyond adding
NoBackChannelErrorand theREQUEST_CANCELLEDconstant.Types of changes
Checklist
Additional context
Stack:
send_requestcontract: returnsdict[str, Any]or raisesMCPError. Implementations normalize all handler exceptions toMCPErrorso callers see one exception type. Timeout maps toMCPError(REQUEST_TIMEOUT).AI Disclaimer