Validate Required Params for initialize and resources/subscribe

## Motivation and Context

Fixes https://github.com/modelcontextprotocol/ruby-sdk/issues/450

The MCP schema requires `protocolVersion`, `capabilities`, and `clientInfo` on `initialize` params,
and `uri` on `resources/subscribe` and `resources/unsubscribe` params. The server accepted requests with
these fields absent: `initialize` silently negotiated the server's default protocol version,
and `resources/subscribe` returned an empty success result. Both now return `-32602` Invalid params,
matching the Python SDK, which rejects such requests during pydantic validation.
The `uri` check runs before dispatch so it also covers handlers registered via `resources_subscribe_handler`
and `resources_unsubscribe_handler`.

A `protocolVersion` value that is present but unsupported still negotiates the server's fallback version,
as the spec's lifecycle section requires.

## How Has This Been Tested?

Reproduced the issue's stdio repro steps: both malformed requests now return `-32602` instead of succeeding.

## Breaking Changes

Clients that send `initialize` without `protocolVersion`, `capabilities`, or `clientInfo`,
or `resources/subscribe` / `resources/unsubscribe` without `uri`, now receive `-32602` Invalid params
instead of a success response. Such requests violate the MCP schema, but any code relying on
the previous lenient behavior (for example, test harnesses sending bare `initialize` requests)
must now include the required fields.
This commit is contained in:
Koichi ITO 2026-07-11 22:30:25 +09:00
commit abd1070cfa
3 changed files with 369 additions and 219 deletions

View file

@ -108,6 +108,7 @@ module MCP
id: "1",
params: {
protocolVersion: "2025-11-25",
capabilities: {},
clientInfo: { name: "stdio-client", version: "1.0" },
},
}
@ -140,6 +141,7 @@ module MCP
id: "first",
params: {
protocolVersion: "2025-11-25",
capabilities: {},
clientInfo: { name: "original", version: "1.0" },
},
}
@ -149,6 +151,7 @@ module MCP
id: "second",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "intruder", version: "9.9" },
},
}

View file

@ -7,6 +7,8 @@ module MCP
class Server
module Transports
class StreamableHTTPNotificationIntegrationTest < ActiveSupport::TestCase
include InitializeParamsTestHelper
setup do
@server = Server.new(
name: "test_server",
@ -23,7 +25,7 @@ module MCP
"POST",
"/",
{ "CONTENT_TYPE" => "application/json" },
{ jsonrpc: "2.0", method: "initialize", id: "init" }.to_json,
{ jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json,
)
init_response = @transport.handle_request(init_request)
session_id = init_response[1]["Mcp-Session-Id"]
@ -72,7 +74,7 @@ module MCP
"POST",
"/",
{ "CONTENT_TYPE" => "application/json" },
{ jsonrpc: "2.0", method: "initialize", id: "123" }.to_json,
{ jsonrpc: "2.0", method: "initialize", id: "123", params: initialize_params }.to_json,
)
init_response1 = @transport.handle_request(init_request1)
session_id1 = init_response1[1]["Mcp-Session-Id"]
@ -81,7 +83,7 @@ module MCP
"POST",
"/",
{ "CONTENT_TYPE" => "application/json" },
{ jsonrpc: "2.0", method: "initialize", id: "456" }.to_json,
{ jsonrpc: "2.0", method: "initialize", id: "456", params: initialize_params }.to_json,
)
init_response2 = @transport.handle_request(init_request2)
session_id2 = init_response2[1]["Mcp-Session-Id"]
@ -127,7 +129,7 @@ module MCP
"POST",
"/",
{ "CONTENT_TYPE" => "application/json" },
{ jsonrpc: "2.0", method: "initialize", id: "init" }.to_json,
{ jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json,
)
init_response = @transport.handle_request(init_request)
session_id = init_response[1]["Mcp-Session-Id"]
@ -163,7 +165,7 @@ module MCP
"POST",
"/",
{ "CONTENT_TYPE" => "application/json" },
{ jsonrpc: "2.0", method: "initialize", id: "init" }.to_json,
{ jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json,
)
init_response = @transport.handle_request(init_request)
session_id = init_response[1]["Mcp-Session-Id"]
@ -207,7 +209,7 @@ module MCP
"POST",
"/",
{ "CONTENT_TYPE" => "application/json" },
{ jsonrpc: "2.0", method: "initialize", id: "init" }.to_json,
{ jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json,
)
init_response = @transport.handle_request(init_request)
session_id = init_response[1]["Mcp-Session-Id"]

File diff suppressed because it is too large Load diff