From 9c90cc71ffd32ca13ac5058c34a7e42831d15d56 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Fri, 19 Jun 2026 03:10:54 +0900 Subject: [PATCH] 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. --- README.md | 6 ++ lib/mcp/server_context.rb | 12 +++ lib/mcp/server_session.rb | 40 +++++++++ .../streamable_http_transport_test.rb | 61 ++++++++++++++ test/mcp/server_context_test.rb | 81 +++++++++++++++++++ test/mcp/server_elicitation_test.rb | 49 +++++++++++ test/mcp/server_roots_test.rb | 28 ++++++- test/mcp/server_sampling_test.rb | 62 +++++++++++--- 8 files changed, 323 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b986c262..91b46736 100644 --- a/README.md +++ b/README.md @@ -1023,6 +1023,12 @@ Roots define the boundaries of where a server can operate, providing a list of d - **Client Capability**: Clients must declare `roots` capability during initialization - **Change Notifications**: Clients that support `roots.listChanged` send `notifications/roots/list_changed` when roots change +> [!NOTE] +> Per SEP-2260, server-to-client requests (`roots/list`, `sampling/createMessage`, `elicitation/create`) must be associated with +> an originating client request (`ping` is exempt). Use the `server_context` passed to your handler, which stamps the association +> automatically and routes the request onto the originating POST stream on the Streamable HTTP transport. Calling the corresponding +> `ServerSession` methods without `related_request_id:` still works but emits a deprecation warning. + **Using Roots in Tools:** Tools that accept a `server_context:` parameter can call `list_roots` on it. diff --git a/lib/mcp/server_context.rb b/lib/mcp/server_context.rb index 92b1abd6..dc5b20a5 100644 --- a/lib/mcp/server_context.rb +++ b/lib/mcp/server_context.rb @@ -54,6 +54,9 @@ def notify_resources_updated(uri:) 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 @@ def ping # 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 @@ def create_sampling_message(**kwargs) # 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 @@ def create_form_elicitation(**kwargs) # 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) diff --git a/lib/mcp/server_session.rb b/lib/mcp/server_session.rb index 502c6163..754c1d00 100644 --- a/lib/mcp/server_session.rb +++ b/lib/mcp/server_session.rb @@ -98,11 +98,17 @@ def client_capabilities 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 @@ def ping(related_request_id: nil) 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 @@ def create_form_elicitation(message:, requested_schema:, related_request_id: nil 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 @@ def forward_to_transport(transport_method, method, params, kwargs) # 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 diff --git a/test/mcp/server/transports/streamable_http_transport_test.rb b/test/mcp/server/transports/streamable_http_transport_test.rb index 9b26949a..6b0789fa 100644 --- a/test/mcp/server/transports/streamable_http_transport_test.rb +++ b/test/mcp/server/transports/streamable_http_transport_test.rb @@ -2801,6 +2801,67 @@ def string assert_equal "Hi there", result[:content][:text] end + test "send_request with related_request_id rides the originating POST stream, not the GET stream" do + # Per SEP-2260, server-to-client requests associated with a client request are delivered on + # that request's POST SSE response stream rather than the standalone GET stream. + init_request = create_rack_request( + "POST", + "/", + { "CONTENT_TYPE" => "application/json" }, + { jsonrpc: "2.0", method: "initialize", id: "init" }.to_json, + ) + init_response = @transport.handle_request(init_request) + session_id = init_response[1]["Mcp-Session-Id"] + + # Connect GET SSE. + get_io = StringIO.new + get_request = create_rack_request( + "GET", + "/", + { "HTTP_MCP_SESSION_ID" => session_id }, + ) + response = @transport.handle_request(get_request) + response[2].call(get_io) if response[2].is_a?(Proc) + sleep(0.1) + + # Register an in-flight POST request stream for "req-1". + post_io = StringIO.new + @transport.instance_variable_get(:@sessions)[session_id][:post_request_streams] = { "req-1" => post_io } + + result_queue = Queue.new + Thread.new do + result = @transport.send_request( + "roots/list", + nil, + session_id: session_id, + related_request_id: "req-1", + ) + result_queue.push(result) + end + + sleep(0.1) + + post_io.rewind + post_output = post_io.read + assert_includes post_output, "roots/list" + + get_io.rewind + refute_includes get_io.read, "roots/list" + + # Respond so the background send_request completes. + data_lines = post_output.lines.select { |line| line.start_with?("data: ") } + request_id = JSON.parse(data_lines.first.sub("data: ", ""))["id"] + client_response = create_rack_request( + "POST", + "/", + { "CONTENT_TYPE" => "application/json", "HTTP_MCP_SESSION_ID" => session_id }, + { jsonrpc: "2.0", id: request_id, result: { roots: [] } }.to_json, + ) + @transport.handle_request(client_response) + + assert_equal({ roots: [] }, result_queue.pop) + end + test "send_request ignores response from wrong session" do # Create two sessions. init_a = create_rack_request( diff --git a/test/mcp/server_context_test.rb b/test/mcp/server_context_test.rb index 44fa12e4..91258052 100644 --- a/test/mcp/server_context_test.rb +++ b/test/mcp/server_context_test.rb @@ -57,6 +57,87 @@ class ServerContextTest < ActiveSupport::TestCase assert_equal [{ uri: "file:///project", name: "Project" }], result[:roots] end + test "ServerContext#list_roots stamps the originating request id" do + # Per SEP-2260, server-to-client requests are associated with the originating client request automatically. + notification_target = mock + notification_target.expects(:list_roots).with(related_request_id: "req-1").returns({ roots: [] }) + + progress = Progress.new(notification_target: notification_target, progress_token: nil) + server_context = ServerContext.new( + mock, + progress: progress, + notification_target: notification_target, + related_request_id: "req-1", + ) + + server_context.list_roots + end + + test "ServerContext#create_sampling_message stamps related_request_id non-overridably" do + # A caller-supplied `related_request_id:` must not detach the request from its originating client request (SEP-2260): + # the literal keyword after `**kwargs` wins. + notification_target = mock + notification_target.expects(:create_sampling_message).with( + messages: [], + max_tokens: 5, + related_request_id: "req-1", + ).returns({}) + + progress = Progress.new(notification_target: notification_target, progress_token: nil) + server_context = ServerContext.new( + mock, + progress: progress, + notification_target: notification_target, + related_request_id: "req-1", + ) + + server_context.create_sampling_message(messages: [], max_tokens: 5, related_request_id: "attacker") + end + + test "ServerContext#create_form_elicitation stamps related_request_id non-overridably" do + notification_target = mock + notification_target.expects(:create_form_elicitation).with( + message: "hi", + requested_schema: {}, + related_request_id: "req-1", + ).returns({}) + + progress = Progress.new(notification_target: notification_target, progress_token: nil) + server_context = ServerContext.new( + mock, + progress: progress, + notification_target: notification_target, + related_request_id: "req-1", + ) + + server_context.create_form_elicitation(message: "hi", requested_schema: {}, related_request_id: "attacker") + end + + test "ServerContext#create_url_elicitation stamps related_request_id non-overridably" do + notification_target = mock + notification_target.expects(:create_url_elicitation).with( + message: "hi", + url: "https://example.com", + elicitation_id: "el-1", + related_request_id: "req-1", + ).returns({}) + + progress = Progress.new(notification_target: notification_target, progress_token: nil) + server_context = ServerContext.new( + mock, + progress: progress, + notification_target: notification_target, + related_request_id: "req-1", + ) + + server_context.create_url_elicitation( + message: "hi", + url: "https://example.com", + elicitation_id: "el-1", + related_request_id: "attacker", + ) + end + test "ServerContext#list_roots raises NoMethodError when notification_target does not respond" do notification_target = mock context = mock diff --git a/test/mcp/server_elicitation_test.rb b/test/mcp/server_elicitation_test.rb index 66a70f42..03f36090 100644 --- a/test/mcp/server_elicitation_test.rb +++ b/test/mcp/server_elicitation_test.rb @@ -48,8 +48,52 @@ def handle_request(request); end @session.instance_variable_set(:@client_capabilities, { elicitation: {} }) end + test "#create_form_elicitation warns when called without related_request_id" do + # Per SEP-2260, server-to-client requests must be associated with an originating client request. + # `$VERBOSE = false` because the rake test task runs with `-W0`, under which `Kernel#warn` emits nothing. + original_verbose = $VERBOSE + $VERBOSE = false + + assert_output(nil, /SEP-2260/) do + @session.create_form_elicitation( + message: "Please provide your name", + requested_schema: { type: "object", properties: { name: { type: "string" } } }, + ) + end + ensure + $VERBOSE = original_verbose + end + + test "#create_url_elicitation warns when called without related_request_id" do + @session.instance_variable_set(:@client_capabilities, { elicitation: { url: true } }) + + original_verbose = $VERBOSE + $VERBOSE = false + + assert_output(nil, /SEP-2260/) do + @session.create_url_elicitation( + message: "Please sign in", + url: "https://example.com/signin", + elicitation_id: "el-1", + ) + end + ensure + $VERBOSE = original_verbose + end + + test "#create_form_elicitation does not warn when related_request_id is given" do + assert_silent do + @session.create_form_elicitation( + related_request_id: "req-1", + message: "Please provide your name", + requested_schema: { type: "object", properties: { name: { type: "string" } } }, + ) + end + end + test "#create_form_elicitation sends request through transport" do result = @session.create_form_elicitation( + related_request_id: "req-1", message: "Please provide your name", requested_schema: { type: "object", @@ -76,6 +120,7 @@ def handle_request(request); end error = assert_raises(RuntimeError) do @session.create_form_elicitation( + related_request_id: "req-1", message: "Please provide your name", requested_schema: { type: "object", properties: { name: { type: "string" } } }, ) @@ -93,6 +138,7 @@ def handle_request(request); end error = assert_raises(RuntimeError) do @session.create_form_elicitation( + related_request_id: "req-1", message: "Please provide your name", requested_schema: { type: "object", properties: { name: { type: "string" } } }, ) @@ -108,6 +154,7 @@ def handle_request(request); end @session.instance_variable_set(:@client_capabilities, { elicitation: { url: {} } }) result = @session.create_url_elicitation( + related_request_id: "req-1", message: "Please authorize access", url: "https://example.com/oauth", elicitation_id: "abc-123", @@ -128,6 +175,7 @@ def handle_request(request); end test "#create_url_elicitation raises error when client does not support url mode" do error = assert_raises(RuntimeError) do @session.create_url_elicitation( + related_request_id: "req-1", message: "Please authorize access", url: "https://example.com/oauth", elicitation_id: "abc-123", @@ -146,6 +194,7 @@ def handle_request(request); end error = assert_raises(RuntimeError) do @session.create_url_elicitation( + related_request_id: "req-1", message: "Please authorize access", url: "https://example.com/oauth", elicitation_id: "abc-123", diff --git a/test/mcp/server_roots_test.rb b/test/mcp/server_roots_test.rb index c887398f..da448165 100644 --- a/test/mcp/server_roots_test.rb +++ b/test/mcp/server_roots_test.rb @@ -110,7 +110,7 @@ def close; end transport.instance_variable_get(:@sessions)["s1"] = { stream: nil, server_session: session_with_roots } error_with_roots = assert_raises(RuntimeError) do - session_with_roots.list_roots + capture_io { session_with_roots.list_roots } end assert_equal("No active stream for roots/list request.", error_with_roots.message) @@ -118,11 +118,31 @@ def close; end session_without_roots.store_client_info(client: { name: "incapable" }, capabilities: {}) error = assert_raises(RuntimeError) do - session_without_roots.list_roots + capture_io { session_without_roots.list_roots } end assert_equal("Client does not support roots.", error.message) end + test "ServerSession#list_roots warns when called without related_request_id" do + # Per SEP-2260, server-to-client requests must be associated with an originating client request. + # `$VERBOSE = false` because the rake test task runs with `-W0`, under which `Kernel#warn` emits nothing. + session = ServerSession.new(server: @server, transport: @mock_transport) + session.store_client_info(client: { name: "capable" }, capabilities: { roots: {} }) + + original_verbose = $VERBOSE + $VERBOSE = false + assert_output(nil, /SEP-2260/) { session.list_roots } + ensure + $VERBOSE = original_verbose + end + + test "ServerSession#list_roots does not warn when related_request_id is given" do + session = ServerSession.new(server: @server, transport: @mock_transport) + session.store_client_info(client: { name: "capable" }, capabilities: { roots: {} }) + + assert_silent { session.list_roots(related_request_id: "req-1") } + end + test "ServerSession#client_capabilities falls back to server global capabilities" do transport = MCP::Server::Transports::StreamableHTTPTransport.new(@server) @@ -130,7 +150,7 @@ def close; end transport.instance_variable_get(:@sessions)["s3"] = { stream: nil, server_session: session } error = assert_raises(RuntimeError) do - session.list_roots + capture_io { session.list_roots } end assert_equal("No active stream for roots/list request.", error.message) end @@ -194,7 +214,7 @@ def close; end transport.instance_variable_get(:@sessions)["s1"] = { stream: nil, server_session: session } error = assert_raises(RuntimeError) do - session.list_roots + capture_io { session.list_roots } end assert_equal("No active stream for roots/list request.", error.message) end diff --git a/test/mcp/server_sampling_test.rb b/test/mcp/server_sampling_test.rb index dd123564..9c6985b3 100644 --- a/test/mcp/server_sampling_test.rb +++ b/test/mcp/server_sampling_test.rb @@ -48,6 +48,7 @@ def close; end test "create_sampling_message sends request with required params" do result = @session.create_sampling_message( + related_request_id: "req-1", messages: [{ role: "user", content: { type: "text", text: "Hello" } }], max_tokens: 100, ) @@ -64,6 +65,7 @@ def close; end test "create_sampling_message sends all optional params" do @session.create_sampling_message( + related_request_id: "req-1", messages: [{ role: "user", content: { type: "text", text: "Hello" } }], max_tokens: 100, system_prompt: "You are helpful", @@ -90,6 +92,7 @@ def close; end error = assert_raises(RuntimeError) do @session.create_sampling_message( + related_request_id: "req-1", messages: [{ role: "user", content: { type: "text", text: "Hello" } }], max_tokens: 100, ) @@ -101,6 +104,7 @@ def close; end test "create_sampling_message raises error when tools used but client lacks sampling.tools" do error = assert_raises(RuntimeError) do @session.create_sampling_message( + related_request_id: "req-1", messages: [{ role: "user", content: { type: "text", text: "Hello" } }], max_tokens: 100, tools: [{ name: "test_tool", inputSchema: { type: "object" } }], @@ -113,6 +117,7 @@ def close; end test "create_sampling_message raises error when tool_choice used alone but client lacks sampling.tools" do error = assert_raises(RuntimeError) do @session.create_sampling_message( + related_request_id: "req-1", messages: [{ role: "user", content: { type: "text", text: "Hello" } }], max_tokens: 100, tool_choice: { mode: "auto" }, @@ -126,6 +131,7 @@ def close; end @session.store_client_info(client: { name: "test-client" }, capabilities: { sampling: { tools: {} } }) result = @session.create_sampling_message( + related_request_id: "req-1", messages: [{ role: "user", content: { type: "text", text: "Hello" } }], max_tokens: 100, tools: [{ name: "test_tool", inputSchema: { type: "object" } }], @@ -140,6 +146,31 @@ def close; end assert_equal "Response from LLM", result[:content][:text] end + test "create_sampling_message warns when called without related_request_id" do + # Per SEP-2260, server-to-client requests must be associated with an originating client request. + # `$VERBOSE = false` because the rake test task runs with `-W0`, under which `Kernel#warn` emits nothing. + original_verbose = $VERBOSE + $VERBOSE = false + assert_output(nil, /SEP-2260/) do + @session.create_sampling_message( + messages: [{ role: "user", content: { type: "text", text: "Hello" } }], + max_tokens: 100, + ) + end + ensure + $VERBOSE = original_verbose + end + + test "create_sampling_message does not warn when related_request_id is given" do + assert_silent do + @session.create_sampling_message( + related_request_id: "req-1", + messages: [{ role: "user", content: { type: "text", text: "Hello" } }], + max_tokens: 100, + ) + end + end + test "create_sampling_message uses per-session capabilities via ServerSession" do transport = MCP::Server::Transports::StreamableHTTPTransport.new(@server) @@ -149,10 +180,12 @@ def close; end transport.instance_variable_get(:@sessions)["s1"] = { stream: nil, server_session: session_with_sampling } error_with_sampling = assert_raises(RuntimeError) do - session_with_sampling.create_sampling_message( - messages: [{ role: "user", content: { type: "text", text: "Hello" } }], - max_tokens: 100, - ) + capture_io do + session_with_sampling.create_sampling_message( + messages: [{ role: "user", content: { type: "text", text: "Hello" } }], + max_tokens: 100, + ) + end end assert_equal("No active stream for sampling/createMessage request.", error_with_sampling.message) @@ -161,10 +194,12 @@ def close; end session_without_sampling.store_client_info(client: { name: "incapable" }, capabilities: {}) error = assert_raises(RuntimeError) do - session_without_sampling.create_sampling_message( - messages: [{ role: "user", content: { type: "text", text: "Hello" } }], - max_tokens: 100, - ) + capture_io do + session_without_sampling.create_sampling_message( + messages: [{ role: "user", content: { type: "text", text: "Hello" } }], + max_tokens: 100, + ) + end end assert_equal("Client does not support sampling.", error.message) end @@ -190,10 +225,12 @@ def close; end # Server was initialized with sampling capability, so fallback should pass validation. error = assert_raises(RuntimeError) do - session.create_sampling_message( - messages: [{ role: "user", content: { type: "text", text: "Hello" } }], - max_tokens: 100, - ) + capture_io do + session.create_sampling_message( + messages: [{ role: "user", content: { type: "text", text: "Hello" } }], + max_tokens: 100, + ) + end end assert_equal("No active stream for sampling/createMessage request.", error.message) end @@ -243,6 +280,7 @@ def close; end test "create_sampling_message omits nil optional params" do @session.create_sampling_message( + related_request_id: "req-1", messages: [{ role: "user", content: { type: "text", text: "Hello" } }], max_tokens: 100, system_prompt: nil,