Support elicitation/create per MCP specification
## 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.
This commit is contained in:
parent
88248fc4c7
commit
985fd03ecb
4 changed files with 566 additions and 36 deletions
|
|
@ -33,6 +33,7 @@ module MCP
|
|||
NOTIFICATIONS_MESSAGE = "notifications/message"
|
||||
NOTIFICATIONS_PROGRESS = "notifications/progress"
|
||||
NOTIFICATIONS_CANCELLED = "notifications/cancelled"
|
||||
NOTIFICATIONS_ELICITATION_COMPLETE = "notifications/elicitation/complete"
|
||||
|
||||
class MissingRequiredCapabilityError < StandardError
|
||||
attr_reader :method
|
||||
|
|
@ -79,8 +80,8 @@ module MCP
|
|||
require_capability!(method, capabilities, :sampling)
|
||||
when ELICITATION_CREATE
|
||||
require_capability!(method, capabilities, :elicitation)
|
||||
when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED
|
||||
# No specific capability required for initialize, ping, progress or cancelled
|
||||
when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED, NOTIFICATIONS_ELICITATION_COMPLETE
|
||||
# No specific capability required.
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -31,14 +31,27 @@ module MCP
|
|||
MAX_COMPLETION_VALUES = 100
|
||||
|
||||
class RequestHandlerError < StandardError
|
||||
attr_reader :error_type
|
||||
attr_reader :original_error
|
||||
attr_reader :error_type, :original_error, :error_code, :error_data
|
||||
|
||||
def initialize(message, request, error_type: :internal_error, original_error: nil)
|
||||
def initialize(message, request, error_type: :internal_error, original_error: nil, error_code: nil, error_data: nil)
|
||||
super(message)
|
||||
@request = request
|
||||
@error_type = error_type
|
||||
@original_error = original_error
|
||||
@error_code = error_code
|
||||
@error_data = error_data
|
||||
end
|
||||
end
|
||||
|
||||
class URLElicitationRequiredError < RequestHandlerError
|
||||
def initialize(elicitations)
|
||||
super(
|
||||
"URL elicitation required",
|
||||
nil,
|
||||
error_type: :url_elicitation_required,
|
||||
error_code: -32042,
|
||||
error_data: { elicitations: elicitations },
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -114,7 +127,6 @@ module MCP
|
|||
# No op handlers for currently unsupported methods
|
||||
Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
|
||||
Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
|
||||
Methods::ELICITATION_CREATE => ->(_) {},
|
||||
}
|
||||
@transport = transport
|
||||
end
|
||||
|
|
|
|||
|
|
@ -43,6 +43,42 @@ module MCP
|
|||
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, ...)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,35 @@ module MCP
|
|||
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.
|
||||
def create_form_elicitation(message:, requested_schema:, related_request_id: nil)
|
||||
unless client_capabilities&.dig(:elicitation)
|
||||
raise "Client does not support elicitation. " \
|
||||
"The client must declare the `elicitation` capability during initialization."
|
||||
end
|
||||
|
||||
params = { mode: "form", message: message, requestedSchema: requested_schema }
|
||||
send_to_transport_request(Methods::ELICITATION_CREATE, params, related_request_id: related_request_id)
|
||||
end
|
||||
|
||||
# Sends an `elicitation/create` request (URL mode) scoped to this session.
|
||||
def create_url_elicitation(message:, url:, elicitation_id:, related_request_id: nil)
|
||||
unless client_capabilities&.dig(:elicitation, :url)
|
||||
raise "Client does not support URL mode elicitation. " \
|
||||
"The client must declare the `elicitation.url` capability during initialization."
|
||||
end
|
||||
|
||||
params = { mode: "url", message: message, url: url, elicitationId: elicitation_id }
|
||||
send_to_transport_request(Methods::ELICITATION_CREATE, params, related_request_id: related_request_id)
|
||||
end
|
||||
|
||||
# Sends an elicitation complete notification scoped to this session.
|
||||
def notify_elicitation_complete(elicitation_id:)
|
||||
send_to_transport(Methods::NOTIFICATIONS_ELICITATION_COMPLETE, { elicitationId: elicitation_id })
|
||||
rescue => e
|
||||
@server.report_exception(e, notification: "elicitation_complete")
|
||||
end
|
||||
|
||||
# Sends a progress notification to this session only.
|
||||
def notify_progress(progress_token:, progress:, total: nil, message: nil, related_request_id: nil)
|
||||
params = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue