Skip to content

Fix unknown-method error code and add a protocol version registry#2836

Merged
maxisbey merged 9 commits into
mainfrom
maxisbey/method-not-found-and-version-registry
Jun 11, 2026
Merged

Fix unknown-method error code and add a protocol version registry#2836
maxisbey merged 9 commits into
mainfrom
maxisbey/method-not-found-and-version-registry

Conversation

@maxisbey

@maxisbey maxisbey commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

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 -32602 instead 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 -32601 for 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-supplied MCP-Protocol-Version header. 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 through mcp.shared.version: an ordered registry of released protocol versions plus is_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.md correction: it claimed extra fields on MCP types raise a ValidationError; 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 main with the old behavior (-32602 with 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

  • 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

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 -32601 fix is prepared separately.

AI Disclaimer

maxisbey added 6 commits June 11, 2026 13:44
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.
@maxisbey maxisbey marked this pull request as ready for review June 11, 2026 14:44
maxisbey added 3 commits June 11, 2026 14:57
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.
@maxisbey maxisbey requested a review from felixweinberger June 11, 2026 15:20
Comment thread src/mcp/shared/version.py
Comment on lines +14 to +19
KNOWN_PROTOCOL_VERSIONS: Final[tuple[str, ...]] = (
"2024-11-05",
"2025-03-26",
"2025-06-18",
"2025-11-25",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not adding 2026-07-28 as part of this yet?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep not yet

Comment thread src/mcp/shared/version.py
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@maxisbey maxisbey merged commit 7267818 into main Jun 11, 2026
36 checks passed
@maxisbey maxisbey deleted the maxisbey/method-not-found-and-version-registry branch June 11, 2026 15:47
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.

Invalid method names return error code -32602 instead of -32601

2 participants