## Motivation and Context The MCP specification defines `sampling/createMessage` to request LLM completions from the client. The Ruby SDK did not yet support server-to-client requests, so the server had no way to ask the client to sample an LLM. This caused the `tools-call-sampling` conformance test to fail. This adds `Server#create_sampling_message` to send a `sampling/createMessage` request to the client via the transport layer. The method validates that the client declared the `sampling` capability during initialization, and optionally checks the `sampling.tools` capability when tools or tool_choice parameters are provided. On the transport side, `Transport#send_request` is introduced as an abstract method, with implementations in both `StdioTransport` and `StreamableHTTPTransport`. Each transport assigns a unique request ID, sends the JSON-RPC request to the client, and blocks until the client responds. Client capabilities are stored per session via `ServerSession`, so validation is scoped to the originating client. `ServerSession#create_sampling_message` uses `build_sampling_params` for capability validation and sends the request directly through the transport with the session ID, following the same pattern as `notify_progress`. `Server#create_sampling_message` is the public API for single-client transports (e.g., StdioTransport). For multi-client transports (e.g., StreamableHTTPTransport), `ServerSession#create_sampling_message` must be used to route requests to the correct client. Tools using `server_context.create_sampling_message` automatically route to the correct session regardless of transport. `StreamableHTTPTransport#send_request` requires `session_id` to prevent broadcasting sampling requests to all connected clients. Missing or invalid session IDs raise explicit errors instead of silently failing. The `include_context` parameter is soft-deprecated in the MCP specification but is included for compatibility with existing clients that declare the `sampling.context` capability. Python and TypeScript SDKs also retain this parameter. Ref: https://modelcontextprotocol.io/specification/latest/server/utilities/sampling ## How Has This Been Tested? Added comprehensive tests in `test/mcp/server_sampling_test.rb` covering required and optional parameters, capability validation, error handling, nil-param omission, per-session capability isolation, and HTTP init capability scoping. Added transport tests in `test/mcp/server/transports/stdio_transport_test.rb` and `test/mcp/server/transports/streamable_http_transport_test.rb` for `send_request` round-trip behavior, error responses, stateless mode rejection, missing session_id, invalid session_id, and no-active-stream error. Conformance: `tools-call-sampling` is removed from expected failures. The conformance server's `TestSampling` tool now calls `server_context.create_sampling_message` instead of returning a stub error. This aligns the Ruby SDK with the MCP specification and other SDK implementations. ## Breaking Changes None for end users. `Transport#send_request` is a new abstract method. `ServerSession#create_sampling_message` is a new method.
57 lines
2 KiB
Ruby
57 lines
2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module MCP
|
|
class ServerContext
|
|
def initialize(context, progress:, notification_target:)
|
|
@context = context
|
|
@progress = progress
|
|
@notification_target = notification_target
|
|
end
|
|
|
|
# Reports progress for the current tool operation.
|
|
# The notification is automatically scoped to the originating session.
|
|
#
|
|
# @param progress [Numeric] Current progress value.
|
|
# @param total [Numeric, nil] Total expected value.
|
|
# @param message [String, nil] Human-readable status message.
|
|
def report_progress(progress, total: nil, message: nil)
|
|
@progress.report(progress, total: total, message: message)
|
|
end
|
|
|
|
# Sends a log message notification scoped to the originating session.
|
|
#
|
|
# @param data [Object] The log data to send.
|
|
# @param level [String] Log level (e.g., `"debug"`, `"info"`, `"error"`).
|
|
# @param logger [String, nil] Logger name.
|
|
def notify_log_message(data:, level:, logger: nil)
|
|
return unless @notification_target
|
|
|
|
@notification_target.notify_log_message(data: data, level: level, logger: logger)
|
|
end
|
|
|
|
# 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.
|
|
def create_sampling_message(**kwargs)
|
|
if @notification_target.respond_to?(:create_sampling_message)
|
|
@notification_target.create_sampling_message(**kwargs)
|
|
elsif @context.respond_to?(:create_sampling_message)
|
|
@context.create_sampling_message(**kwargs)
|
|
else
|
|
raise NoMethodError, "undefined method 'create_sampling_message' for #{self}"
|
|
end
|
|
end
|
|
|
|
def method_missing(name, ...)
|
|
if @context.respond_to?(name)
|
|
@context.public_send(name, ...)
|
|
else
|
|
super
|
|
end
|
|
end
|
|
|
|
def respond_to_missing?(name, include_private = false)
|
|
@context.respond_to?(name) || super
|
|
end
|
|
end
|
|
end
|