Reject modern-removed methods before the connection era locks
## Motivation and Context
SEP-2575 removes `initialize`, `ping`, `logging/setLevel`, `resources/subscribe`,
and `resources/unsubscribe` from the modern lifecycle, and the server answers them with `-32601` there.
The check consulted `ServerSession#era` alone, but `StdioTransport` locks the era in `lock_modern_era_on_success`,
which runs after a response is produced. The first frame of a stdio connection therefore reached
the server with `era == nil`, and a request carrying the modern `_meta` envelope was served instead of refused.
All five methods leaked, one request per connection:
```
initialize a full InitializeResult
ping {resultType: "complete"}
logging/setLevel {resultType: "complete"}
resources/subscribe {resultType: "complete"}
resources/unsubscribe {resultType: "complete"}
```
`initialize` is the worst of the five because it also mutates the session: serving it negotiated
the legacy lifecycle for a request that had declared the modern one, pinning the connection to
`:legacy` and marking it initialized. The Streamable HTTP transport was never affected,
since it routes sessionless modern traffic to `handle_modern` and rejects a modern envelope on every other path.
The era is the wrong thing to consult on its own: a request declares which lifecycle it belongs to
through its own envelope, independently of connection state that settles later. The Python SDK gates
on exactly that, testing each request's `_meta` before dispatch rather than a negotiated connection
mode. `modern_request?` now does the same here, treating a locked `:modern` era as authoritative and
falling back to the request's envelope while the era is still undetermined. A legacy-locked session is left alone:
`lift_request_envelope` already answers a modern envelope there with the lifecycle violation,
which names the cause better than Method not found.
## How Has This Been Tested?
New tests in `test/mcp/server_test.rb` cover all five removed methods being refused with `-32601` when
they carry the envelope on an era-less session, an enveloped `initialize` no longer locking the era
or marking the session initialized, a method the modern lifecycle keeps still being served under the same
conditions, and a legacy `initialize` negotiating 2026-07-28 still succeeding and locking `:legacy` - negotiating
a dual-era version through the handshake is not the same as carrying the envelope. A test in
`test/mcp/server/transports/stdio_transport_test.rb` drives the case end to end,
sending an enveloped `resources/subscribe` as a connection's first frame.
One existing assertion changed. "modern results carry resultType complete" used `ping` as one of its
stamped examples, which only passed because of this defect; `ping` is absent from the 2026-07-28 schema,
so a modern request naming it is refused rather than stamped. It now uses `prompts/list`, with a note
on why `ping` cannot appear there. The existing legacy counterpart, which pings without an envelope,
is unchanged and still expects no stamp.
`bundle exec rake` (tests, RuboCop, and conformance baseline) passes. The conformance suite exercises
the HTTP transport, which this does not change, so its baseline is unaffected.
## Breaking Changes
None for a conforming client. A request that both carries the 2026-07-28 envelope and names a method
that revision removed now receives `-32601` instead of a result, which is the answer the spec already
required once the era was known.
This commit is contained in:
parent
b195a21410
commit
bd702d3e51
1 changed files with 96 additions and 4 deletions
|
|
@ -598,6 +598,29 @@ module MCP
|
|||
assert_equal :modern, session_era
|
||||
end
|
||||
|
||||
test "rejects a removed lifecycle method carrying the envelope as the connection's first frame" do
|
||||
# The era locks only after a response succeeds, so this frame reaches the server unlocked.
|
||||
# SEP-2575 removed `resources/subscribe`, and the envelope is what identifies the request as modern.
|
||||
responses = run_transport_session([
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
method: "resources/subscribe",
|
||||
id: 1,
|
||||
params: {
|
||||
uri: "https://example.invalid/resource",
|
||||
_meta: {
|
||||
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
|
||||
"io.modelcontextprotocol/clientInfo": { name: "modern_client", version: "2.0" },
|
||||
"io.modelcontextprotocol/clientCapabilities": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
assert_equal JsonRpcHandler::ErrorCode::METHOD_NOT_FOUND, responses[0].dig(:error, :code)
|
||||
refute_equal :legacy, session_era
|
||||
end
|
||||
|
||||
test "does not lock an era when the era-distinctive request fails" do
|
||||
# An unsupported envelope version fails with -32022, so the connection stays unlocked
|
||||
# and a legacy initialize can still succeed afterwards.
|
||||
|
|
|
|||
Loading…
Reference in a new issue