Handle the SEP-2575 Modern Request Envelope in the Server Core - #475
Open
koic wants to merge 1 commit into
Open
Conversation
## Motivation and Context Second step of the stateless lifecycle (SEP-2575, modelcontextprotocol/modelcontextprotocol#2575) for the 2026-07-28 MCP spec release, building on the `MCP::RequestEnvelope` foundations. `Server#handle_request` now lifts the per-request `_meta` envelope before dispatch: a request whose `_meta` carries the full required triple is validated (raising `-32022` with `data: { supported:, requested: }` for unsupported versions) and its envelope is threaded into `tools/call`, `prompts/get`, `completion/complete`, `resources/read`, subscribe/unsubscribe, and custom method handlers via `MCP::ServerContext`. A partial triple keeps flowing through the legacy path untouched, so existing `_meta` usage (`progressToken`, trace context) is unaffected. `MCP::ServerContext` gains per-request readers and a capability guard: - `modern?`, `protocol_version`, `client_info`, and `client_capabilities` read from the envelope first and fall back to session state stored by `initialize` on legacy requests. The envelope always wins because servers MUST NOT infer client state from prior requests; nothing is written to the session on the modern path. - `require_client_capability!(*path)` raises `Server::MissingRequiredClientCapabilityError` (`-32021` with `data: { requiredCapabilities: }`) when the request did not declare the capability. - `notify_log_message` honors the per-request `io.modelcontextprotocol/logLevel`: on modern requests without it, the server MUST NOT send `notifications/message`, and an insufficient level drops the message the same way. `MCP::ServerSession` gains the connection-era lock of the dual-era serving model: `era` is `nil` until the first era-distinctive message succeeds, a successful `initialize` locks `:legacy` as a side effect of `mark_initialized!`, `lock_era!` refuses to flip an established era, and transports can construct per-request sessions with `era: :modern`. On a modern-locked session, `initialize` is rejected with `-32022` (the modern lifecycle has no handshake) and the envelope triple becomes required for every other request except `server/discover`. The existing plain `RuntimeError` raises in `ServerSession#list_roots` and friends are intentionally unchanged: converting them to `RequestHandlerError` subclasses would break callers rescuing `RuntimeError`. Typed `-32021` errors are scoped to the new envelope-based guard. Refs modelcontextprotocol#389. ## How Has This Been Tested? New tests in `test/mcp/server_test.rb` cover the wire behavior through `Server#handle`: envelope data exposure without session mutation, `-32022` for unsupported envelope versions, partial triples staying legacy, the envelope requirement and `initialize` rejection on modern-locked sessions, the `server/discover` exemption, `require_client_capability!` returning `-32021` with the `requiredCapabilities` data shape, and the `ServerSession` era-lock transitions (including preset `era: :modern` and invalid values). New tests in `test/mcp/server_context_test.rb` cover the per-request logLevel gate (absent, insufficient, and sufficient levels), the envelope-first/session-fallback readers, and string-keyed capability matching in `require_client_capability!`. ## Breaking Changes None. All new keyword arguments default to `nil`, legacy requests take exactly the same code path as before, and the era lock only constrains sequences that were previously impossible (modern-era traffic). Fix a Ruby 2.7 ArgumentError When Raising UnsupportedProtocolVersionError ## Motivation and Context On Ruby 2.7, a method that declares a keyword parameter splits a trailing symbol-keyed Hash positional argument into keywords. `UnsupportedProtocolVersionError#initialize` declared `supported:`, so passing the request Hash as the second positional argument raised `ArgumentError: unknown keywords: :name, :arguments, :_meta` inside the error constructor, and the intended `-32022` response surfaced as a `-32603` internal error on the Ruby 2.7 CI job. No caller overrides `supported:`; drop the keyword parameter and read `Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS` directly, matching the keyword-free shape of `ResourceNotFoundError` and `MissingRequiredClientCapabilityError`. ## How Has This Been Tested? - Reproduced the `-32603` responses on Ruby 2.7.8 and confirmed both now return `-32022` - Added a regression test constructing the error with a symbol-keyed request Hash - `bundle exec rake test` on Ruby 2.7.8: 1467 runs, 0 failures ## Breaking Changes None. The `supported:` keyword argument was never passed by any caller.
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.
Motivation and Context
Second step of the stateless lifecycle (SEP-2575, modelcontextprotocol/modelcontextprotocol#2575) for the 2026-07-28 MCP spec release, building on the
MCP::RequestEnvelopefoundations.Server#handle_requestnow lifts the per-request_metaenvelope before dispatch: a request whose_metacarries the full required triple is validated (raising-32022withdata: { supported:, requested: }for unsupported versions) and its envelope is threaded intotools/call,prompts/get,completion/complete,resources/read, subscribe/unsubscribe, and custom method handlers viaMCP::ServerContext. A partial triple keeps flowing through the legacy path untouched, so existing_metausage (progressToken, trace context) is unaffected.MCP::ServerContextgains per-request readers and a capability guard:modern?,protocol_version,client_info, andclient_capabilitiesread from the envelope first and fall back to session state stored byinitializeon legacy requests. The envelope always wins because servers MUST NOT infer client state from prior requests; nothing is written to the session on the modern path.require_client_capability!(*path)raisesServer::MissingRequiredClientCapabilityError(-32021withdata: { requiredCapabilities: }) when the request did not declare the capability.notify_log_messagehonors the per-requestio.modelcontextprotocol/logLevel: on modern requests without it, the server MUST NOT sendnotifications/message, and an insufficient level drops the message the same way.MCP::ServerSessiongains the connection-era lock of the dual-era serving model:eraisniluntil the first era-distinctive message succeeds, a successfulinitializelocks:legacyas a side effect ofmark_initialized!,lock_era!refuses to flip an established era, and transports can construct per-request sessions withera: :modern. On a modern-locked session,initializeis rejected with-32022(the modern lifecycle has no handshake) and the envelope triple becomes required for every other request exceptserver/discover.The existing plain
RuntimeErrorraises inServerSession#list_rootsand friends are intentionally unchanged: converting them toRequestHandlerErrorsubclasses would break callers rescuingRuntimeError. Typed-32021errors are scoped to the new envelope-based guard.Refs #389.
How Has This Been Tested?
New tests in
test/mcp/server_test.rbcover the wire behavior throughServer#handle: envelope data exposure without session mutation,-32022for unsupported envelope versions, partial triples staying legacy, the envelope requirement andinitializerejection on modern-locked sessions, theserver/discoverexemption,require_client_capability!returning-32021with therequiredCapabilitiesdata shape, and theServerSessionera-lock transitions (including presetera: :modernand invalid values).New tests in
test/mcp/server_context_test.rbcover the per-request logLevel gate (absent, insufficient, and sufficient levels), the envelope-first/session-fallback readers, and string-keyed capability matching inrequire_client_capability!.Breaking Changes
None. All new keyword arguments default to
nil, legacy requests take exactly the same code path as before, and the era lock only constrains sequences that were previously impossible (modern-era traffic).Types of changes
Checklist