Merge pull request #440 from koic/mcp_2026_07_28_spec_associate_server_requests
Associate server-to-client requests with the originating client request per SEP-2260
This commit is contained in:
commit
a793dde6cd
8 changed files with 323 additions and 16 deletions
|
|
@ -1172,6 +1172,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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2891,6 +2891,67 @@ module MCP
|
|||
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", params: initialize_params }.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(
|
||||
|
|
|
|||
|
|
@ -57,6 +57,87 @@ module MCP
|
|||
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
|
||||
|
|
|
|||
|
|
@ -48,8 +48,52 @@ module MCP
|
|||
@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 @@ module MCP
|
|||
|
||||
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 @@ module MCP
|
|||
|
||||
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 @@ module MCP
|
|||
@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 @@ module MCP
|
|||
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 @@ module MCP
|
|||
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ module MCP
|
|||
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)
|
||||
|
||||
|
|
@ -119,11 +119,31 @@ module MCP
|
|||
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)
|
||||
|
||||
|
|
@ -131,7 +151,7 @@ module MCP
|
|||
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
|
||||
|
|
@ -195,7 +215,7 @@ module MCP
|
|||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ module MCP
|
|||
|
||||
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 @@ module MCP
|
|||
|
||||
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 @@ module MCP
|
|||
|
||||
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 @@ module MCP
|
|||
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 @@ module MCP
|
|||
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 @@ module MCP
|
|||
@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 @@ module MCP
|
|||
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 @@ module MCP
|
|||
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 @@ module MCP
|
|||
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 @@ module MCP
|
|||
|
||||
# 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 @@ module MCP
|
|||
|
||||
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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue