From dc25610162450de36f1f173d167f5f44337f8a73 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Fri, 31 Jul 2026 12:27:58 +0900 Subject: [PATCH] Support 2026-07-28 as the Latest Protocol Version ## 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. --- conformance/expected_failures.yml | 6 ++++- docs/building-clients.md | 4 +-- lib/mcp/configuration.rb | 11 ++++---- test/mcp/configuration_test.rb | 15 +++++------ .../streamable_http_transport_test.rb | 26 +++++++++++++++++++ test/mcp/server_test.rb | 14 ++++++++++ 6 files changed, 60 insertions(+), 16 deletions(-) diff --git a/conformance/expected_failures.yml b/conformance/expected_failures.yml index 0bbbbfb4..371853de 100644 --- a/conformance/expected_failures.yml +++ b/conformance/expected_failures.yml @@ -1,2 +1,6 @@ server: [] -client: [] +client: + # TODO: The conformance framework's client-initialization check pins the expected requested version to 2025-11-25, + # while this SDK's latest protocol version is 2026-07-28. Remove once the @modelcontextprotocol/conformance package + # accepts 2026-07-28. + - initialize diff --git a/docs/building-clients.md b/docs/building-clients.md index 6b4c7d01..9dfaf0d8 100644 --- a/docs/building-clients.md +++ b/docs/building-clients.md @@ -22,7 +22,7 @@ Call `MCP::Client#connect` to perform the MCP [initialization handshake](https:/ ```ruby client.connect -# => { "protocolVersion" => "2025-11-25", "capabilities" => {...}, "serverInfo" => {...} } +# => { "protocolVersion" => "2026-07-28", "capabilities" => {...}, "serverInfo" => {...} } client.connected? # => true client.server_info # => cached InitializeResult @@ -99,7 +99,7 @@ After `connect` succeeds, the HTTP transport captures the `Mcp-Session-Id` heade ```ruby http_transport.session_id # => "abc123..." -http_transport.protocol_version # => "2025-11-25" +http_transport.protocol_version # => "2026-07-28" ``` If the server terminates the session, subsequent requests return HTTP 404 and the transport raises `MCP::Client::SessionExpiredError` (a subclass of `RequestHandlerError`). Session state is cleared automatically; callers should start a new session by calling `connect` again. diff --git a/lib/mcp/configuration.rb b/lib/mcp/configuration.rb index 98f25e92..ed6d430b 100644 --- a/lib/mcp/configuration.rb +++ b/lib/mcp/configuration.rb @@ -2,16 +2,17 @@ module MCP class Configuration - LATEST_STABLE_PROTOCOL_VERSION = "2025-11-25" + LATEST_STABLE_PROTOCOL_VERSION = "2026-07-28" SUPPORTED_STABLE_PROTOCOL_VERSIONS = [ - LATEST_STABLE_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05", + LATEST_STABLE_PROTOCOL_VERSION, "2025-11-25", "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. + # 2026-07-28 serves both lifecycles of the dual-era model: it is negotiable through the legacy `initialize` + # handshake (so it also appears in `SUPPORTED_STABLE_PROTOCOL_VERSIONS`), and it is the version of the modern + # lifecycle, where each request carries its own version in `_meta` and is validated against this list + # independently, with no handshake. # https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575 LATEST_MODERN_PROTOCOL_VERSION = "2026-07-28" SUPPORTED_MODERN_PROTOCOL_VERSIONS = [LATEST_MODERN_PROTOCOL_VERSION].freeze diff --git a/test/mcp/configuration_test.rb b/test/mcp/configuration_test.rb index 732e5c25..86231f71 100644 --- a/test/mcp/configuration_test.rb +++ b/test/mcp/configuration_test.rb @@ -54,7 +54,7 @@ class ConfigurationTest < ActiveSupport::TestCase Configuration.new(protocol_version: "DRAFT-2025-v3") end - assert_equal("protocol_version must be 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05", exception.message) + 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 @@ -63,7 +63,7 @@ class ConfigurationTest < ActiveSupport::TestCase custom_version = "2025-03-27" config.protocol_version = custom_version end - assert_equal("protocol_version must be 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05", exception.message) + 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 @@ -73,12 +73,11 @@ class ConfigurationTest < ActiveSupport::TestCase refute Configuration.modern_protocol_version?("2025-11-25") end - test "raises ArgumentError when setting a modern protocol version" do - # Modern versions (SEP-2575) are never negotiated via `initialize`, so they are deliberately not settable as - # the legacy fallback version. - assert_raises(ArgumentError) do - Configuration.new(protocol_version: Configuration::LATEST_MODERN_PROTOCOL_VERSION) - 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 diff --git a/test/mcp/server/transports/streamable_http_transport_test.rb b/test/mcp/server/transports/streamable_http_transport_test.rb index 099686e1..4912cebf 100644 --- a/test/mcp/server/transports/streamable_http_transport_test.rb +++ b/test/mcp/server/transports/streamable_http_transport_test.rb @@ -1993,6 +1993,32 @@ def string assert_equal 200, response[0] end + test "POST request with the 2026-07-28 MCP-Protocol-Version header succeeds" do + init_request = create_rack_request( + "POST", + "/", + { "CONTENT_TYPE" => "application/json" }, + { jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params(protocolVersion: "2026-07-28") }.to_json, + ) + init_response = @transport.handle_request(init_request) + assert_equal "2026-07-28", JSON.parse(init_response[2][0])["result"]["protocolVersion"] + session_id = init_response[1]["mcp-session-id"] + + request = create_rack_request( + "POST", + "/", + { + "CONTENT_TYPE" => "application/json", + "HTTP_MCP_SESSION_ID" => session_id, + "HTTP_MCP_PROTOCOL_VERSION" => "2026-07-28", + }, + { jsonrpc: "2.0", method: "tools/list", id: "list" }.to_json, + ) + + response = @transport.handle_request(request) + assert_equal 200, response[0] + end + test "POST request without MCP-Protocol-Version header succeeds" do init_request = create_rack_request( "POST", diff --git a/test/mcp/server_test.rb b/test/mcp/server_test.rb index c51c2109..4d9c5b05 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -2036,6 +2036,20 @@ def read_resource_request(uri) assert_equal Configuration::LATEST_STABLE_PROTOCOL_VERSION, response[:result][:protocolVersion] end + test "server negotiates 2026-07-28 when the client requests it via initialize" do + server = Server.new(name: "test_server") + + request = { + jsonrpc: "2.0", + method: "initialize", + id: 1, + params: initialize_params(protocolVersion: "2026-07-28"), + } + + response = server.handle(request) + assert_equal "2026-07-28", response[:result][:protocolVersion] + end + test "server removes description and icons from server_info when negotiating to 2025-06-18" do server = Server.new( name: "test_server",