Merge pull request #312 from koic/support_elicitation

Support `elicitation/create` per MCP specification
This commit is contained in:
Koichi ITO 2026-04-15 02:22:55 +09:00 • committed by GitHub
commit 3b1777adb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 566 additions and 36 deletions

104
README.md
View file

@ -53,6 +53,7 @@ It implements the Model Context Protocol specification, handling model context r
- `resources/templates/list` - Lists all registered resource templates and their schemas - `resources/templates/list` - Lists all registered resource templates and their schemas
- `completion/complete` - Returns autocompletion suggestions for prompt arguments and resource URIs - `completion/complete` - Returns autocompletion suggestions for prompt arguments and resource URIs
- `sampling/createMessage` - Requests LLM completion from the client (server-to-client) - `sampling/createMessage` - Requests LLM completion from the client (server-to-client)
- `elicitation/create` - Requests user input from the client (server-to-client)
### Usage ### Usage
@ -1137,6 +1138,108 @@ The SDK automatically enforces the 100-item limit per the MCP specification.
The server validates that the referenced prompt, resource, or resource template is registered before calling the handler. The server validates that the referenced prompt, resource, or resource template is registered before calling the handler.
Requests for unknown references return an error. Requests for unknown references return an error.
### Elicitation
The MCP Ruby SDK supports [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation),
which allows servers to request additional information from users through the client during tool execution.
Elicitation is a **server-to-client request**. The server sends a request and blocks until the user responds via the client.
#### Capabilities
Clients must declare the `elicitation` capability during initialization. The server checks this before sending any elicitation request
and raises a `RuntimeError` if the client does not support it.
For URL mode support, the client must also declare `elicitation.url` capability.
#### Using Elicitation in Tools
Tools that accept a `server_context:` parameter can call `create_form_elicitation` on it:
```ruby
server.define_tool(name: "collect_info", description: "Collect user info") do |server_context:|
result = server_context.create_form_elicitation(
message: "Please provide your name",
requested_schema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
},
)
MCP::Tool::Response.new([{ type: "text", text: "Hello, #{result[:content][:name]}" }])
end
```
#### Form Mode
Form mode collects structured data from the user directly through the MCP client:
```ruby
server.define_tool(name: "collect_contact", description: "Collect contact info") do |server_context:|
result = server_context.create_form_elicitation(
message: "Please provide your contact information",
requested_schema: {
type: "object",
properties: {
name: { type: "string", description: "Your full name" },
email: { type: "string", format: "email", description: "Your email address" },
},
required: ["name", "email"],
},
)
text = case result[:action]
when "accept"
"Hello, #{result[:content][:name]} (#{result[:content][:email]})"
when "decline"
"User declined"
when "cancel"
"User cancelled"
end
MCP::Tool::Response.new([{ type: "text", text: text }])
end
```
#### URL Mode
URL mode directs the user to an external URL for out-of-band interactions such as OAuth flows:
```ruby
server.define_tool(name: "authorize_github", description: "Authorize GitHub") do |server_context:|
elicitation_id = SecureRandom.uuid
result = server_context.create_url_elicitation(
message: "Please authorize access to your GitHub account",
url: "https://example.com/oauth/authorize?elicitation_id=#{elicitation_id}",
elicitation_id: elicitation_id,
)
server_context.notify_elicitation_complete(elicitation_id: elicitation_id)
MCP::Tool::Response.new([{ type: "text", text: "Authorization complete" }])
end
```
#### URLElicitationRequiredError
When a tool cannot proceed until an out-of-band elicitation is completed, raise `MCP::Server::URLElicitationRequiredError`.
This returns a JSON-RPC error with code `-32042` to the client:
```ruby
server.define_tool(name: "access_github", description: "Access GitHub") do |server_context:|
raise MCP::Server::URLElicitationRequiredError.new([
{
mode: "url",
elicitationId: SecureRandom.uuid,
url: "https://example.com/oauth/authorize",
message: "GitHub authorization is required.",
},
])
end
```
### Logging ### Logging
The MCP Ruby SDK supports structured logging through the `notify_log_message` method, following the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging). The MCP Ruby SDK supports structured logging through the `notify_log_message` method, following the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging).
@ -1302,7 +1405,6 @@ end
### Unsupported Features (to be implemented in future versions) ### Unsupported Features (to be implemented in future versions)
- Resource subscriptions - Resource subscriptions
- Elicitation
## Building an MCP Client ## Building an MCP Client

