Skip to content

feat: PeerMixin, BaseContext, Connection, server Context#2460

Draft
maxisbey wants to merge 5 commits intomaxisbey/v2-jsonrpc-dispatcherfrom
maxisbey/v2-peer-connection-context
Draft

feat: PeerMixin, BaseContext, Connection, server Context#2460
maxisbey wants to merge 5 commits intomaxisbey/v2-jsonrpc-dispatcherfrom
maxisbey/v2-peer-connection-context

Conversation

@maxisbey
Copy link
Copy Markdown
Contributor

Stacked on #2458. The MCP-type layer that sits above the dispatcher: typed request sugar, the per-request Context, and the per-connection Connection.

Motivation and Context

Outbound (from #2452) is the raw send_raw_request(method, params) -> dict channel. 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 constrains self: Outbound so any class with send_raw_request/notify gets them. Peer is a standalone wrapper for bare dispatchers.
  • BaseContext[TT] (shared/context.py) — composition over a DispatchContext. Forwards transport/cancel_requested/send_raw_request/notify/report_progress; adds meta. Satisfies Outbound.
  • TypedServerRequestMixin (server/_typed_request.py) — typed send_request(req) -> Result with per-spec overloads (CreateMessageRequest/ElicitRequest/ListRootsRequest/PingRequest) plus a result_type= fallback for custom requests.
  • Connection (server/connection.py) — per-client state and the standalone-stream Outbound. Best-effort notify (never raises); send_raw_request gated on has_standalone_channel; convenience notifications (log, send_*_list_changed, send_resource_updated). Mixes in TypedServerRequestMixin.
  • Context[LifespanT, TT] (server/context.py, alongside v1's ServerRequestContext) — BaseContext + PeerMixin + TypedServerRequestMixin + lifespan/connection. What ServerRunner (next PR) hands to user handlers.

dump_params(model, meta) merges user-supplied meta into _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 over DirectDispatcher. 31 tests, 0.06s.

Breaking Changes

None. New code only; v1's ServerRequestContext/ClientRequestContext are untouched.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Stack:

  1. feat: add Dispatcher Protocol and DirectDispatcher #2452 — Dispatcher Protocol + DirectDispatcher
  2. feat: JSONRPCDispatcher #2458 — JSONRPCDispatcher
  3. (this PR) PeerMixin / BaseContext / Connection / server Context
  4. ServerRunner

The typed send_request is shape 2 (per-spec overloads) for now. A HasResult[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 once Client (PR7) lands and the overload duplication becomes visible.

AI Disclaimer

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant