## 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.
145 lines
5.6 KiB
Ruby
145 lines
5.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "test_helper"
|
|
|
|
module MCP
|
|
class RequestEnvelopeTest < ActiveSupport::TestCase
|
|
test "exposes the reserved SEP-2575 meta key names" do
|
|
# The exact strings are wire vocabulary shared with other SDKs.
|
|
assert_equal "io.modelcontextprotocol/protocolVersion", RequestEnvelope::PROTOCOL_VERSION_META_KEY
|
|
assert_equal "io.modelcontextprotocol/clientInfo", RequestEnvelope::CLIENT_INFO_META_KEY
|
|
assert_equal "io.modelcontextprotocol/clientCapabilities", RequestEnvelope::CLIENT_CAPABILITIES_META_KEY
|
|
assert_equal "io.modelcontextprotocol/logLevel", RequestEnvelope::LOG_LEVEL_META_KEY
|
|
end
|
|
|
|
test ".modern? returns true when the protocolVersion claim key is present" do
|
|
assert RequestEnvelope.modern?(modern_params)
|
|
assert RequestEnvelope.modern?({ _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } })
|
|
end
|
|
|
|
test ".modern? returns true for string keys" do
|
|
params = {
|
|
"_meta" => {
|
|
"io.modelcontextprotocol/protocolVersion" => "2026-07-28",
|
|
"io.modelcontextprotocol/clientInfo" => { "name" => "c", "version" => "1" },
|
|
"io.modelcontextprotocol/clientCapabilities" => {},
|
|
},
|
|
}
|
|
|
|
assert RequestEnvelope.modern?(params)
|
|
end
|
|
|
|
test ".modern? stays true for a claimed but incomplete envelope" do
|
|
# A claimed envelope must be validated by `parse!` (`-32602`), never silently served as legacy,
|
|
# matching the TypeScript and Python classifiers.
|
|
params = modern_params
|
|
params[:_meta].delete(:"io.modelcontextprotocol/clientCapabilities")
|
|
|
|
assert RequestEnvelope.modern?(params)
|
|
end
|
|
|
|
test ".modern? returns false for legacy _meta entries such as progressToken" do
|
|
refute RequestEnvelope.modern?({ name: "echo", _meta: { progressToken: "token" } })
|
|
end
|
|
|
|
test ".modern? returns false without params or _meta" do
|
|
refute RequestEnvelope.modern?(nil)
|
|
refute RequestEnvelope.modern?({})
|
|
refute RequestEnvelope.modern?({ name: "echo" })
|
|
refute RequestEnvelope.modern?({ _meta: nil })
|
|
end
|
|
|
|
test ".parse! returns a frozen envelope with the triple and optional log level" do
|
|
params = modern_params
|
|
params[:_meta][:"io.modelcontextprotocol/logLevel"] = "warning"
|
|
|
|
envelope = RequestEnvelope.parse!(params)
|
|
|
|
assert_predicate envelope, :frozen?
|
|
assert_equal "2026-07-28", envelope.protocol_version
|
|
assert_equal({ name: "test_client", version: "1.0.0" }, envelope.client_info)
|
|
assert_equal({ elicitation: {} }, envelope.client_capabilities)
|
|
assert_equal "warning", envelope.log_level
|
|
end
|
|
|
|
test ".parse! leaves log_level nil when absent" do
|
|
assert_nil RequestEnvelope.parse!(modern_params).log_level
|
|
end
|
|
|
|
test ".parse! raises UnsupportedProtocolVersionError with the SEP-2575 data shape" do
|
|
params = modern_params(version: "2025-11-25")
|
|
|
|
error = assert_raises(Server::UnsupportedProtocolVersionError) do
|
|
RequestEnvelope.parse!(params)
|
|
end
|
|
|
|
assert_equal ErrorCodes::UNSUPPORTED_PROTOCOL_VERSION, error.error_code
|
|
assert_equal Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS, error.error_data[:supported]
|
|
assert_equal "2025-11-25", error.error_data[:requested]
|
|
end
|
|
|
|
test ".parse! accepts an envelope without the optional clientInfo" do
|
|
# `clientInfo` became optional after the SEP was finalized (spec PR #3002);
|
|
# a conformant client configured not to identify itself must not be rejected.
|
|
params = modern_params
|
|
params[:_meta].delete(:"io.modelcontextprotocol/clientInfo")
|
|
|
|
envelope = RequestEnvelope.parse!(params)
|
|
|
|
assert_nil envelope.client_info
|
|
assert_equal "2026-07-28", envelope.protocol_version
|
|
assert_equal({ elicitation: {} }, envelope.client_capabilities)
|
|
end
|
|
|
|
test ".parse! raises Invalid params naming the missing key when clientCapabilities is absent" do
|
|
params = modern_params
|
|
params[:_meta].delete(:"io.modelcontextprotocol/clientCapabilities")
|
|
|
|
error = assert_raises(Server::RequestHandlerError) do
|
|
RequestEnvelope.parse!(params)
|
|
end
|
|
|
|
assert_equal :invalid_params, error.error_type
|
|
assert_equal JsonRpcHandler::ErrorCode::INVALID_PARAMS, error.error_code
|
|
assert_includes error.message, "io.modelcontextprotocol/clientCapabilities"
|
|
refute_includes error.message, "io.modelcontextprotocol/protocolVersion"
|
|
end
|
|
|
|
test ".parse! raises Invalid params when a required field has the wrong type" do
|
|
params = modern_params
|
|
params[:_meta][:"io.modelcontextprotocol/clientCapabilities"] = "not-a-hash"
|
|
|
|
error = assert_raises(Server::RequestHandlerError) do
|
|
RequestEnvelope.parse!(params)
|
|
end
|
|
|
|
assert_equal :invalid_params, error.error_type
|
|
assert_equal JsonRpcHandler::ErrorCode::INVALID_PARAMS, error.error_code
|
|
end
|
|
|
|
test ".parse! raises Invalid params when the optional clientInfo has the wrong type" do
|
|
params = modern_params
|
|
params[:_meta][:"io.modelcontextprotocol/clientInfo"] = "not-a-hash"
|
|
|
|
error = assert_raises(Server::RequestHandlerError) do
|
|
RequestEnvelope.parse!(params)
|
|
end
|
|
|
|
assert_equal :invalid_params, error.error_type
|
|
assert_includes error.message, "io.modelcontextprotocol/clientInfo"
|
|
end
|
|
|
|
private
|
|
|
|
def modern_params(version: "2026-07-28")
|
|
{
|
|
name: "echo",
|
|
_meta: {
|
|
"io.modelcontextprotocol/protocolVersion": version,
|
|
"io.modelcontextprotocol/clientInfo": { name: "test_client", version: "1.0.0" },
|
|
"io.modelcontextprotocol/clientCapabilities": { elicitation: {} },
|
|
},
|
|
}
|
|
end
|
|
end
|
|
end
|