View file

@ -1,5 +1 @@
server: server: []
# TODO: Server-to-client requests (elicitation/create) are not implemented.
- tools-call-elicitation
- elicitation-sep1034-defaults
- elicitation-sep1330-enums

View file

@ -178,7 +178,6 @@ module Conformance
end end
end end
# TODO: Implement when `Transport` supports server-to-client requests.
class TestElicitation < MCP::Tool class TestElicitation < MCP::Tool
tool_name "test_elicitation" tool_name "test_elicitation"
description "A tool that requests user input from the client" description "A tool that requests user input from the client"
@ -188,41 +187,96 @@ module Conformance
) )
class << self class << self
def call(message:) def call(server_context:, message:)
MCP::Tool::Response.new( result = server_context.create_form_elicitation(
[MCP::Content::Text.new("Elicitation not supported in this SDK version").to_h], message: message,
error: true, requested_schema: {
type: "object",
properties: {
username: { type: "string", description: "User's response" },
email: { type: "string", description: "User's email address" },
},
required: ["username", "email"],
},
) )
MCP::Tool::Response.new([MCP::Content::Text.new("User response: #{result}").to_h])
end end
end end
end end
# TODO: Implement when `Transport` supports server-to-client requests.
class TestElicitationSep1034Defaults < MCP::Tool class TestElicitationSep1034Defaults < MCP::Tool
tool_name "test_elicitation_sep1034_defaults" tool_name "test_elicitation_sep1034_defaults"
description "A tool that tests elicitation with default values" description "A tool that tests elicitation with default values"
class << self class << self
def call(**_args) def call(server_context:, **_args)
MCP::Tool::Response.new( result = server_context.create_form_elicitation(
[MCP::Content::Text.new("Elicitation not supported in this SDK version").to_h], message: "Please provide your information (with defaults)",
error: true, requested_schema: {
type: "object",
properties: {
name: { type: "string", default: "John Doe" },
age: { type: "integer", default: 30 },
score: { type: "number", default: 95.5 },
status: { type: "string", enum: ["active", "inactive", "pending"], default: "active" },
verified: { type: "boolean", default: true },
},
},
) )
MCP::Tool::Response.new([MCP::Content::Text.new("Elicitation result: #{result}").to_h])
end end
end end
end end
# TODO: Implement when `Transport` supports server-to-client requests.
class TestElicitationSep1330Enums < MCP::Tool class TestElicitationSep1330Enums < MCP::Tool
tool_name "test_elicitation_sep1330_enums" tool_name "test_elicitation_sep1330_enums"
description "A tool that tests elicitation with enum schemas" description "A tool that tests elicitation with enum schemas"
class << self class << self
def call(**_args) def call(server_context:, **_args)
MCP::Tool::Response.new( result = server_context.create_form_elicitation(
[MCP::Content::Text.new("Elicitation not supported in this SDK version").to_h], message: "Please select options",
error: true, requested_schema: {
type: "object",
properties: {
untitledSingle: {
type: "string",
enum: ["option1", "option2", "option3"],
},
titledSingle: {
type: "string",
oneOf: [
{ const: "value1", title: "First Option" },
{ const: "value2", title: "Second Option" },
{ const: "value3", title: "Third Option" },
],
},
legacyEnum: {
type: "string",
enum: ["opt1", "opt2", "opt3"],
enumNames: ["Option One", "Option Two", "Option Three"],
},
untitledMulti: {
type: "array",
items: {
type: "string",
enum: ["option1", "option2", "option3"],
},
},
titledMulti: {
type: "array",
items: {
anyOf: [
{ const: "value1", title: "First Choice" },
{ const: "value2", title: "Second Choice" },
{ const: "value3", title: "Third Choice" },
],
},
},
},
},
) )
MCP::Tool::Response.new([MCP::Content::Text.new("Elicitation result: #{result}").to_h])
end end
end end
end end

View file

