Fix unknown-method error code and add a protocol version registry#2836
Merged
Conversation
Protocol revisions are an enumerated set, not an ordered scalar: future
identifiers are not guaranteed to be date-shaped, and unrecognized peer
strings must not accidentally compare as newer ("zzz" sorts after every
date). Add KNOWN_PROTOCOL_VERSIONS plus is_version_at_least and
is_stateful_protocol_version helpers, and replace the lexicographic
string comparisons in OAuth resource-param gating and streamable HTTP
priming/close-callback gating. Unknown versions now gate conservatively
(treated as older than every known revision).
A peer request whose method is not in the session's receive union previously failed union validation and was answered with -32602 (INVALID_PARAMS). JSON-RPC 2.0 reserves -32601 (METHOD_NOT_FOUND) for unknown methods, and peers probing for optional features key off that code. Check the method against the union's discriminator literals before validating and answer unknown ones with -32601, carrying the method name in the error data. Unknown notification methods are still dropped without a response.
The runner's pre-initialize gate rejected every non-exempt request with INVALID_PARAMS, including methods the server does not know at all. JSON-RPC 2.0 reserves -32601 for unknown methods regardless of session state, and clients probing a server before the handshake key off that code. Check for unknown methods before the gate; known-but-ungated methods keep the existing rejection. METHOD_NOT_FOUND errors now carry the method name in the error data.
Handlers had no way to read which protocol revision the initialize handshake settled on. Add ServerSession.protocol_version, backed by the connection state: None before initialization and always None on stateless connections, where no handshake reaches the session and the per-request version lives in the MCP-Protocol-Version header.
The migration guide claimed v2 MCP types raise a ValidationError for unknown top-level fields. They are configured with pydantic's default extra behavior, which silently drops unknown fields during validation. Describe the actual behavior (values do not round-trip, no error) and keep pointing users at _meta for custom data.
KNOWN_PROTOCOL_VERSIONS now lists only released revisions (2024-11-05 through 2025-11-25). Drop DRAFT_PROTOCOL_VERSION, STATEFUL_PROTOCOL_VERSIONS, and is_stateful_protocol_version: nothing uses them yet, and with no post-2025-11-25 revision in the registry the stateful classification is vacuous. They can return alongside the code that needs them.
The -32602 -> -32601 change for unknown request methods is wire-visible, so it gets a Bug Fixes entry. Also add the new protocol_version property to the ServerSession surface enumeration.
- _SERVER_REQUEST_METHODS documents the SDK union, not the spec union (the 2025-11-25 tasks methods are deliberately absent) - protocol_version docstrings no longer claim 'always None' in stateless mode: a client that sends initialize anyway still commits the version - the priming-event test asserts the store saw traffic, so the final all() cannot pass vacuously - add missing test docstrings
…e gate A spec-defined method with no registered handler previously got the init gate's INVALID_PARAMS before initialization but METHOD_NOT_FOUND after. -32601 means "not available on this server", so the handler lookup now runs before the gate for every method: probing clients get the same answer in every initialization state, matching the TypeScript SDK. The gate still answers -32602 for methods the server does serve.
Comment on lines
+14
to
+19
| KNOWN_PROTOCOL_VERSIONS: Final[tuple[str, ...]] = ( | ||
| "2024-11-05", | ||
| "2025-03-26", | ||
| "2025-06-18", | ||
| "2025-11-25", | ||
| ) |
Contributor
There was a problem hiding this comment.
not adding 2026-07-28 as part of this yet?
felixweinberger
approved these changes
Jun 11, 2026
| raise ValueError(f"minimum must be a known protocol version, got {minimum!r}") | ||
| if version not in KNOWN_PROTOCOL_VERSIONS: | ||
| return False | ||
| return KNOWN_PROTOCOL_VERSIONS.index(version) >= KNOWN_PROTOCOL_VERSIONS.index(minimum) |
Contributor
There was a problem hiding this comment.
looks correct but feels brittle to tie the logic specifically to a random array constant being in the right order, but seems fine 🤷♂️ we won't change this that often I guess.
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.
Bundled fixes around the same version/dispatch seams: unknown methods now get the JSON-RPC-correct error code on both sides, and protocol version comparisons go through a small registry instead of raw string compares.
Motivation and Context
Unknown methods answered
-32602instead of-32601. A request with an unknown method failed inside the request-union validation and fell into the generic invalid-params handler, so peers received-32602(Invalid params) instead of-32601(Method not found). This affected the server runner for pre-initialization requests and the client-side receive loop. JSON-RPC 2.0 reserves-32601for exactly this case, and clients that probe for optional/newer methods need to distinguish "method doesn't exist" from "bad params" to fall back correctly. Fixes #1561 (its repro — an unknown method sent before any handshake — is the exact pre-initialization path fixed here).Lexicographic version compares on wire input. Three call sites compared protocol versions with raw string ordering (e.g.
>= "2025-11-25"), including against the client-suppliedMCP-Protocol-Versionheader. Any non-date string sorts somewhere (letters sort after digits, so e.g."draft"compares newer than every release) and can silently flip version-gated behavior. Comparisons now go throughmcp.shared.version: an ordered registry of released protocol versions plusis_version_at_least(), which treats unknown wire strings conservatively (no version-gated behavior enabled) and raises on an unknown threshold, since that's a typo in our own code.Also included:
ServerSession.protocol_version: read access to the negotiated version (the field was previously write-only internal state).docs/migration.mdcorrection: it claimed extra fields on MCP types raise aValidationError; they are silently ignored. The doc now describes actual behavior.How Has This Been Tested?
Regression tests for both error-code fixes were written first and verified to fail on
mainwith the old behavior (-32602with empty data) before the fix. Full suite green locally at 100% line+branch coverage; pyright and ruff clean. The registry refactor of the three compare sites is behavior-preserving for all released versions (covered by equivalence tests); only unknown-string inputs change outcome, conservatively.Breaking Changes
No API changes. One wire-visible behavior change: requests with unknown methods now receive
-32601(Method not found) instead of-32602— the JSON-RPC-specified code. Anything matching on the old, incorrect code for this case would need updating.Types of changes
Checklist
Additional context
Commits are stacked for review (registry first, then the two error-code fixes, then the property and doc fix) and are independently revertable. A v1.x backport of the
-32601fix is prepared separately.AI Disclaimer