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:
Koichi ITO 2026-07-15 01:04:38 +09:00
commit f3d6da2371

View file

@ -423,6 +423,44 @@ module MCP
transport.on_server_request(Methods::ELICITATION_CREATE, &handler)
end
# Registers a handler for `sampling/createMessage` requests the server sends while one of this client's requests is in flight.
# The handler receives the request `params` (`messages`, `maxTokens`, optionally `systemPrompt`, `modelPreferences`, `tools`,
# `toolChoice`, ...; string keys) and must return a `CreateMessageResult`-shaped Hash:
# `{ role: "assistant", content: { type: "text", text: "..." }, model: "...", stopReason: "..." }`.
#
# For trust and safety, the spec recommends a human in the loop able to review, edit, or reject the request and the generated response
# before it is returned to the server. To reject, raise `ServerRequestError` with the spec's user-rejection code `-1`.
#
# Requires a transport that supports server-to-client requests (e.g. `MCP::Client::HTTP`); pass `capabilities: { sampling: {} }` to
# `connect` (or `{ sampling: { tools: {} } }` to receive tool-enabled sampling requests) so the server knows it may send them.
#
# @example Forward the request to an LLM and return its completion
#
# client.on_sampling do |params|
# raise MCP::Client::ServerRequestError.new("User rejected sampling request", code: -1) unless approved?(params)
#
# completion = my_llm.complete(params["messages"], max_tokens: params["maxTokens"])
# {
# role: "assistant",
# content: { type: "text", text: completion.text },
# model: completion.model,
# stopReason: "endTurn",
# }
# end
#
# https://modelcontextprotocol.io/specification/2025-11-25/client/sampling
#
# @deprecated MCP Sampling (`sampling/createMessage`) is deprecated as of MCP protocol version 2026-07-28 (SEP-2577).
# Register this handler only to interoperate with servers that still send sampling requests during the deprecation window;
# new servers should call LLM provider APIs directly.
def on_sampling(&handler)
unless transport.respond_to?(:on_server_request)
raise ArgumentError, "The transport does not support server-to-client requests"
end
transport.on_server_request(Methods::SAMPLING_CREATE_MESSAGE, &handler)
end
# Sends a `ping` request to the server to verify the connection is alive.
# Per the MCP spec, the server responds with an empty result.
#