@ -117,20 +117,27 @@ module JsonRpcHandler
end end
def handle_request_error(error, id, id_validation_pattern) def handle_request_error(error, id, id_validation_pattern)
error_type = error.respond_to?(:error_type) ? error.error_type : nil if error.respond_to?(:error_code) && error.error_code
code = error.error_code
message = error.message
else
error_type = error.respond_to?(:error_type) ? error.error_type : nil
code, message = case error_type code, message = case error_type
when :invalid_request then [ErrorCode::INVALID_REQUEST, "Invalid Request"] when :invalid_request then [ErrorCode::INVALID_REQUEST, "Invalid Request"]
when :invalid_params then [ErrorCode::INVALID_PARAMS, "Invalid params"] when :invalid_params then [ErrorCode::INVALID_PARAMS, "Invalid params"]
when :parse_error then [ErrorCode::PARSE_ERROR, "Parse error"] when :parse_error then [ErrorCode::PARSE_ERROR, "Parse error"]
when :internal_error then [ErrorCode::INTERNAL_ERROR, "Internal error"] when :internal_error then [ErrorCode::INTERNAL_ERROR, "Internal error"]
else [ErrorCode::INTERNAL_ERROR, "Internal error"] else [ErrorCode::INTERNAL_ERROR, "Internal error"]
end
end end
data = error.respond_to?(:error_data) && error.error_data ? error.error_data : error.message
error_response(id: id, id_validation_pattern: id_validation_pattern, error: { error_response(id: id, id_validation_pattern: id_validation_pattern, error: {
code: code, code: code,
message: message, message: message,
data: error.message, data: data,
}) })
end end

View file

@ -33,6 +33,7 @@ module MCP
NOTIFICATIONS_MESSAGE = "notifications/message" NOTIFICATIONS_MESSAGE = "notifications/message"
NOTIFICATIONS_PROGRESS = "notifications/progress" NOTIFICATIONS_PROGRESS = "notifications/progress"
NOTIFICATIONS_CANCELLED = "notifications/cancelled" NOTIFICATIONS_CANCELLED = "notifications/cancelled"
NOTIFICATIONS_ELICITATION_COMPLETE = "notifications/elicitation/complete"
class MissingRequiredCapabilityError < StandardError class MissingRequiredCapabilityError < StandardError
attr_reader :method attr_reader :method
@ -79,8 +80,8 @@ module MCP
require_capability!(method, capabilities, :sampling) require_capability!(method, capabilities, :sampling)
when ELICITATION_CREATE when ELICITATION_CREATE
require_capability!(method, capabilities, :elicitation) require_capability!(method, capabilities, :elicitation)
when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED, NOTIFICATIONS_ELICITATION_COMPLETE
# No specific capability required for initialize, ping, progress or cancelled # No specific capability required.
end end
end end

View file

@ -31,14 +31,27 @@ module MCP
MAX_COMPLETION_VALUES = 100 MAX_COMPLETION_VALUES = 100
class RequestHandlerError < StandardError class RequestHandlerError < StandardError
attr_reader :error_type attr_reader :error_type, :original_error, :error_code, :error_data
attr_reader :original_error
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) super(message)
@request = request @request = request
@error_type = error_type @error_type = error_type
@original_error = original_error @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
end end
@ -114,7 +127,6 @@ module MCP
# No op handlers for currently unsupported methods # No op handlers for currently unsupported methods
Methods::RESOURCES_SUBSCRIBE => ->(_) { {} }, Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} }, Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
Methods::ELICITATION_CREATE => ->(_) {},
} }
@transport = transport @transport = transport
end end

View file

@ -43,6 +43,42 @@ module MCP
end end
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, ...) def method_missing(name, ...)
if @context.respond_to?(name) if @context.respond_to?(name)
@context.public_send(name, ...) @context.public_send(name, ...)

View file

