Associate server-to-client requests with the originating client request per SEP-2260
## Motivation and Context SEP-2260 (modelcontextprotocol/modelcontextprotocol#2260, merged for the 2026-07-28 spec release) requires servers to send `roots/list`, `sampling/createMessage`, and `elicitation/create` only in association with an originating client request (`ping` is exempt); standalone server-initiated requests on independent streams must not be implemented. On the Streamable HTTP transport, an associated request rides the originating POST's SSE response stream instead of the standalone GET stream. The Ruby SDK's `ServerContext` already satisfies the requirement for handler-scoped calls: `list_roots`, `create_sampling_message`, `create_form_elicitation`, and `create_url_elicitation` stamp the originating request id as `related_request_id`, and because the literal keyword appears after `**kwargs`, a caller-supplied `related_request_id:` is overridden. That behavior was previously untested and undocumented. This change pins it down and adds a migration signal for direct `ServerSession` calls, mirroring the TypeScript SDK's approach (typescript-sdk#2228, non-overridable `relatedRequestId` stamping on ctx-scoped requests): - `ServerContext` documents the non-overridable stamping, and new tests lock it in for all four request helpers. - `ServerSession#list_roots`, `#create_sampling_message`, `#create_form_elicitation`, and `#create_url_elicitation` emit a deprecation warning when called without `related_request_id:` (the call still goes out unchanged, on the GET stream for Streamable HTTP). `ServerSession#ping` is exempt per the SEP. Hard enforcement is deferred to a future protocol-version gate for 2026-07-28. - A new transport test verifies that `send_request` with `related_request_id` delivers on the originating POST stream and never falls back to the GET stream. Resolves #381. ## How Has This Been Tested? - `test/mcp/server_context_test.rb`: `list_roots` stamps the originating request id; `create_sampling_message`, `create_form_elicitation`, and `create_url_elicitation` stamp it non-overridably (a caller-supplied `related_request_id: "attacker"` is replaced). - `test/mcp/server_roots_test.rb`, `test/mcp/server_sampling_test.rb`, `test/mcp/server_elicitation_test.rb`: each `ServerSession` method warns (matching /SEP-2260/) without `related_request_id:` and stays silent with it. The warning assertions temporarily set `$VERBOSE = false` because the rake test task runs with `-W0`, following the existing `assert_implicit_connect_deprecation_warning` precedent. - `test/mcp/server/transports/streamable_http_transport_test.rb`: a `roots/list` request with `related_request_id` is written to the registered POST request stream and not to the GET SSE stream. - Existing direct-call test sites were updated to pass `related_request_id:` (or capture the warning) so the suite output stays clean; their assertions are unchanged. ## Breaking Changes None. Behavior is unchanged; direct `ServerSession` calls without `related_request_id:` now emit a deprecation warning but are still sent exactly as before.
This commit is contained in:
parent
d8ba1a84ff
commit
6ddcc49865
2 changed files with 323 additions and 16 deletions
|
|
@ -54,6 +54,9 @@ module MCP
|
|||
end
|
||||
|
||||
# Delegates to the session so the request is scoped to the originating client.
|
||||
# The originating request id is stamped as `related_request_id`, satisfying
|
||||
# SEP-2260's requirement that server-to-client requests be associated with
|
||||
# the client request being processed.
|
||||
# @deprecated MCP Roots (`roots/list` and
|
||||
# `notifications/roots/list_changed`) is deprecated as of MCP protocol
|
||||
# version 2026-07-28 (SEP-2577). Use tool parameters, resource URIs,
|
||||
|
|
@ -91,6 +94,11 @@ module MCP
|
|||
# Delegates to the session so the request is scoped to the originating client.
|
||||
# Falls back to `@context` (via `method_missing`) when `@notification_target`
|
||||
# does not support sampling.
|
||||
#
|
||||
# The originating request id is stamped as `related_request_id` and cannot be
|
||||
# overridden by callers (the literal keyword after `**kwargs` wins),
|
||||
# satisfying SEP-2260's requirement that server-to-client requests be
|
||||
# associated with the client request being processed.
|
||||
# @deprecated MCP Sampling (`sampling/createMessage`) is deprecated as of
|
||||
# MCP protocol version 2026-07-28 (SEP-2577). Use direct LLM provider
|
||||
# APIs instead.
|
||||
|
|
@ -107,6 +115,8 @@ module MCP
|
|||
# Delegates to the session so the request is scoped to the originating client.
|
||||
# Falls back to `@context` (via `method_missing`) when `@notification_target`
|
||||
# does not support elicitation.
|
||||
# The originating request id is stamped as a non-overridable
|
||||
# `related_request_id`, as with `create_sampling_message` (SEP-2260).
|
||||
def create_form_elicitation(**kwargs)
|
||||
if @notification_target.respond_to?(:create_form_elicitation)
|
||||
@notification_target.create_form_elicitation(**kwargs, related_request_id: @related_request_id)
|
||||
|
|
@ -119,6 +129,8 @@ module MCP
|
|||
|
||||
# Delegates to the session so the request is scoped to the originating client.
|
||||
# Falls back to `@context` when `@notification_target` does not support URL mode elicitation.
|
||||
# The originating request id is stamped as a non-overridable `related_request_id`,
|
||||
# as with `create_sampling_message` (SEP-2260).
|
||||
def create_url_elicitation(**kwargs)
|
||||
if @notification_target.respond_to?(:create_url_elicitation)
|
||||
@notification_target.create_url_elicitation(**kwargs, related_request_id: @related_request_id)
|
||||
|
|
|
|||
|
|
@ -98,11 +98,17 @@ module MCP
|
|||
end
|
||||
|
||||
# Sends a `roots/list` request scoped to this session.
|
||||
#
|
||||
# Per SEP-2260, the request must be associated with an originating client
|
||||
# request; prefer `server_context.list_roots` inside a handler, which stamps
|
||||
# the association automatically.
|
||||
# @deprecated MCP Roots (`roots/list` and
|
||||
# `notifications/roots/list_changed`) is deprecated as of MCP protocol
|
||||
# version 2026-07-28 (SEP-2577). Use tool parameters, resource URIs,
|
||||
# server configuration, or environment variables instead.
|
||||
def list_roots(related_request_id: nil)
|
||||
warn_unassociated_request(__method__, related_request_id)
|
||||
|
||||
unless client_capabilities&.dig(:roots)
|
||||
raise "Client does not support roots."
|
||||
end
|
||||
|
|
@ -119,16 +125,28 @@ module MCP
|
|||
end
|
||||
|
||||
# Sends a `sampling/createMessage` request scoped to this session.
|
||||
#
|
||||
# Per SEP-2260, the request must be associated with an originating client
|
||||
# request; prefer `server_context.create_sampling_message` inside a handler,
|
||||
# which stamps the association automatically.
|
||||
# @deprecated MCP Sampling (`sampling/createMessage`) is deprecated as of
|
||||
# MCP protocol version 2026-07-28 (SEP-2577). Use direct LLM provider
|
||||
# APIs instead.
|
||||
def create_sampling_message(related_request_id: nil, **kwargs)
|
||||
warn_unassociated_request(__method__, related_request_id)
|
||||
|
||||
params = @server.build_sampling_params(client_capabilities, **kwargs)
|
||||
send_to_transport_request(Methods::SAMPLING_CREATE_MESSAGE, params, related_request_id: related_request_id)
|
||||
end
|
||||
|
||||
# Sends an `elicitation/create` request (form mode) scoped to this session.
|
||||
#
|
||||
# Per SEP-2260, the request must be associated with an originating client
|
||||
# request; prefer `server_context.create_form_elicitation` inside a handler,
|
||||
# which stamps the association automatically.
|
||||
def create_form_elicitation(message:, requested_schema:, related_request_id: nil)
|
||||
warn_unassociated_request(__method__, related_request_id)
|
||||
|
||||
unless client_capabilities&.dig(:elicitation)
|
||||
raise "Client does not support elicitation. " \
|
||||
"The client must declare the `elicitation` capability during initialization."
|
||||
|
|
@ -139,7 +157,13 @@ module MCP
|
|||
end
|
||||
|
||||
# Sends an `elicitation/create` request (URL mode) scoped to this session.
|
||||
#
|
||||
# Per SEP-2260, the request must be associated with an originating client
|
||||
# request; prefer `server_context.create_url_elicitation` inside a handler,
|
||||
# which stamps the association automatically.
|
||||
def create_url_elicitation(message:, url:, elicitation_id:, related_request_id: nil)
|
||||
warn_unassociated_request(__method__, related_request_id)
|
||||
|
||||
unless client_capabilities&.dig(:elicitation, :url)
|
||||
raise "Client does not support URL mode elicitation. " \
|
||||
"The client must declare the `elicitation.url` capability during initialization."
|
||||
|
|
@ -264,5 +288,21 @@ module MCP
|
|||
# The explicit splat suppresses that conversion and is a no-op when `supported` is empty.
|
||||
transport_method.call(method, params, **supported)
|
||||
end
|
||||
|
||||
# Per SEP-2260, servers MUST send `roots/list`, `sampling/createMessage`, and `elicitation/create` only
|
||||
# in association with an originating client request (`ping` is exempt). A request without `related_request_id:` is
|
||||
# delivered on the standalone GET stream by the Streamable HTTP transport, which the 2026-07-28 spec forbids;
|
||||
# warn so callers migrate to the `server_context` helpers before this becomes an error.
|
||||
# https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2260
|
||||
def warn_unassociated_request(method_name, related_request_id)
|
||||
return if related_request_id
|
||||
|
||||
warn(
|
||||
"Calling `ServerSession##{method_name}` without `related_request_id:` is deprecated (SEP-2260): " \
|
||||
"server-to-client #{method_name} requests must be associated with an originating client request. " \
|
||||
"Use `server_context.#{method_name}` inside a handler, or pass `related_request_id:` explicitly.",
|
||||
uplevel: 2,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue