[v2] Server registry + per-connection state; ServerRunner consumes Server[L] directly#2562
Draft
maxisbey wants to merge 3 commits intomaxisbey/v2-server-runnerfrom
Draft
[v2] Server registry + per-connection state; ServerRunner consumes Server[L] directly#2562maxisbey wants to merge 3 commits intomaxisbey/v2-server-runnerfrom
maxisbey wants to merge 3 commits intomaxisbey/v2-server-runnerfrom
Conversation
…er[L] directly Server is generic in LifespanResultT only — no TransportContextT. Spike (scratch/spike-tt-on-server) found a third generic breaks bare-Server plumbing helpers via invariance and only buys one None-check; it remains additive later via PEP 696 default if demand materialises. TT stays on the transport layer (Dispatcher/DispatchContext/BaseContext in mcp.shared); the server layer (Server/Context/ServerRunner/ServerMiddleware) consumes base TransportContext. - HandlerEntry[L] frozen dataclass (params_type, handler) replaces bare callables in the registry; params type erased to Any in storage, correlated at add_request_handler[P] - Public add_request_handler/add_notification_handler; capabilities() zero-arg (notification_options/experimental_capabilities now ctor kwargs) - ServerRunner drops the ServerRegistry Protocol scaffold and reads Server[L] directly; _make_context no longer narrows dctx - ServerMiddleware[L] (one contravariant param) - Context[L] (BaseContext[TransportContext] fixed)
…tContext.headers Per-connection state without a connection_lifespan CM or a second Server generic. Stateless is the default deployment, where a per-connection lifespan would wrap a single request; the enter-late mechanics it would need (race init vs dispatcher-done, ready-gate) were more machinery than the use case warrants. - Connection.session_id: str | None — set by the mount via ServerRunner(session_id=...); per-connection, not per-message - Connection.state: dict[str, Any] — scratch that persists across requests; handlers/middleware read and write freely - Connection.exit_stack: AsyncExitStack — handlers/middleware push CMs or callbacks for per-connection teardown; ServerRunner.run() unwinds it (shielded) in a finally after dispatcher.run() returns - TransportContext.headers: Mapping[str, str] | None on the base — populated by HTTP transports, None on stdio - Context.session_id / Context.headers convenience properties - create_direct_dispatcher_pair(headers=...) and connected_runner(session_id=..., headers=...) for tests
…r correlation Matches BaseSession._normalize_request_id and the TypeScript SDK: a peer that echoes the request ID as a JSON string still resolves the waiter. Applied at both lookup sites (_resolve_pending and the progress-token match). Parity prep for the PR6 e2e suite.
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.
PR5 of the V2 server-side refactor (
Transport → Dispatcher → ServerRunner → Serverregistry). Stacked on #PR4 (maxisbey/v2-server-runner).Status: WIP — chunks A+B landed, chunk C (additive new-path entry points) in progress. Opening as draft so the registry/typing changes are reviewable while the rest lands.
Motivation and Context
ServerRunner(PR4) needs to consume the lowlevelServeras a typed handler registry rather than via the old_handle_requestcallback path. This PR reshapesServerso the runner can read(params_type, handler)entries directly, and adds the per-connection scaffolding (state,exit_stack,session_id,headers) that the newContextexposes.The
Servergenerics were settled atServer[LifespanResultT]only — no transport generic, no connection-state generic. A transport generic makesServerinvariant in a way that breaks baredef deploy(server: Server)plumbing helpers; the connection-state generic doesn't earn its place when stateless is the default deployment. Both remain additive later via PEP 696 trailing defaults if demand appears. The transport-layer types (Dispatcher[TT_co],DispatchContext[TT],BaseContext[TT]) keep their generic; the server layer consumes baseTransportContext.What's in here so far
HandlerEntry[L]frozen dataclass replaces bare callables in the registry; publicadd_request_handler/add_notification_handler; zero-argcapabilities()(notification_options/experimental_capabilitiesare now ctor kwargs)ServerRunnerreadsServer[L]directly — the PR4ServerRegistryProtocol scaffold is goneConnection.session_id,Connection.state: dict[str, Any],Connection.exit_stack: AsyncExitStack(unwound shielded byServerRunner.run()after the dispatcher closes — handlers/middleware register per-connection teardown with stdlibenter_async_context/push_async_callback)TransportContext.headers: Mapping[str, str] | Noneon the base;ctx.session_id/ctx.headersconvenience properties onContextJSONRPCDispatchercoerces string response/progress IDs to int for correlation (matchesBaseSessionand the TS SDK)Still to come in this PR
Additive only — nothing in the existing
Server.run()/_handle_*/ServerSessionpath is touched.OutboundMiddlewareonJSONRPCDispatcher+otel_outbound_middleware(span + W3C_metainject onsend_raw_request, mirroringBaseSession.send_request)stdio.serve(server)+StdioTransportContext— new-path stdio entry for lowlevel usersContext(.lifespan_context/.sessionproxy) so the high-levelmcpserver.Contextworks on either backend — temporary parity scaffoldingMCPServerNextsibling class routing throughServerRunnerfor the parametrized e2e parity suite (next PR)How Has This Been Tested?
tests/server/test_runner.pyend-to-end overDirectDispatcher; new tests forstatepersistence,exit_stackunwound on close/error,session_id/headersround-trip.tests/shared/test_jsonrpc_dispatcher.pyfor the ID coercion. 100% coverage on touched files.Breaking Changes
None yet. This PR is additive; the old runtime path is untouched. The eventual breaking changes (delete
_handle_*/ServerSession/BaseSession) are deferred until the parity suite proves equivalence.Types of changes
Checklist
AI Disclaimer