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.
This commit is contained in:
Koichi ITO 2026-07-31 12:27:58 +09:00
commit dc25610162
6 changed files with 60 additions and 16 deletions

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -54,7 +54,7 @@ module MCP
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 @@ module MCP
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 @@ module MCP
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

View file

@ -1993,6 +1993,32 @@ module MCP
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",

View file

@ -2036,6 +2036,20 @@ module MCP
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",