feat: PeerMixin, BaseContext, Connection, server Context#2460
Draft
maxisbey wants to merge 5 commits intomaxisbey/v2-jsonrpc-dispatcherfrom
Draft
feat: PeerMixin, BaseContext, Connection, server Context#2460maxisbey wants to merge 5 commits intomaxisbey/v2-jsonrpc-dispatcherfrom
maxisbey wants to merge 5 commits intomaxisbey/v2-jsonrpc-dispatcherfrom
Conversation
PeerMixin defines the typed server-to-client request methods (sample with overloads, elicit_form, elicit_url, list_roots, ping) once. Each method constrains `self: Outbound` so any class with send_request/notify can mix it in — pyright checks the host structurally at the call site. The mixin does no capability gating; that's the host's send_request's job. Peer is a trivial standalone wrapper for when you have a bare Outbound (e.g. a dispatcher) and want the typed sugar without writing your own host class. 6 tests over DirectDispatcher, 0.03s.
Composition over a DispatchContext: forwards transport/cancel_requested/ send_request/notify/progress and adds meta. Satisfies Outbound so PeerMixin works on it (proven by Peer(bctx).ping() round-tripping). The server Context (next commit) extends this with lifespan/connection; ClientContext will be an alias once ClientSession is reworked.
…ntext PeerMixin methods and Peer/BaseContext now call/expose send_raw_request. The typed send_request lands on Connection/Context in the next commit.
TypedServerRequestMixin (server/_typed_request.py) provides shape-2 typed send_request: per-spec overloads (CreateMessage/Elicit/ListRoots/Ping) infer the result type; custom requests pass result_type explicitly. Mixed into both Connection and the server Context. Connection (server/connection.py) wraps an Outbound for the standalone stream. notify is best-effort (never raises); send_raw_request gated on has_standalone_channel; check_capability mirrors v1 for now (FOLLOWUP). Holds peer info populated at initialize time and the per-connection lifespan state. Context (server/context.py, alongside v1's ServerRequestContext) composes BaseContext + PeerMixin + TypedServerRequestMixin and adds lifespan/connection. Request-scoped log() rides the request's back-channel; ctx.connection.log() uses the standalone stream. dump_params(model, meta) merges user-supplied meta into _meta; threaded through every PeerMixin and Connection convenience method. 31 tests, 0.06s.
- Connection.check_capability per-field branches (parametrized) - Context.log with logger and meta supplied - Peer.notify forwards to wrapped Outbound
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.
Stacked on #2458. The MCP-type layer that sits above the dispatcher: typed request sugar, the per-request
Context, and the per-connectionConnection.Motivation and Context
Outbound(from #2452) is the rawsend_raw_request(method, params) -> dictchannel. This PR builds the typed surface on top:PeerMixin(shared/peer.py) — kwarg-style typed methods (sample,elicit_form,elicit_url,list_roots,ping) defined once. Each constrainsself: Outboundso any class withsend_raw_request/notifygets them.Peeris a standalone wrapper for bare dispatchers.BaseContext[TT](shared/context.py) — composition over aDispatchContext. Forwardstransport/cancel_requested/send_raw_request/notify/report_progress; addsmeta. SatisfiesOutbound.TypedServerRequestMixin(server/_typed_request.py) — typedsend_request(req) -> Resultwith per-spec overloads (CreateMessageRequest/ElicitRequest/ListRootsRequest/PingRequest) plus aresult_type=fallback for custom requests.Connection(server/connection.py) — per-client state and the standalone-streamOutbound. Best-effortnotify(never raises);send_raw_requestgated onhas_standalone_channel; convenience notifications (log,send_*_list_changed,send_resource_updated). Mixes inTypedServerRequestMixin.Context[LifespanT, TT](server/context.py, alongside v1'sServerRequestContext) —BaseContext+PeerMixin+TypedServerRequestMixin+lifespan/connection. WhatServerRunner(next PR) hands to user handlers.dump_params(model, meta)merges user-suppliedmetainto_meta; threaded through every convenience method.How Has This Been Tested?
tests/shared/test_peer.py(8),tests/shared/test_context.py(5),tests/server/test_connection.py(14),tests/server/test_server_context.py(4) — all overDirectDispatcher. 31 tests, 0.06s.Breaking Changes
None. New code only; v1's
ServerRequestContext/ClientRequestContextare untouched.Types of changes
Checklist
Additional context
Stack:
The typed
send_requestis shape 2 (per-spec overloads) for now. AHasResult[R]protocol (__mcp_result__ClassVar on each Request type, one generic signature, O(1) maintenance) is the cleaner long-term shape and would also handle MRTR/Tasks via discriminated unions — tracked as a follow-up onceClient(PR7) lands and the overload duplication becomes visible.AI Disclaimer