## 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.
190 lines
6.4 KiB
Ruby
190 lines
6.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module MCP
|
|
class Configuration
|
|
LATEST_STABLE_PROTOCOL_VERSION = "2025-11-25"
|
|
SUPPORTED_STABLE_PROTOCOL_VERSIONS = [
|
|
LATEST_STABLE_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05",
|
|
].freeze
|
|
DEFAULT_NEGOTIATED_PROTOCOL_VERSION = "2025-03-26"
|
|
|
|
# Protocol versions of the stateless "modern" lifecycle introduced by the MCP 2026-07-28 spec release (SEP-2575).
|
|
# Modern versions are deliberately kept out of `SUPPORTED_STABLE_PROTOCOL_VERSIONS`: the modern lifecycle has
|
|
# no `initialize` handshake, so these versions are never negotiated (each request carries its own version in `_meta`
|
|
# and is validated against this list independently), and `protocol_version=` keeps rejecting them for the same reason.
|
|
# https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575
|
|
LATEST_MODERN_PROTOCOL_VERSION = "2026-07-28"
|
|
SUPPORTED_MODERN_PROTOCOL_VERSIONS = [LATEST_MODERN_PROTOCOL_VERSION].freeze
|
|
|
|
class << self
|
|
def modern_protocol_version?(version)
|
|
SUPPORTED_MODERN_PROTOCOL_VERSIONS.include?(version)
|
|
end
|
|
end
|
|
|
|
attr_writer :exception_reporter, :around_request
|
|
|
|
# @deprecated Use {#around_request=} instead. `instrumentation_callback`
|
|
# fires only after a request completes and cannot wrap execution in a
|
|
# surrounding block (e.g. for Application Performance Monitoring (APM) spans).
|
|
# @see #around_request=
|
|
attr_writer :instrumentation_callback
|
|
|
|
def initialize(exception_reporter: nil, around_request: nil, instrumentation_callback: nil, protocol_version: nil,
|
|
validate_tool_call_arguments: true, validate_tool_call_results: false)
|
|
@exception_reporter = exception_reporter
|
|
@around_request = around_request
|
|
@instrumentation_callback = instrumentation_callback
|
|
@protocol_version = protocol_version
|
|
if protocol_version
|
|
validate_protocol_version!(protocol_version)
|
|
end
|
|
validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
|
|
validate_value_of_validate_tool_call_results!(validate_tool_call_results)
|
|
|
|
@validate_tool_call_arguments = validate_tool_call_arguments
|
|
@validate_tool_call_results = validate_tool_call_results
|
|
end
|
|
|
|
def protocol_version=(protocol_version)
|
|
validate_protocol_version!(protocol_version)
|
|
|
|
@protocol_version = protocol_version
|
|
end
|
|
|
|
def validate_tool_call_arguments=(validate_tool_call_arguments)
|
|
validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
|
|
|
|
@validate_tool_call_arguments = validate_tool_call_arguments
|
|
end
|
|
|
|
def validate_tool_call_results=(validate_tool_call_results)
|
|
validate_value_of_validate_tool_call_results!(validate_tool_call_results)
|
|
|
|
@validate_tool_call_results = validate_tool_call_results
|
|
end
|
|
|
|
def protocol_version
|
|
@protocol_version || LATEST_STABLE_PROTOCOL_VERSION
|
|
end
|
|
|
|
def protocol_version?
|
|
!@protocol_version.nil?
|
|
end
|
|
|
|
def exception_reporter
|
|
@exception_reporter || default_exception_reporter
|
|
end
|
|
|
|
def exception_reporter?
|
|
!@exception_reporter.nil?
|
|
end
|
|
|
|
def around_request
|
|
@around_request || default_around_request
|
|
end
|
|
|
|
def around_request?
|
|
!@around_request.nil?
|
|
end
|
|
|
|
# @deprecated Use {#around_request} instead. `instrumentation_callback`
|
|
# fires only after a request completes and cannot wrap execution in a
|
|
# surrounding block (e.g. for Application Performance Monitoring (APM) spans).
|
|
# @see #around_request
|
|
def instrumentation_callback
|
|
@instrumentation_callback || default_instrumentation_callback
|
|
end
|
|
|
|
# @deprecated Use {#around_request?} instead.
|
|
# @see #around_request?
|
|
def instrumentation_callback?
|
|
!@instrumentation_callback.nil?
|
|
end
|
|
|
|
attr_reader :validate_tool_call_arguments
|
|
attr_reader :validate_tool_call_results
|
|
|
|
def validate_tool_call_arguments?
|
|
!!@validate_tool_call_arguments
|
|
end
|
|
|
|
def validate_tool_call_results?
|
|
!!@validate_tool_call_results
|
|
end
|
|
|
|
def merge(other)
|
|
return self if other.nil?
|
|
|
|
exception_reporter = if other.exception_reporter?
|
|
other.exception_reporter
|
|
else
|
|
@exception_reporter
|
|
end
|
|
|
|
around_request = if other.around_request?
|
|
other.around_request
|
|
else
|
|
@around_request
|
|
end
|
|
|
|
instrumentation_callback = if other.instrumentation_callback?
|
|
other.instrumentation_callback
|
|
else
|
|
@instrumentation_callback
|
|
end
|
|
|
|
protocol_version = if other.protocol_version?
|
|
other.protocol_version
|
|
else
|
|
@protocol_version
|
|
end
|
|
|
|
validate_tool_call_arguments = other.validate_tool_call_arguments
|
|
validate_tool_call_results = other.validate_tool_call_results
|
|
|
|
Configuration.new(
|
|
exception_reporter: exception_reporter,
|
|
around_request: around_request,
|
|
instrumentation_callback: instrumentation_callback,
|
|
protocol_version: protocol_version,
|
|
validate_tool_call_arguments: validate_tool_call_arguments,
|
|
validate_tool_call_results: validate_tool_call_results,
|
|
)
|
|
end
|
|
|
|
private
|
|
|
|
def validate_protocol_version!(protocol_version)
|
|
unless SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(protocol_version)
|
|
message = "protocol_version must be #{SUPPORTED_STABLE_PROTOCOL_VERSIONS[0...-1].join(", ")}, or #{SUPPORTED_STABLE_PROTOCOL_VERSIONS[-1]}"
|
|
raise ArgumentError, message
|
|
end
|
|
end
|
|
|
|
def validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
|
|
unless validate_tool_call_arguments.is_a?(TrueClass) || validate_tool_call_arguments.is_a?(FalseClass)
|
|
raise ArgumentError, "validate_tool_call_arguments must be a boolean"
|
|
end
|
|
end
|
|
|
|
def validate_value_of_validate_tool_call_results!(validate_tool_call_results)
|
|
unless validate_tool_call_results.is_a?(TrueClass) || validate_tool_call_results.is_a?(FalseClass)
|
|
raise ArgumentError, "validate_tool_call_results must be a boolean"
|
|
end
|
|
end
|
|
|
|
def default_exception_reporter
|
|
@default_exception_reporter ||= ->(exception, server_context) {}
|
|
end
|
|
|
|
def default_around_request
|
|
@default_around_request ||= ->(_data, &request_handler) { request_handler.call }
|
|
end
|
|
|
|
# @deprecated Use {#default_around_request} instead.
|
|
def default_instrumentation_callback
|
|
@default_instrumentation_callback ||= ->(data) {}
|
|
end
|
|
end
|
|
end
|