## Motivation and Context Clients that require MCP 2026-07-28, such as ChatGPT, reject connections because the SDK does not advertise that version: `Server#init` answers a 2026-07-28 request with 2025-11-25, the Streamable HTTP transport 400s an `MCP-Protocol-Version: 2026-07-28` header, and `server/discover` omits the version from `supportedVersions`. Making 2026-07-28 the latest entry in `SUPPORTED_STABLE_PROTOCOL_VERSIONS` fixes all three at once, since they all read that list. The version now serves both lifecycles of the SEP-2575 dual-era model: negotiable through the legacy `initialize` handshake, and simultaneously the version of the per-request modern lifecycle. Parts of the 2026-07-28 feature set remain unimplemented, such as the stateless modern lifecycle over the transports, and are being built separately. This change deliberately limits itself to the version bump, focusing first on eliminating the connection errors that 2026-07-28 clients hit. Known follow-up: the in-progress modern HTTP path routes requests to the sessionless modern lifecycle when the header version is absent from `SUPPORTED_STABLE_PROTOCOL_VERSIONS`, so that routing predicate needs rework on top of this change. Closes #474 ## How Has This Been Tested? New regression tests: `initialize` with `protocolVersion: "2026-07-28"` echoes it back (`server_test.rb`), a follow-up POST with `MCP-Protocol-Version: 2026-07-28` passes header validation (`streamable_http_transport_test.rb`), and `Configuration.new(protocol_version: "2026-07-28")` is accepted (`configuration_test.rb`). `bundle exec rake test` passes (1454 runs, 0 failures). The conformance server suite passes 30/30. The client `initialize` scenario is added to the expected-failures baseline: the conformance framework pins the client's requested version to 2025-11-25 and does not accept 2026-07-28 yet; the entry carries a TODO to remove it when the framework catches up. ## Breaking Changes Servers that do not pin `configuration.protocol_version` now answer version negotiation fallbacks and default client requests with 2026-07-28 instead of 2025-11-25. 2025-11-25 remains fully supported and negotiable; clients that request it are unaffected.
250 lines
9.3 KiB
Ruby
250 lines
9.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "test_helper"
|
|
|
|
module MCP
|
|
class ConfigurationTest < ActiveSupport::TestCase
|
|
test "initializes with a default no-op exception reporter" do
|
|
config = Configuration.new
|
|
assert_respond_to config, :exception_reporter
|
|
|
|
# The default reporter should be callable but do nothing
|
|
exception = StandardError.new("test error")
|
|
server_context = { test: "context" }
|
|
|
|
assert_nothing_raised do
|
|
config.exception_reporter.call(exception, server_context)
|
|
end
|
|
end
|
|
|
|
test "allows setting a custom exception reporter" do
|
|
config = Configuration.new
|
|
reported_exception = nil
|
|
reported_context = nil
|
|
|
|
config.exception_reporter = ->(exception, server_context) do
|
|
reported_exception = exception
|
|
reported_context = server_context
|
|
end
|
|
|
|
test_exception = StandardError.new("test error")
|
|
test_context = { foo: "bar" }
|
|
|
|
config.exception_reporter.call(test_exception, test_context)
|
|
|
|
assert_equal test_exception, reported_exception
|
|
assert_equal test_context, reported_context
|
|
end
|
|
|
|
# https://github.com/modelcontextprotocol/modelcontextprotocol/blob/14ec41c/schema/draft/schema.ts#L15
|
|
test "initializes with default protocol version" do
|
|
config = Configuration.new
|
|
assert_equal Configuration::LATEST_STABLE_PROTOCOL_VERSION, config.protocol_version
|
|
end
|
|
|
|
test "uses the draft protocol version when protocol_version is set to nil" do
|
|
config = Configuration.new(protocol_version: nil)
|
|
assert_equal Configuration::LATEST_STABLE_PROTOCOL_VERSION, config.protocol_version
|
|
end
|
|
|
|
test "raises ArgumentError when setting the draft protocol version" do
|
|
exception = assert_raises(ArgumentError) do
|
|
# DRAFT-2025-v3 is the latest draft protocol version:
|
|
# https://github.com/modelcontextprotocol/modelcontextprotocol/blob/14ec41c/schema/draft/schema.ts#L15
|
|
Configuration.new(protocol_version: "DRAFT-2025-v3")
|
|
end
|
|
|
|
assert_equal("protocol_version must be 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05", exception.message)
|
|
end
|
|
|
|
test "raises ArgumentError when protocol_version is not a supported protocol version" do
|
|
config = Configuration.new
|
|
exception = assert_raises(ArgumentError) do
|
|
custom_version = "2025-03-27"
|
|
config.protocol_version = custom_version
|
|
end
|
|
assert_equal("protocol_version must be 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05", exception.message)
|
|
end
|
|
|
|
test "exposes the SEP-2575 modern protocol versions" do
|
|
assert_equal "2026-07-28", Configuration::LATEST_MODERN_PROTOCOL_VERSION
|
|
assert_equal ["2026-07-28"], Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS
|
|
assert Configuration.modern_protocol_version?("2026-07-28")
|
|
refute Configuration.modern_protocol_version?("2025-11-25")
|
|
end
|
|
|
|
test "accepts 2026-07-28 as the protocol version" do
|
|
# 2026-07-28 serves both lifecycles of the dual-era model (SEP-2575), so it is negotiable through
|
|
# the legacy `initialize` handshake and settable as the fallback version.
|
|
config = Configuration.new(protocol_version: Configuration::LATEST_MODERN_PROTOCOL_VERSION)
|
|
assert_equal "2026-07-28", config.protocol_version
|
|
end
|
|
|
|
test "raises ArgumentError when protocol_version is not a boolean value" do
|
|
config = Configuration.new
|
|
exception = assert_raises(ArgumentError) do
|
|
config.validate_tool_call_arguments = "true"
|
|
end
|
|
assert_equal("validate_tool_call_arguments must be a boolean", exception.message)
|
|
end
|
|
|
|
test "raises ArgumentError when validate_tool_call_results is not a boolean value" do
|
|
config = Configuration.new
|
|
exception = assert_raises(ArgumentError) do
|
|
config.validate_tool_call_results = "true"
|
|
end
|
|
assert_equal("validate_tool_call_results must be a boolean", exception.message)
|
|
end
|
|
|
|
test "merges protocol version from other configuration" do
|
|
config1 = Configuration.new(protocol_version: "2025-03-26")
|
|
config2 = Configuration.new(protocol_version: "2025-06-18")
|
|
config3 = Configuration.new
|
|
|
|
merged = config1.merge(config2)
|
|
assert_equal "2025-06-18", merged.protocol_version
|
|
|
|
merged = config1.merge(config3)
|
|
assert_equal "2025-03-26", merged.protocol_version
|
|
|
|
merged = config3.merge(config1)
|
|
assert_equal "2025-03-26", merged.protocol_version
|
|
end
|
|
|
|
test "defaults validate_tool_call_arguments to true" do
|
|
config = Configuration.new
|
|
assert config.validate_tool_call_arguments
|
|
end
|
|
|
|
test "can set validate_tool_call_arguments to false" do
|
|
config = Configuration.new(validate_tool_call_arguments: false)
|
|
refute config.validate_tool_call_arguments
|
|
end
|
|
|
|
test "validate_tool_call_arguments? returns false when set" do
|
|
config = Configuration.new(validate_tool_call_arguments: false)
|
|
refute config.validate_tool_call_arguments?
|
|
end
|
|
|
|
test "validate_tool_call_arguments? returns true when not set" do
|
|
config = Configuration.new
|
|
assert config.validate_tool_call_arguments?
|
|
end
|
|
|
|
test "merge preserves validate_tool_call_arguments from other config" do
|
|
config1 = Configuration.new(validate_tool_call_arguments: false)
|
|
config2 = Configuration.new
|
|
merged = config1.merge(config2)
|
|
assert merged.validate_tool_call_arguments?
|
|
end
|
|
|
|
test "merge preserves validate_tool_call_arguments from self when other not set" do
|
|
config1 = Configuration.new(validate_tool_call_arguments: false)
|
|
config2 = Configuration.new
|
|
merged = config2.merge(config1)
|
|
refute merged.validate_tool_call_arguments
|
|
end
|
|
|
|
test "defaults validate_tool_call_results to false" do
|
|
config = Configuration.new
|
|
refute config.validate_tool_call_results
|
|
end
|
|
|
|
test "can set validate_tool_call_results to true" do
|
|
config = Configuration.new(validate_tool_call_results: true)
|
|
assert config.validate_tool_call_results
|
|
end
|
|
|
|
test "validate_tool_call_results? returns false when not set" do
|
|
config = Configuration.new
|
|
refute config.validate_tool_call_results?
|
|
end
|
|
|
|
test "validate_tool_call_results? returns true when set" do
|
|
config = Configuration.new(validate_tool_call_results: true)
|
|
assert config.validate_tool_call_results?
|
|
end
|
|
|
|
test "merge preserves validate_tool_call_results from other config" do
|
|
config1 = Configuration.new(validate_tool_call_results: true)
|
|
config2 = Configuration.new
|
|
merged = config1.merge(config2)
|
|
refute merged.validate_tool_call_results?
|
|
end
|
|
|
|
test "merge preserves validate_tool_call_results from self when other set" do
|
|
config1 = Configuration.new(validate_tool_call_results: true)
|
|
config2 = Configuration.new
|
|
merged = config2.merge(config1)
|
|
assert merged.validate_tool_call_results
|
|
end
|
|
|
|
test "initializes with a default pass-through around_request" do
|
|
config = Configuration.new
|
|
called = false
|
|
config.around_request.call({}) { called = true }
|
|
assert called
|
|
end
|
|
|
|
test "allows setting a custom around_request" do
|
|
config = Configuration.new
|
|
call_log = []
|
|
config.around_request = ->(_data, &request_handler) {
|
|
call_log << :before
|
|
request_handler.call
|
|
call_log << :after
|
|
}
|
|
|
|
config.around_request.call({}) { call_log << :execute }
|
|
assert_equal([:before, :execute, :after], call_log)
|
|
end
|
|
|
|
test "around_request? returns false by default" do
|
|
config = Configuration.new
|
|
refute config.around_request?
|
|
end
|
|
|
|
test "around_request? returns true when set" do
|
|
config = Configuration.new
|
|
config.around_request = ->(_data, &request_handler) { request_handler.call }
|
|
assert config.around_request?
|
|
end
|
|
|
|
test "merge preserves around_request from other config" do
|
|
custom = ->(_data, &request_handler) { request_handler.call }
|
|
config1 = Configuration.new
|
|
config2 = Configuration.new(around_request: custom)
|
|
merged = config1.merge(config2)
|
|
assert_equal custom, merged.around_request
|
|
end
|
|
|
|
test "merge preserves around_request from self when other not set" do
|
|
custom = ->(_data, &request_handler) { request_handler.call }
|
|
config1 = Configuration.new(around_request: custom)
|
|
config2 = Configuration.new
|
|
merged = config1.merge(config2)
|
|
assert_equal custom, merged.around_request
|
|
end
|
|
|
|
test "raises ArgumentError when protocol_version is not a supported value" do
|
|
exception = assert_raises(ArgumentError) do
|
|
Configuration.new(protocol_version: "1999-12-31")
|
|
end
|
|
assert_match(/\Aprotocol_version must be/, exception.message)
|
|
end
|
|
|
|
test "raises ArgumentError when validate_tool_call_arguments is not a boolean" do
|
|
exception = assert_raises(ArgumentError) do
|
|
Configuration.new(validate_tool_call_arguments: "true")
|
|
end
|
|
assert_equal("validate_tool_call_arguments must be a boolean", exception.message)
|
|
end
|
|
|
|
test "raises ArgumentError when validate_tool_call_results is not a boolean" do
|
|
exception = assert_raises(ArgumentError) do
|
|
Configuration.new(validate_tool_call_results: "true")
|
|
end
|
|
assert_equal("validate_tool_call_results must be a boolean", exception.message)
|
|
end
|
|
end
|
|
end
|