@ -47,6 +47,35 @@ module MCP
send_to_transport_request(Methods::SAMPLING_CREATE_MESSAGE, params, related_request_id: related_request_id) send_to_transport_request(Methods::SAMPLING_CREATE_MESSAGE, params, related_request_id: related_request_id)
end 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. # Sends a progress notification to this session only.
def notify_progress(progress_token:, progress:, total: nil, message: nil, related_request_id: nil) def notify_progress(progress_token:, progress:, total: nil, message: nil, related_request_id: nil)
params = { params = {

View file

@ -488,6 +488,43 @@ describe JsonRpcHandler do
} }
end end
it "returns a custom error code when RequestHandlerError has error_code set" do
register("test_method") do
raise MCP::Server::RequestHandlerError.new(
"Custom error",
nil,
error_code: -32042,
error_data: { elicitations: [{ id: "abc" }] },
)
end
handle jsonrpc: "2.0", id: 1, method: "test_method"
assert_rpc_error expected_error: {
code: -32042,
message: "Custom error",
data: { elicitations: [{ id: "abc" }] },
}
end
it "falls back to error_type when error_code is nil" do
register("test_method") do
raise MCP::Server::RequestHandlerError.new(
"Invalid params error",
nil,
error_type: :invalid_params,
)
end
handle jsonrpc: "2.0", id: 1, method: "test_method"
assert_rpc_error expected_error: {
code: -32602,
message: "Invalid params",
data: "Invalid params error",
}
end
# 6 Batch # 6 Batch
# #
# To send several Request objects at the same time, the Client MAY send an Array filled with Request objects. # To send several Request objects at the same time, the Client MAY send an Array filled with Request objects.

View file

@ -81,5 +81,6 @@ module MCP
ensure_capability_does_not_raise_for Methods::PING ensure_capability_does_not_raise_for Methods::PING
ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_PROGRESS ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_PROGRESS
ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_CANCELLED ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_CANCELLED
ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_ELICITATION_COMPLETE
end end
end end

View file

@ -83,6 +83,62 @@ module MCP
assert_equal "Fallback", result[:content][:text] assert_equal "Fallback", result[:content][:text]
end end
test "ServerContext#create_form_elicitation delegates to notification_target" do
notification_target = mock
notification_target.expects(:create_form_elicitation).with(
message: "Please provide your name",
requested_schema: { type: "object", properties: { name: { type: "string" } } },
related_request_id: nil,
).returns(action: "accept", content: { name: "test_user" })
context = mock
progress = Progress.new(notification_target: notification_target, progress_token: nil)
server_context = ServerContext.new(context, progress: progress, notification_target: notification_target)
result = server_context.create_form_elicitation(
message: "Please provide your name",
requested_schema: { type: "object", properties: { name: { type: "string" } } },
)
assert_equal "accept", result[:action]
end
test "ServerContext#create_url_elicitation delegates to notification_target" do
notification_target = mock
notification_target.expects(:create_url_elicitation).with(
message: "Please authorize",
url: "https://example.com/oauth",
elicitation_id: "abc-123",
related_request_id: nil,
).returns(action: "accept")
context = mock
progress = Progress.new(notification_target: notification_target, progress_token: nil)
server_context = ServerContext.new(context, progress: progress, notification_target: notification_target)
result = server_context.create_url_elicitation(
message: "Please authorize",
url: "https://example.com/oauth",
elicitation_id: "abc-123",
)
assert_equal "accept", result[:action]
end
test "ServerContext#notify_elicitation_complete delegates to notification_target" do
notification_target = mock
notification_target.expects(:notify_elicitation_complete).with(elicitation_id: "abc-123")
context = mock
progress = Progress.new(notification_target: notification_target, progress_token: nil)
server_context = ServerContext.new(context, progress: progress, notification_target: notification_target)
server_context.notify_elicitation_complete(elicitation_id: "abc-123")
end
test "ServerContext delegates to custom object context" do test "ServerContext delegates to custom object context" do
context = Object.new context = Object.new
def context.custom_method def context.custom_method

View file

