## Motivation and Context
SEP-2575 (modelcontextprotocol/modelcontextprotocol#2575, merged for the 2026-07-28 spec release) replaces
the `initialize` handshake with a stateless "modern" lifecycle: every request carries its protocol version,
client info, and client capabilities in reserved `_meta` keys, and the server validates each request independently.
This change lays the shared vocabulary for that lifecycle without changing any behavior:
- `MCP::Configuration` gains `LATEST_MODERN_PROTOCOL_VERSION` ("2026-07-28"), `SUPPORTED_MODERN_PROTOCOL_VERSIONS`,
and `Configuration.modern_protocol_version?`. Modern versions are deliberately kept out of `SUPPORTED_STABLE_PROTOCOL_VERSIONS`
so `initialize` can never negotiate them and `protocol_version=` keeps rejecting them.
- `MCP::ErrorCodes` gains `HEADER_MISMATCH` (-32020), completing the spec's stateless lifecycle error code block alongside
the existing -32021/-32022 constants.
- New `MCP::RequestEnvelope` parses and validates the per-request `_meta` triple (`io.modelcontextprotocol/protocolVersion`,
`clientInfo`, `clientCapabilities`, plus the optional deprecated `logLevel`). A request is classified as modern only
when the full triple is present, matching the TypeScript SDK's `RequestMetaEnvelopeSchema` and the Python SDK's `_has_modern_envelope`,
so partial or legacy `_meta` entries keep flowing through the legacy path untouched.
- New typed errors `Server::UnsupportedProtocolVersionError` (-32022 with `data: { supported:, requested: }`) and
`Server::MissingRequiredClientCapabilityError` (-32021 with `data: { requiredCapabilities: }`) reuse the established `RequestHandlerError`
`error_code`/`error_data` plumbing (the -32042 URL elicitation precedent).
Nothing dispatches through `RequestEnvelope` yet; the server-side modern handling, transport routing, and client support build on
this in follow-up changes.
Refs #389.
## How Has This Been Tested?
New `test/mcp/request_envelope_test.rb` covers the modern-triple classification (full triple, partial triple, string keys,
legacy `progressToken`), parsed field access, the -32022 error data shape, and invalid-request errors for incomplete or mistyped triples.
`test/mcp/configuration_test.rb` asserts the new constants, the predicate, and that modern versions stay rejected by `protocol_version=`.
`test/mcp/server_test.rb` asserts both typed errors surface on the wire with their SEP-2575 codes and data through `Server#handle`.
`test/mcp/error_codes_test.rb` pins -32020.
## Breaking Changes
None. Constants and classes are additive; no request path changes behavior.
95 lines
3.7 KiB
Ruby
95 lines
3.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
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.
|
|
#
|
|
# https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575
|
|
class RequestEnvelope
|
|
PROTOCOL_VERSION_META_KEY = "io.modelcontextprotocol/protocolVersion"
|
|
CLIENT_INFO_META_KEY = "io.modelcontextprotocol/clientInfo"
|
|
CLIENT_CAPABILITIES_META_KEY = "io.modelcontextprotocol/clientCapabilities"
|
|
|
|
# Optional per-request log level, replacing the `logging/setLevel` RPC in the modern lifecycle.
|
|
# Deprecated as of 2026-07-28 (SEP-2577) but still part of the wire format.
|
|
LOG_LEVEL_META_KEY = "io.modelcontextprotocol/logLevel"
|
|
|
|
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.
|
|
def modern?(params)
|
|
meta = extract_meta(params)
|
|
return false unless meta.is_a?(Hash)
|
|
|
|
REQUIRED_META_KEYS.all? { |key| !read(meta, key).nil? }
|
|
end
|
|
|
|
# Parses and validates the envelope. `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)
|
|
|
|
protocol_version = read(meta, PROTOCOL_VERSION_META_KEY)
|
|
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)
|
|
raise Server::RequestHandlerError.new(
|
|
"Invalid Request: modern requests require `#{REQUIRED_META_KEYS.join("`, `")}` in `_meta`",
|
|
request,
|
|
error_type: :invalid_request,
|
|
)
|
|
end
|
|
|
|
unless Configuration.modern_protocol_version?(protocol_version)
|
|
raise Server::UnsupportedProtocolVersionError.new(protocol_version, request)
|
|
end
|
|
|
|
new(
|
|
protocol_version: protocol_version,
|
|
client_info: client_info,
|
|
client_capabilities: client_capabilities,
|
|
log_level: read(meta, LOG_LEVEL_META_KEY),
|
|
)
|
|
end
|
|
|
|
private
|
|
|
|
# `Server#handle` accepts hashes parsed with either symbol or string keys, so read both forms
|
|
# (the same tolerance as `Server#handle_cancelled_notification`).
|
|
def extract_meta(params)
|
|
return unless params.is_a?(Hash)
|
|
|
|
meta = params[:_meta]
|
|
meta.nil? ? params["_meta"] : meta
|
|
end
|
|
|
|
def read(meta, key)
|
|
value = meta[key.to_sym]
|
|
value.nil? ? meta[key] : value
|
|
end
|
|
end
|
|
|
|
attr_reader :protocol_version, :client_info, :client_capabilities, :log_level
|
|
|
|
def initialize(protocol_version:, client_info:, client_capabilities:, log_level: nil)
|
|
@protocol_version = protocol_version
|
|
@client_info = client_info
|
|
@client_capabilities = client_capabilities
|
|
@log_level = log_level
|
|
freeze
|
|
end
|
|
end
|
|
end
|