# 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, **_kwargs) @requests << { method: method, params: params } { action: "accept" } end def send_response(response); end def send_notification(method, params = nil, **_kwargs) @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 warns when called without related_request_id" do # Per SEP-2260, server-to-client requests must be associated with an originating client request. # `$VERBOSE = false` because the rake test task runs with `-W0`, under which `Kernel#warn` emits nothing. original_verbose = $VERBOSE $VERBOSE = false assert_output(nil, /SEP-2260/) do @session.create_form_elicitation( message: "Please provide your name", requested_schema: { type: "object", properties: { name: { type: "string" } } }, ) end ensure $VERBOSE = original_verbose end test "#create_url_elicitation warns when called without related_request_id" do @session.instance_variable_set(:@client_capabilities, { elicitation: { url: true } }) original_verbose = $VERBOSE $VERBOSE = false assert_output(nil, /SEP-2260/) do @session.create_url_elicitation( message: "Please sign in", url: "https://example.com/signin", elicitation_id: "el-1", ) end ensure $VERBOSE = original_verbose end test "#create_form_elicitation does not warn when related_request_id is given" do assert_silent do @session.create_form_elicitation( related_request_id: "req-1", message: "Please provide your name", requested_schema: { type: "object", properties: { name: { type: "string" } } }, ) end end test "#create_form_elicitation sends request through transport" do result = @session.create_form_elicitation( related_request_id: "req-1", 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( related_request_id: "req-1", 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( related_request_id: "req-1", 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( related_request_id: "req-1", 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( related_request_id: "req-1", 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( related_request_id: "req-1", 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