## Motivation and Context The MCP specification (2025-11-25) defines elicitation as a server-to-client request mechanism that allows servers to request user input during tool execution: https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation This aligns with the Python SDK, which also exposes separate methods for form and URL mode elicitation. Three methods are added following the Python SDK's API design: - `create_form_elicitation`: collects structured data via JSON Schema - `create_url_elicitation`: directs users to external URLs for sensitive interactions (OAuth, credentials, etc.) - `notify_elicitation_complete`: notifies the client that a URL mode interaction has completed `URLElicitationRequiredError` (error code `-32042`) is defined as a subclass of `RequestHandlerError` with its own `error_code` and `error_data` attributes. `JsonRpcHandler#handle_request_error` is extended to check for these attributes so that MCP-specific error codes can be propagated without embedding MCP knowledge in the generic JSON-RPC layer. ## How Has This Been Tested? - Unit tests for `ServerSession` form/URL mode, capability checks, nil capabilities, notification, and `URLElicitationRequiredError` - `ServerContext` delegation tests for all three elicitation methods - `JsonRpcHandler` tests for custom error code and fallback behavior - `Methods` capability test for `NOTIFICATIONS_ELICITATION_COMPLETE` - Conformance tests pass (40/40), including `tools-call-elicitation`, `elicitation-sep1034-defaults`, and `elicitation-sep1330-enums` - All tests pass (`rake test`), RuboCop is clean - Verified form mode elicitation end-to-end with a sample MCP server running in Claude Code ## Breaking Changes None. This is a new feature addition.
94 lines
4 KiB
Ruby
94 lines
4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module MCP
|
|
class ServerContext
|
|
def initialize(context, progress:, notification_target:, related_request_id: nil)
|
|
@context = context
|
|
@progress = progress
|
|
@notification_target = notification_target
|
|
@related_request_id = related_request_id
|
|
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, related_request_id: @related_request_id)
|
|
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, related_request_id: @related_request_id)
|
|
elsif @context.respond_to?(:create_sampling_message)
|
|
@context.create_sampling_message(**kwargs, related_request_id: @related_request_id)
|
|
else
|
|
raise NoMethodError, "undefined method 'create_sampling_message' for #{self}"
|
|
end
|
|
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 elicitation.
|
|
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)
|
|
elsif @context.respond_to?(:create_form_elicitation)
|
|
@context.create_form_elicitation(**kwargs, related_request_id: @related_request_id)
|
|
else
|
|
raise NoMethodError, "undefined method 'create_form_elicitation' for #{self}"
|
|
end
|
|
end
|
|
|
|
# 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.
|
|
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)
|
|
elsif @context.respond_to?(:create_url_elicitation)
|
|
@context.create_url_elicitation(**kwargs, related_request_id: @related_request_id)
|
|
else
|
|
raise NoMethodError, "undefined method 'create_url_elicitation' for #{self}"
|
|
end
|
|
end
|
|
|
|
# Delegates to the session so the notification is scoped to the originating client.
|
|
def notify_elicitation_complete(**kwargs)
|
|
if @notification_target.respond_to?(:notify_elicitation_complete)
|
|
@notification_target.notify_elicitation_complete(**kwargs)
|
|
elsif @context.respond_to?(:notify_elicitation_complete)
|
|
@context.notify_elicitation_complete(**kwargs)
|
|
else
|
|
raise NoMethodError, "undefined method 'notify_elicitation_complete' 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
|