feat: ServerRunner — per-connection orchestrator#2491
Draft
maxisbey wants to merge 3 commits intomaxisbey/v2-peer-connection-contextfrom
Draft
feat: ServerRunner — per-connection orchestrator#2491maxisbey wants to merge 3 commits intomaxisbey/v2-peer-connection-contextfrom
maxisbey wants to merge 3 commits intomaxisbey/v2-peer-connection-contextfrom
Conversation
ServerRunner is the per-connection orchestrator over a Dispatcher. This commit lands the skeleton: ServerRegistry Protocol, _on_request (lookup → validate → build Context → call handler → dump), _handle_initialize (populates Connection, opens the init-gate), and a basic _on_notify. Additive methods on lowlevel Server (get_request_handler / get_notification_handler / middleware / connection_lifespan) so it satisfies ServerRegistry without touching the existing run() path. _PARAMS_FOR_METHOD is scaffolding (marked TODO) until the registry stores params types directly. 5 tests over DirectDispatcher + a real lowlevel Server.
ContextMiddleware is a Protocol[L] (contravariant) so Server[L].middleware: list[ContextMiddleware[L]] is properly typed. App-specific middleware sees ctx.lifespan: L; reusable middleware typed ContextMiddleware[object] registers on any Server via contravariance. Context's covariance (previous PR3 commit) makes Context[L, ST] <: Context[L, TransportContext] so the chain composes without casts. dispatch_middleware (DispatchMiddleware list on ServerRunner) wraps the raw _on_request and sees everything including initialize/METHOD_NOT_FOUND. server.middleware (ContextMiddleware) runs inside _on_request after validation/ctx-build and wraps registered handlers only. _on_notify routes notifications/initialized (sets the flag), drops before-init and unknown methods, otherwise builds Context and calls the registered handler. 11 tests over DirectDispatcher + a real lowlevel Server.
run() composes dispatch_middleware over _on_request and forwards task_status to dispatcher.run() so callers can 'await tg.start(runner.run)'. otel_middleware is a DispatchMiddleware that wraps each request in a span, mirroring the existing Server._handle_request span shape: name 'MCP handle <method> [<target>]', mcp.method.name attribute, W3C trace context extracted from params._meta (SEP-414), and ERROR status if the handler raises. connection_lifespan plumbing (the enter-late dance) is deferred to a separate commit since Server.connection_lifespan is None today.
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.
Design doc: https://gist.github.com/maxisbey/1e14e741d774acf52b80e69db292c5d7
Stacked on #2460.
ServerRunneris the per-connection orchestrator that bridges the dispatcher layer (raw(dctx, method, params) -> dict) and the user's handler layer (typedContext, validated params).Motivation and Context
One
ServerRunnerper client connection. It handles theinitializehandshake (populatesConnection), gates requests until initialized (pingexempt), looks up the handler in the server's registry, validates params, buildsContext, runs the middleware chain, and returns the result dict.run()composesdispatch_middlewareand drivesdispatcher.run().Consumes any
ServerRegistryProtocol — the lowlevelServersatisfies it via four additive methods (get_request_handler,get_notification_handler,.middleware,.connection_lifespan) so the existingServer.run()path is unaffected. Nothing is wired in until PR6 swapsServer.run()over.Two-tier middleware:
dispatch_middleware: list[DispatchMiddleware]onServerRunner— wraps raw_on_request, sees everything includinginitialize/METHOD_NOT_FOUND. Includesotel_middleware(mirrors the existing span shape).Server.middleware: list[ContextMiddleware[L]]— runs inside_on_requestafter validate/ctx-build; wraps registered handlers only.ContextMiddlewareis a contravariantProtocol[L]soServer[L].middlewareis properly typed: app-specific middleware seesctx.lifespan: L; reusableContextMiddleware[object]registers on anyServervia contravariance.Contextis covariant in both params (#2460's last commit) soContext[L, ST] <: Context[object, TransportContext]and the chain composes without casts.How Has This Been Tested?
tests/server/test_runner.py— 14 tests overDirectDispatcher+ a real lowlevelServer. Initialize handshake, init-gate, METHOD_NOT_FOUND, stateless,_on_notify, both middleware tiers + ordering,run()end-to-end,otel_middlewarepass-through. 92% coverage onrunner.py; remaining gaps (otel target/_meta branches,_dump_resultBaseModel/TypeError) to follow.Breaking Changes
None.
Servergains additive attrs only; the existingrun()path is untouched.Types of changes
Checklist
Additional context
Stack:
Server.run()→ServerRunnerScaffolding (marked
TODO, cleaned up in PR6):_PARAMS_FOR_METHODstatic lookup until the registry stores params types; placeholder version negotiation in_handle_initialize.connection_lifespanenter-late plumbing is deferred —Server.connection_lifespanisNonetoday and the dance (TG +dispatcher_doneevent raced withconnection.initialized) lands when PR6 adds the registration API.AI Disclaimer