Resolve protocol version per request and expose it as ctx.protocol_version#2886
Draft
maxisbey wants to merge 4 commits into
Draft
Resolve protocol version per request and expose it as ctx.protocol_version#2886maxisbey wants to merge 4 commits into
maxisbey wants to merge 4 commits into
Conversation
…rsion The runner's version-keyed surface validation (added in #2849) needs a version on every request, but Connection.protocol_version is set only by the initialize handshake and so stays None on stateless streamable-HTTP connections. The transport already reads and validates the MCP-Protocol-Version header per request but only uses it for SSE priming/close-callback gating. This threads that value through to the runner via a new optional ServerMessageMetadata.protocol_version field and replaces the hard-coded '2025-11-25' fallback in _on_request/_on_notify with a single _resolve_protocol_version() helper: a handshake-committed value governs the whole connection when present; otherwise per-request signals apply (_meta then the transport hint), then the literal 2025-11-25 terminal default. The result is also exposed to handlers as ServerRequestContext.protocol_version (always set). Connection.protocol_version and ServerSession.protocol_version keep their meaning (handshake result, None if no handshake); their docstrings now point at ctx.protocol_version for the per-request value.
LATEST_PROTOCOL_VERSION as a default would have been wrong once it bumps to a modern-era revision; the field is always set by the runner so forcing direct constructors to supply it is the honest contract.
… a migration step)
The match-on-params form tripped a 3.10-only branch-coverage quirk on the case-body fall-through arc. Reading the already-extracted RequestParamsMeta avoids the match, drops the per-message dict copy, and keeps a single _meta extraction per request. Docstrings/comments added in this PR trimmed to one-liners or dropped where the test/function name already says it.
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.
Adds
ctx.protocol_version: str— the protocol version a request is being served at — resolved per request and always set, including on stateless connections.Motivation and Context
Since #2849, the runner's inbound surface validation (
validate_client_request/validate_client_notification) and outbound result serialization are version-keyed, and they need a version on every request. Today the only source isConnection.protocol_version, which is set by theinitializehandshake — so on stateless streamable-HTTP connections it staysNonefor the connection's whole life and the runner falls back to a hard-coded"2025-11-25". The streamable HTTP transport already reads and validates theMCP-Protocol-Versionheader per request, but only uses it for transport-local SSE-priming/close-callback gating and then drops it.This threads that value through to the runner and exposes it to handlers as
ctx.protocol_version.Resolution rule. A handshake-committed value governs the whole connection when present; otherwise per-request signals apply (
_meta["io.modelcontextprotocol/protocolVersion"], then the transport's per-message hint), then the literal2025-11-25terminal default. This mirrors the spec's own framing in draft Versioning § Terminology: handshake-based (≤2025-11-25) versions are a connection fact; per-request-metadata (2026-07-28+) versions are a request fact. The resolver doesn't branch on a mode flag —connection.protocol_version is not Noneis "this connection ran a handshake."Connection.protocol_versionandServerSession.protocol_versionkeep their existing meaning ("the version negotiated byinitialize,Noneif no handshake ran"); their docstrings now point atctx.protocol_versionfor the per-request value.How Has This Been Tested?
_resolve_protocol_versioncovering negotiated-wins,_meta, transport-hint, unsupported-value fall-through, and the terminal default.connected_runnertests forctx.protocol_versionon stateful (negotiated) and stateless in-memory (terminal default, withctx.session.protocol_versionstillNone).ctx.protocol_versionmatching the request'sMCP-Protocol-Versionheader (parametrized over two values), and the spec's2025-03-26default when the header is absent.Breaking Changes
ServerRequestContextgains a requiredprotocol_version: strfield. The runner always sets it; the only direct constructors were two tests, updated here.ServerMessageMetadatagains an optionalprotocol_versionfield (defaults toNone).Types of changes
Checklist
Additional context
SUPPORTED_PROTOCOL_VERSIONSare skipped by the resolver so an unrecognized declaration falls through rather than poisoning surface validation. ExplicitUnsupportedProtocolVersionErrorrejection for_meta(and the header/_metaMUST-match check) belong in the transport and arrive with the 2026-07-28 negotiation work.or "2025-11-25"fallbacks on the outbound server-to-client paths (ServerSession.send_request,Connection.send_request) are unchanged; those are connection-scoped sends without access to per-request context, and on stateless connections they fail fast withNoBackChannelErroranyway.