@ -0,0 +1,199 @@
# frozen_string_literal: true
require "test_helper"
module MCP
class ServerElicitationTest < ActiveSupport::TestCase
include InstrumentationTestHelper
class MockTransport < Transport
attr_reader :requests, :notifications
def initialize(server)
super
@requests = []
@notifications = []
end
def send_request(method, params = nil)
@requests << { method: method, params: params }
{ action: "accept" }
end
def send_response(response); end
def send_notification(method, params = nil)
@notifications << { method: method, params: params }
end
def open; end
def close; end
def handle_request(request); end
end
setup do
configuration = MCP::Configuration.new
configuration.instrumentation_callback = instrumentation_helper.callback
@server = Server.new(
name: "test_server",
version: "1.0.0",
configuration: configuration,
)
@mock_transport = MockTransport.new(@server)
@server.transport = @mock_transport
@session = ServerSession.new(server: @server, transport: @mock_transport)
@session.instance_variable_set(:@client_capabilities, { elicitation: {} })
end
test "#create_form_elicitation sends request through transport" do
result = @session.create_form_elicitation(
message: "Please provide your name",
requested_schema: {
type: "object",
properties: {
name: { type: "string" },
},
required: ["name"],
},
)
assert_equal 1, @mock_transport.requests.size
request = @mock_transport.requests.first
assert_equal Methods::ELICITATION_CREATE, request[:method]
assert_equal "form", request[:params][:mode]
assert_equal "Please provide your name", request[:params][:message]
assert_equal "object", request[:params][:requestedSchema][:type]
assert_equal({ action: "accept" }, result)
end
test "#create_form_elicitation raises error when client does not support elicitation" do
@session.instance_variable_set(:@client_capabilities, {})
error = assert_raises(RuntimeError) do
@session.create_form_elicitation(
message: "Please provide your name",
requested_schema: { type: "object", properties: { name: { type: "string" } } },
)
end
assert_equal(
"Client does not support elicitation. The client must declare the `elicitation` capability during initialization.",
error.message,
)
end
test "#create_form_elicitation raises error when client capabilities are nil" do
@session.instance_variable_set(:@client_capabilities, nil)
@server.instance_variable_set(:@client_capabilities, nil)
error = assert_raises(RuntimeError) do
@session.create_form_elicitation(
message: "Please provide your name",
requested_schema: { type: "object", properties: { name: { type: "string" } } },
)
end
assert_equal(
"Client does not support elicitation. The client must declare the `elicitation` capability during initialization.",
error.message,
)
end
test "#create_url_elicitation sends url mode request through transport" do
@session.instance_variable_set(:@client_capabilities, { elicitation: { url: {} } })
result = @session.create_url_elicitation(
message: "Please authorize access",
url: "https://example.com/oauth",
elicitation_id: "abc-123",
)
assert_equal 1, @mock_transport.requests.size
request = @mock_transport.requests.first
assert_equal Methods::ELICITATION_CREATE, request[:method]
assert_equal "url", request[:params][:mode]
assert_equal "Please authorize access", request[:params][:message]
assert_equal "https://example.com/oauth", request[:params][:url]
assert_equal "abc-123", request[:params][:elicitationId]
assert_equal({ action: "accept" }, result)
end
test "#create_url_elicitation raises error when client does not support url mode" do
error = assert_raises(RuntimeError) do
@session.create_url_elicitation(
message: "Please authorize access",
url: "https://example.com/oauth",
elicitation_id: "abc-123",
)
end
assert_equal(
"Client does not support URL mode elicitation. The client must declare the `elicitation.url` capability during initialization.",
error.message,
)
end
test "#create_url_elicitation raises error when client capabilities are nil" do
@session.instance_variable_set(:@client_capabilities, nil)
@server.instance_variable_set(:@client_capabilities, nil)
error = assert_raises(RuntimeError) do
@session.create_url_elicitation(
message: "Please authorize access",
url: "https://example.com/oauth",
elicitation_id: "abc-123",
)
end
assert_equal(
"Client does not support URL mode elicitation. The client must declare the `elicitation.url` capability during initialization.",
error.message,
)
end
test "#notify_elicitation_complete sends notification through transport" do
@session.notify_elicitation_complete(elicitation_id: "abc-123")
assert_equal 1, @mock_transport.notifications.size
notification = @mock_transport.notifications.first
assert_equal Methods::NOTIFICATIONS_ELICITATION_COMPLETE, notification[:method]
assert_equal "abc-123", notification[:params][:elicitationId]
end
test "URLElicitationRequiredError can be raised from a tool handler" do
@server.define_tool(name: "needs_auth", description: "Needs OAuth") do
raise Server::URLElicitationRequiredError, [
{ mode: "url", elicitationId: "abc-123", url: "https://example.com/oauth", message: "Auth required" },
]
end
response = JSON.parse(@server.handle_json(JSON.generate({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: "needs_auth", arguments: {} },
})))
assert_equal(-32042, response["error"]["code"])
assert_equal "URL elicitation required", response["error"]["message"]
elicitations = response["error"]["data"]["elicitations"]
assert_equal 1, elicitations.size
assert_equal "abc-123", elicitations.first["elicitationId"]
assert_instrumentation_data(
method: "tools/call", tool_name: "needs_auth", tool_arguments: {}, error: :url_elicitation_required,
)
end
end
end