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

View file

@ -531,6 +531,7 @@ module MCP
when Methods::RESOURCES_READ
build_read_resource_result(read_resource_contents(params, session: session, related_request_id: related_request_id, cancellation: cancellation))
when Methods::RESOURCES_SUBSCRIBE, Methods::RESOURCES_UNSUBSCRIBE
validate_resource_subscription_params!(params)
dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation)
{}
when Methods::TOOLS_CALL
@ -605,6 +606,8 @@ module MCP
end
def init(params, session: nil)
validate_initialize_params!(params)
# MCP spec: the initialization phase MUST be the first interaction between client and server.
# Reject duplicate `initialize` on an already-initialized session so the negotiated
# client identity and capabilities cannot be silently overwritten.
@ -612,15 +615,13 @@ module MCP
raise RequestHandlerError.new("Invalid Request: Server already initialized", params, error_type: :invalid_request)
end
if params
if session
session.store_client_info(client: params[:clientInfo], capabilities: params[:capabilities])
else
@client = params[:clientInfo]
@client_capabilities = params[:capabilities]
end
protocol_version = params[:protocolVersion]
if session
session.store_client_info(client: params[:clientInfo], capabilities: params[:capabilities])
else
@client = params[:clientInfo]
@client_capabilities = params[:capabilities]
end
protocol_version = params[:protocolVersion]
negotiated_version = if Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(protocol_version)
protocol_version
@ -649,6 +650,31 @@ module MCP
}.compact
end
def validate_resource_subscription_params!(params)
unless params.is_a?(Hash) && params[:uri].is_a?(String)
raise RequestHandlerError.new("Missing or invalid uri", params, error_type: :invalid_params)
end
end
def validate_initialize_params!(params)
unless params.is_a?(Hash)
raise RequestHandlerError.new("Invalid params", params, error_type: :invalid_params)
end
unless params[:protocolVersion].is_a?(String)
raise RequestHandlerError.new("Missing or invalid protocolVersion", params, error_type: :invalid_params)
end
unless params[:capabilities].is_a?(Hash)
raise RequestHandlerError.new("Missing or invalid capabilities", params, error_type: :invalid_params)
end
client_info = params[:clientInfo]
if !client_info.is_a?(Hash) || client_info[:name].nil? || client_info[:version].nil?
raise RequestHandlerError.new("Missing or invalid clientInfo", params, error_type: :invalid_params)
end
end
# Handles `server/discover` (MCP 2026-07-28 draft, SEP-2575): sessionless capability discovery.
# Unlike `init`, this is state-free and idempotent: it stores no client info, does not mark
# the session initialized, and responds regardless of capability declarations or initialization state,