Enforce the modern lifecycle admission rules per SEP-2575 - #489
Conversation
6e41c2d to
264cafd
Compare
264cafd to
d34fb74
Compare
|
Updated per the discussion in #491, now that it has merged: rebased onto main, dropped the duplicated |
|
Ran the rebased branch (d34fb74) against a real dual-era deployment, as offered on #491. Setup: a production Rails MCP server exposing The
|
| request | d34fb74 |
|---|---|
| full modern envelope | 200 |
envelope without clientInfo |
200 |
claim key only, clientCapabilities missing |
400 -32602, offending key named in message and data |
| unsupported version in envelope | 400 -32022 + data.supported |
server/discover |
200 |
Two observations, neither blocking
1. The -32022 era-lock defense is unreachable under stateless: true. After a modern-envelope tools/call, a following initialize is served with 200 — no lock survives between requests. That is expected given how we deploy rather than a defect, but if sessionless SEP-2575 servers are a supported shape, the initialize-on-modern-locked defense is a no-op there and dual-era detection has to rely on response codes alone.
2. MCP-Protocol-Version: 2026-07-28 alone does not make a request modern. An initialize sent with the modern version header but no _meta is served as a legacy handshake (200, negotiating 2026-07-28). That follows from the claim-key rule you described on #491, so I assume it is deliberate — flagging it only because it means a client can hold a modern version header and a legacy lifecycle at the same time, and the header alone is not a safe signal for which era a peer is speaking.
Happy to re-run any of this if the branch moves.
|
Thanks for running this against a real dual-era deployment; the before/after on the misclassified Both observations are deliberate. Under Likewise, the |
## Motivation and Context The 2026-07-28 conformance requirements reject two admission gaps in the modern (stateless) lifecycle that the current implementation answers with the wrong signal: - The RPCs the modern lifecycle removes (`initialize`, `ping`, `logging/setLevel`, `resources/subscribe`, `resources/unsubscribe`) were still dispatched, or answered incidental errors (`initialize` answered `-32022`, `resources/subscribe` a capability error); the requirement is `-32601` Method not found with HTTP 404. - `server/discover` was fully envelope-exempt, while the requirements validate its envelope like every other modern request. The per-request `_meta` validation itself (`-32602` naming the offending keys, optional `clientInfo`, era classification on the reserved `protocolVersion` key alone) landed separately in modelcontextprotocol#491; this change builds on that shape and completes the admission rules: - `Server#handle_request` rejects `Methods::MODERN_REMOVED_METHODS` on a modern-era session with `-32601` before the capability check, since the method does not exist in that era at all. The Streamable HTTP modern path maps `-32601` to 404 and `-32602` to 400 through its existing status ladder. - The dual-era sniff treats an `initialize` carrying the modern envelope as modern traffic (a modern client naming a removed method) instead of the legacy-distinctive handshake, and `server/discover` keeps its envelope exemption outside the modern era only. The envelope-less rejection on a modern-locked session adopts modelcontextprotocol#491's wire shape, `-32602` with the detailed message. One deliberate divergence from the Python SDK: its dual-era stream loop answers an `initialize` on a modern-locked connection with `-32022` carrying the supported-version list, while its HTTP path serves the 404/-32601 shape. The conformance requirement phrases removed methods transport-independently, so this SDK answers `-32601` on every transport, stdio included, for uniform era semantics. The stdio era-lock tests used a modern `ping` as the era-distinctive request; they now use `tools/list`, since `ping` no longer exists in the modern lifecycle. ## How Has This Been Tested? `bundle exec rake test` passes with zero failures and RuboCop reports no offenses. New and updated tests cover each admission rule on `Server#handle`, the Streamable HTTP modern path, and the stdio era lock (all five removed methods per transport, the envelope requirement on `server/discover`, and the envelope-carrying `initialize` routing). Against the conformance fixture server, every admission check of the `server-stateless` scenario passes at `--spec-version 2026-07-28`: the per-request `_meta` validation, removed-method 404, discovery, capability, and version-negotiation checks are all green, and the only remaining failures are the `subscriptions/listen` checks that belong to separate work. The `--requirements 2025-11-25` server leg passes 78/78, unchanged. ## Breaking Changes None for stable protocol versions: legacy requests are byte-for-byte unchanged, and a bare legacy `_meta` (`progressToken`, trace context) keeps its legacy classification. Within the modern lifecycle, previously incidental error shapes are replaced by the ones the 2026-07-28 conformance requirements mandate.
d34fb74 to
633a5fa
Compare
Motivation and Context
The 2026-07-28 conformance requirements reject two admission gaps in the modern (stateless) lifecycle that the current implementation answers with the wrong signal:
initialize,ping,logging/setLevel,resources/subscribe,resources/unsubscribe) were still dispatched, or answered incidental errors (initializeanswered-32022,resources/subscribea capability error); the requirement is-32601Method not found with HTTP 404.server/discoverwas fully envelope-exempt, while the requirements validate its envelope like every other modern request.The per-request
_metavalidation itself (-32602naming the offending keys, optionalclientInfo, era classification on the reservedprotocolVersionkey alone) landed separately in #491; this change builds on that shape and completes the admission rules:Server#handle_requestrejectsMethods::MODERN_REMOVED_METHODSon a modern-era session with-32601before the capability check, since the method does not exist in that era at all. The Streamable HTTP modern path maps-32601to 404 and-32602to 400 through its existing status ladder.initializecarrying the modern envelope as modern traffic (a modern client naming a removed method) instead of the legacy-distinctive handshake, andserver/discoverkeeps its envelope exemption outside the modern era only. The envelope-less rejection on a modern-locked session adopts Align modern envelope validation with the finalized spec #491's wire shape,-32602with the detailed message.One deliberate divergence from the Python SDK: its dual-era stream loop answers an
initializeon a modern-locked connection with-32022carrying the supported-version list, while its HTTP path serves the 404/-32601 shape. The conformance requirement phrases removed methods transport-independently, so this SDK answers-32601on every transport, stdio included, for uniform era semantics.The stdio era-lock tests used a modern
pingas the era-distinctive request; they now usetools/list, sincepingno longer exists in the modern lifecycle.How Has This Been Tested?
bundle exec rake testpasses with zero failures and RuboCop reports no offenses. New and updated tests cover each admission rule onServer#handle, the Streamable HTTP modern path, and the stdio era lock (all five removed methods per transport, the envelope requirement onserver/discover, and the envelope-carryinginitializerouting).Against the conformance fixture server, every admission check of the
server-statelessscenario passes at--spec-version 2026-07-28: the per-request_metavalidation, removed-method 404, discovery, capability, and version-negotiation checks are all green, and the only remaining failures are thesubscriptions/listenchecks that belong to separate work. The--requirements 2025-11-25server leg passes 78/78, unchanged.Breaking Changes
None for stable protocol versions: legacy requests are byte-for-byte unchanged, and a bare legacy
_meta(progressToken, trace context) keeps its legacy classification. Within the modern lifecycle, previously incidental error shapes are replaced by the ones the 2026-07-28 conformance requirements mandate.Types of changes
Checklist