Support client-side sampling via MCP::Client#on_sampling
## Motivation and Context The MCP specification defines `sampling/createMessage` as a server-to-client request that lets a server ask the client to generate an LLM completion, so the server can leverage the client's model access without its own API keys. The Ruby SDK already supports the server side (sending the request) and, on the client side, the generic server-to-client request infrastructure introduced for elicitation (the standalone GET SSE listening stream and `on_server_request` dispatch). This adds the client-side `sampling/createMessage` handler on top of that infrastructure. ## Changes - Add `MCP::Client#on_sampling`, a thin wrapper that registers a `sampling/createMessage` handler via `transport.on_server_request`, mirroring `on_elicitation`. The handler receives the request params and returns a `CreateMessageResult`-shaped Hash sent back as the JSON-RPC result. - Document client-side sampling in the README, including the `sampling` (and `sampling.tools`) capability declaration and the human-in-the-loop guidance from the specification. ## How Has This Been Tested? - Unit tests for `on_sampling` covering handler registration on the transport and the error raised when the transport does not support server-to-client requests. - An HTTP transport test that dispatches a `sampling/createMessage` server request delivered on the SSE stream and returns a `CreateMessageResult`. ## Deprecation Note SEP-2577 deprecates sampling (along with roots and logging) starting with protocol version `2026-07-28`. The deprecation warnings are added in https://github.com/modelcontextprotocol/ruby-sdk/pull/406. Sampling remains fully supported under the current `2025-11-25` protocol version and stays available throughout the spec's deprecation window, so the client side is implemented here for parity with the server side and with the Python and TypeScript SDKs. `MCP::Client#on_sampling` carries the same `@deprecated` annotation as the server-side senders annotated in #429, tailored to the receiving side: the handler exists to interoperate with servers that still send sampling requests during the deprecation window. The README section carries the same note. Ref: https://modelcontextprotocol.io/specification/2025-11-25/client/sampling
This commit is contained in:
parent
1101900f31
commit
f3d6da2371
2 changed files with 156 additions and 0 deletions
|
|
@ -913,6 +913,67 @@ module MCP
|
|||
assert_equal("Please accept with defaults", received_params["message"])
|
||||
end
|
||||
|
||||
def test_send_request_dispatches_sampling_request_to_registered_handler
|
||||
request = {
|
||||
jsonrpc: "2.0",
|
||||
id: "test_id",
|
||||
method: "tools/call",
|
||||
params: { name: "ask_llm", arguments: {} },
|
||||
}
|
||||
|
||||
sampling_request = {
|
||||
jsonrpc: "2.0",
|
||||
id: 0,
|
||||
method: "sampling/createMessage",
|
||||
params: {
|
||||
messages: [{ role: "user", content: { type: "text", text: "Hi" } }],
|
||||
maxTokens: 100,
|
||||
},
|
||||
}
|
||||
tool_result = { jsonrpc: "2.0", id: "test_id", result: { content: [] } }
|
||||
sse_body = "event: message\ndata: #{sampling_request.to_json}\n\n" \
|
||||
"event: message\ndata: #{tool_result.to_json}\n\n"
|
||||
|
||||
stub_request(:post, url).with(
|
||||
body: request.to_json,
|
||||
).to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "text/event-stream" },
|
||||
body: sse_body,
|
||||
)
|
||||
|
||||
expected_response = {
|
||||
jsonrpc: "2.0",
|
||||
id: 0,
|
||||
result: {
|
||||
role: "assistant",
|
||||
content: { type: "text", text: "Hello there" },
|
||||
model: "test-model",
|
||||
stopReason: "endTurn",
|
||||
},
|
||||
}
|
||||
response_stub = stub_request(:post, url).with(
|
||||
body: expected_response.to_json,
|
||||
).to_return(status: 202, body: "")
|
||||
|
||||
received_params = nil
|
||||
client.on_server_request("sampling/createMessage") do |params|
|
||||
received_params = params
|
||||
{
|
||||
role: "assistant",
|
||||
content: { type: "text", text: "Hello there" },
|
||||
model: "test-model",
|
||||
stopReason: "endTurn",
|
||||
}
|
||||
end
|
||||
|
||||
response = client.send_request(request: request)
|
||||
|
||||
assert_equal({ "content" => [] }, response["result"])
|
||||
assert_requested(response_stub)
|
||||
assert_equal(100, received_params["maxTokens"])
|
||||
end
|
||||
|
||||
def test_send_request_answers_unregistered_server_request_with_method_not_found
|
||||
request = {
|
||||
jsonrpc: "2.0",
|
||||
|
|
|
|||
|
|
@ -66,6 +66,25 @@ module MCP
|
|||
assert_includes(error.message, "does not support server-to-client requests")
|
||||
end
|
||||
|
||||
def test_on_sampling_registers_handler_on_transport
|
||||
transport = mock
|
||||
handler = proc { { role: "assistant", content: { type: "text", text: "hi" } } }
|
||||
transport.expects(:on_server_request).with("sampling/createMessage")
|
||||
|
||||
Client.new(transport: transport).on_sampling(&handler)
|
||||
end
|
||||
|
||||
def test_on_sampling_raises_when_transport_does_not_support_server_requests
|
||||
transport = mock
|
||||
transport.stubs(:respond_to?).with(:on_server_request).returns(false)
|
||||
|
||||
error = assert_raises(ArgumentError) do
|
||||
Client.new(transport: transport).on_sampling { { role: "assistant", content: { type: "text", text: "hi" } } }
|
||||
end
|
||||
|
||||
assert_includes(error.message, "does not support server-to-client requests")
|
||||
end
|
||||
|
||||
def test_connected_delegates_to_transport_when_supported
|
||||
transport = mock
|
||||
transport.expects(:connected?).returns(true)
|
||||
|
|
|
|||
Loading…
Reference in a new issue