Align modern envelope validation with the finalized spec
## Motivation and Context Two deviations from the finalized 2026-07-28 specification in the SEP-2575 envelope validation, both introduced against the frozen SEP text and overtaken by post-final spec changes: 1. `clientInfo` was treated as required. Spec PR modelcontextprotocol/modelcontextprotocol#3002 made it optional (clients SHOULD include it unless configured not to), and the TypeScript and Python SDKs validate only the `protocolVersion` + `clientCapabilities` pair. A conformant client configured not to identify itself was rejected. 2. A missing or mistyped envelope answered `-32600` Invalid Request. The spec maps missing required envelope fields to `-32602` Invalid params, and both reference SDKs answer with `-32602` naming the offending keys. Fixing these also corrects the era classification to match the reference SDKs: a request claims the modern lifecycle when `_meta` carries `io.modelcontextprotocol/protocolVersion` (the TypeScript envelope claim and the Python `_has_modern_envelope` are both single-key checks), and a claimed-but-incomplete envelope is now validated and rejected with `-32602` naming the missing keys instead of silently flowing through the legacy path. Legacy `_meta` usage without the claim key (`progressToken`, trace context) is classified exactly as before. Unchanged on purpose: the era-lock violations keep their codes (`-32600` for a modern envelope on a legacy-locked session, `-32022` for `initialize` on a modern-locked one), matching the Python SDK. ## How Has This Been Tested? `test/mcp/request_envelope_test.rb` now covers: classification by the claim key alone, a claimed-but-incomplete envelope staying modern, parsing without the optional `clientInfo` (reader returns `nil`), `-32602` with the offending key names for a missing `clientCapabilities` and for mistyped required and optional fields. `test/mcp/server_test.rb` covers the dispatch-level behavior: a claimed but incomplete envelope answers `-32602` naming the missing key, an envelope without `clientInfo` is served with `server_context.client_info` reading `nil`, and a claim-less request on a modern-locked session answers `-32602` naming the required keys. The stdio and Streamable HTTP transport tests assert the new code on their envelope-requirement paths. ## Breaking Changes None for conforming clients. Requests that were already rejected change error code (`-32600` to `-32602`, HTTP status 400 unchanged), and requests carrying the `protocolVersion` claim key with an incomplete envelope are now rejected as the spec mandates instead of being served as legacy requests. Envelopes without `clientInfo`, previously rejected, now succeed.
This commit is contained in:
parent
b2f8070488
commit
f39b1bf4aa
2 changed files with 109 additions and 31 deletions
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
module MCP
|
||||
# The per-request `_meta` envelope of the stateless "modern" lifecycle (MCP 2026-07-28, SEP-2575).
|
||||
# The modern lifecycle has no `initialize` handshake: every request identifies its protocol version,
|
||||
# client, and client capabilities through reserved `_meta` keys, and the server validates
|
||||
# each request independently. Servers MUST NOT infer capabilities from prior requests,
|
||||
# which is why the envelope is a per-request value object rather than session state.
|
||||
# The modern lifecycle has no `initialize` handshake: every request identifies its protocol version
|
||||
# and client capabilities through reserved `_meta` keys (plus an optional client identity),
|
||||
# and the server validates each request independently. Servers MUST NOT infer capabilities from
|
||||
# prior requests, which is why the envelope is a per-request value object rather than session state.
|
||||
#
|
||||
# https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575
|
||||
class RequestEnvelope
|
||||
|
|
@ -22,26 +22,31 @@ module MCP
|
|||
# finalized (spec PR modelcontextprotocol/modelcontextprotocol#3002). A server MAY omit it.
|
||||
SERVER_INFO_META_KEY = "io.modelcontextprotocol/serverInfo"
|
||||
|
||||
# `clientInfo` is deliberately absent: it became optional after the SEP was finalized
|
||||
# (spec PR modelcontextprotocol/modelcontextprotocol#3002), so servers MUST accept
|
||||
# envelopes without it. The TypeScript and Python SDKs validate the same required pair.
|
||||
REQUIRED_META_KEYS = [
|
||||
PROTOCOL_VERSION_META_KEY,
|
||||
CLIENT_INFO_META_KEY,
|
||||
CLIENT_CAPABILITIES_META_KEY,
|
||||
].freeze
|
||||
|
||||
class << self
|
||||
# A request is classified as modern only when the full REQUIRED triple is present,
|
||||
# matching the TypeScript SDK's `RequestMetaEnvelopeSchema` and the Python SDK's `_has_modern_envelope`.
|
||||
# A partial triple is treated as legacy so existing `_meta` usage (`progressToken`, trace context) keeps
|
||||
# flowing through the legacy path.
|
||||
# A request claims the modern lifecycle when its `_meta` carries `io.modelcontextprotocol/protocolVersion`,
|
||||
# matching the TypeScript SDK's envelope claim and the Python SDK's `_has_modern_envelope`.
|
||||
# Classification is deliberately looser than validation: a claimed-but-malformed envelope is
|
||||
# rejected by {parse!} with `-32602` instead of silently flowing through the legacy path,
|
||||
# while `_meta` without the claim key (`progressToken`, trace context) stays legacy.
|
||||
def modern?(params)
|
||||
meta = extract_meta(params)
|
||||
return false unless meta.is_a?(Hash)
|
||||
|
||||
REQUIRED_META_KEYS.all? { |key| !read(meta, key).nil? }
|
||||
!read(meta, PROTOCOL_VERSION_META_KEY).nil?
|
||||
end
|
||||
|
||||
# Parses and validates the envelope. `request` is only used to enrich the raised error;
|
||||
# callers dispatching notifications can omit it.
|
||||
# Parses and validates the envelope: `protocolVersion` and `clientCapabilities` are required,
|
||||
# `clientInfo` is optional. A missing or mistyped field is Invalid params (`-32602`) naming
|
||||
# the offending keys, the code and shape the spec mandates and the reference SDKs emit.
|
||||
# `request` is only used to enrich the raised error; callers dispatching notifications can omit it.
|
||||
def parse!(params, request: nil)
|
||||
meta = extract_meta(params)
|
||||
meta = {} unless meta.is_a?(Hash)
|
||||
|
|
@ -50,11 +55,17 @@ module MCP
|
|||
client_info = read(meta, CLIENT_INFO_META_KEY)
|
||||
client_capabilities = read(meta, CLIENT_CAPABILITIES_META_KEY)
|
||||
|
||||
unless protocol_version.is_a?(String) && client_info.is_a?(Hash) && client_capabilities.is_a?(Hash)
|
||||
invalid_keys = []
|
||||
invalid_keys << PROTOCOL_VERSION_META_KEY unless protocol_version.is_a?(String)
|
||||
invalid_keys << CLIENT_CAPABILITIES_META_KEY unless client_capabilities.is_a?(Hash)
|
||||
invalid_keys << CLIENT_INFO_META_KEY unless client_info.nil? || client_info.is_a?(Hash)
|
||||
|
||||
unless invalid_keys.empty?
|
||||
raise Server::RequestHandlerError.new(
|
||||
"Invalid Request: modern requests require `#{REQUIRED_META_KEYS.join("`, `")}` in `_meta`",
|
||||
"Invalid params: missing or invalid `#{invalid_keys.join("`, `")}` in `_meta`",
|
||||
request,
|
||||
error_type: :invalid_request,
|
||||
error_type: :invalid_params,
|
||||
error_code: JsonRpcHandler::ErrorCode::INVALID_PARAMS,
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -87,9 +98,11 @@ module MCP
|
|||
end
|
||||
end
|
||||
|
||||
# `client_info` is `nil` when the client chose not to identify itself, which is legal:
|
||||
# it is self-reported data and MUST NOT drive behavior or security decisions anyway.
|
||||
attr_reader :protocol_version, :client_info, :client_capabilities, :log_level
|
||||
|
||||
def initialize(protocol_version:, client_info:, client_capabilities:, log_level: nil)
|
||||
def initialize(protocol_version:, client_capabilities:, client_info: nil, log_level: nil)
|
||||
@protocol_version = protocol_version
|
||||
@client_info = client_info
|
||||
@client_capabilities = client_capabilities
|
||||
|
|
|
|||
|
|
@ -689,10 +689,14 @@ module MCP
|
|||
|
||||
RequestEnvelope.parse!(params, request: params)
|
||||
elsif era == :modern
|
||||
# A claim-less request on a modern session is a malformed envelope, not a malformed request:
|
||||
# the spec maps missing required envelope fields to Invalid params (`-32602`),
|
||||
# and the reference SDKs answer it naming the missing keys.
|
||||
raise RequestHandlerError.new(
|
||||
"Invalid Request: modern sessions require the SEP-2575 `_meta` envelope",
|
||||
"Invalid params: missing or invalid `#{RequestEnvelope::REQUIRED_META_KEYS.join("`, `")}` in `_meta`",
|
||||
params,
|
||||
error_type: :invalid_request,
|
||||
error_type: :invalid_params,
|
||||
error_code: JsonRpcHandler::ErrorCode::INVALID_PARAMS,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue