diff --git a/lib/mcp_client/client.rb b/lib/mcp_client/client.rb index 2a604c7..8b0e6a2 100644 --- a/lib/mcp_client/client.rb +++ b/lib/mcp_client/client.rb @@ -25,7 +25,7 @@ class Client # @param logger [Logger, nil] optional logger, defaults to STDOUT # @param elicitation_handler [Proc, nil] optional handler for elicitation requests (MCP 2025-06-18) # @param roots [Array, nil] optional list of roots (MCP 2025-06-18) - # @param sampling_handler [Proc, nil] optional handler for sampling requests (MCP 2025-06-18) + # @param sampling_handler [Proc, nil] optional handler for sampling requests (MCP 2025-11-25) def initialize(mcp_server_configs: [], logger: nil, elicitation_handler: nil, roots: nil, sampling_handler: nil) @logger = logger || Logger.new($stdout, level: Logger::WARN) @logger.progname = self.class.name @@ -41,7 +41,7 @@ def initialize(mcp_server_configs: [], logger: nil, elicitation_handler: nil, ro @notification_listeners = [] # Elicitation handler (MCP 2025-06-18) @elicitation_handler = elicitation_handler - # Sampling handler (MCP 2025-06-18) + # Sampling handler (MCP 2025-11-25) @sampling_handler = sampling_handler # Roots (MCP 2025-06-18) @roots = normalize_roots(roots) @@ -59,7 +59,7 @@ def initialize(mcp_server_configs: [], logger: nil, elicitation_handler: nil, ro end # Register roots list handler on each server (MCP 2025-06-18) server.on_roots_list_request(&method(:handle_roots_list_request)) if server.respond_to?(:on_roots_list_request) - # Register sampling handler on each server (MCP 2025-06-18) + # Register sampling handler on each server (MCP 2025-11-25) server.on_sampling_request(&method(:handle_sampling_request)) if server.respond_to?(:on_sampling_request) end end @@ -857,9 +857,9 @@ def notify_roots_changed end end - # Handle sampling/createMessage request from server (MCP 2025-06-18) + # Handle sampling/createMessage request from server (MCP 2025-11-25) # @param _request_id [String, Integer] the JSON-RPC request ID (unused, kept for callback signature) - # @param params [Hash] the sampling parameters (messages, modelPreferences, systemPrompt, maxTokens) + # @param params [Hash] the sampling parameters # @return [Hash] the sampling response (role, content, model, stopReason) def handle_sampling_request(_request_id, params) # If no handler is configured, return an error @@ -869,24 +869,18 @@ def handle_sampling_request(_request_id, params) end messages = params['messages'] || [] - model_preferences = params['modelPreferences'] + model_preferences = normalize_model_preferences(params['modelPreferences']) system_prompt = params['systemPrompt'] max_tokens = params['maxTokens'] + include_context = params['includeContext'] + temperature = params['temperature'] + stop_sequences = params['stopSequences'] + metadata = params['metadata'] begin - # Call the user-defined handler - result = case @sampling_handler.arity - when 0 - @sampling_handler.call - when 1 - @sampling_handler.call(messages) - when 2 - @sampling_handler.call(messages, model_preferences) - when 3 - @sampling_handler.call(messages, model_preferences, system_prompt) - else - @sampling_handler.call(messages, model_preferences, system_prompt, max_tokens) - end + # Call the user-defined handler with parameters based on arity + result = call_sampling_handler(messages, model_preferences, system_prompt, max_tokens, + include_context, temperature, stop_sequences, metadata) # Validate and format response validate_sampling_response(result) @@ -897,7 +891,70 @@ def handle_sampling_request(_request_id, params) end end - # Validate sampling response from handler (MCP 2025-06-18) + # Call sampling handler with appropriate arity + # @param messages [Array] the messages + # @param model_preferences [Hash, nil] normalized model preferences + # @param system_prompt [String, nil] system prompt + # @param max_tokens [Integer, nil] max tokens + # @param include_context [String, nil] context inclusion setting + # @param temperature [Float, nil] temperature + # @param stop_sequences [Array, nil] stop sequences + # @param metadata [Hash, nil] metadata + # @return [Hash] the handler result + def call_sampling_handler(messages, model_preferences, system_prompt, max_tokens, + include_context, temperature, stop_sequences, metadata) + arity = @sampling_handler.arity + # Normalize negative arity (optional params) to minimum required args + arity = -(arity + 1) if arity.negative? + case arity + when 0 + @sampling_handler.call + when 1 + @sampling_handler.call(messages) + when 2 + @sampling_handler.call(messages, model_preferences) + when 3 + @sampling_handler.call(messages, model_preferences, system_prompt) + when 4 + @sampling_handler.call(messages, model_preferences, system_prompt, max_tokens) + else + @sampling_handler.call(messages, model_preferences, system_prompt, max_tokens, + { 'includeContext' => include_context, 'temperature' => temperature, + 'stopSequences' => stop_sequences, 'metadata' => metadata }) + end + end + + # Normalize and validate modelPreferences from sampling request (MCP 2025-11-25) + # Ensures hints is an array of hashes with 'name', and priority values are clamped to 0.0..1.0 + # @param prefs [Hash, nil] raw modelPreferences from request + # @return [Hash, nil] normalized modelPreferences or nil + def normalize_model_preferences(prefs) + return nil if prefs.nil? + return nil unless prefs.is_a?(Hash) + + normalized = {} + + # Normalize hints: array of { 'name' => String } + if prefs['hints'] + normalized['hints'] = Array(prefs['hints']).filter_map do |hint| + next nil unless hint.is_a?(Hash) && hint['name'] + + { 'name' => hint['name'].to_s } + end + end + + # Normalize priority values (0.0 to 1.0) + %w[costPriority speedPriority intelligencePriority].each do |key| + next unless prefs.key?(key) + + value = prefs[key] + normalized[key] = value.is_a?(Numeric) ? value.to_f.clamp(0.0, 1.0) : nil + end + + normalized + end + + # Validate sampling response from handler (MCP 2025-11-25) # @param result [Hash] the result from the sampling handler # @return [Hash] validated sampling response def validate_sampling_response(result) diff --git a/lib/mcp_client/json_rpc_common.rb b/lib/mcp_client/json_rpc_common.rb index ed7fcd7..ca51b8e 100644 --- a/lib/mcp_client/json_rpc_common.rb +++ b/lib/mcp_client/json_rpc_common.rb @@ -63,9 +63,9 @@ def build_jsonrpc_notification(method, params) # @return [Hash] the initialization parameters def initialization_params capabilities = { - 'elicitation' => {}, # MCP 2025-11-25: form and URL mode elicitation - 'roots' => { 'listChanged' => true }, # MCP 2025-06-18: Support for roots - 'sampling' => {} # MCP 2025-06-18: Support for server-initiated LLM sampling + 'elicitation' => {}, # MCP 2025-11-25: Support for server-initiated user interactions + 'roots' => { 'listChanged' => true }, # MCP 2025-11-25: Support for roots + 'sampling' => {} # MCP 2025-11-25: Support for server-initiated LLM sampling } { diff --git a/lib/mcp_client/server_sse.rb b/lib/mcp_client/server_sse.rb index 2cef201..dc0651c 100644 --- a/lib/mcp_client/server_sse.rb +++ b/lib/mcp_client/server_sse.rb @@ -106,7 +106,7 @@ def initialize(base_url:, headers: {}, read_timeout: 30, ping: 10, @activity_timer_thread = nil @elicitation_request_callback = nil # MCP 2025-06-18 @roots_list_request_callback = nil # MCP 2025-06-18 - @sampling_request_callback = nil # MCP 2025-06-18 + @sampling_request_callback = nil # MCP 2025-11-25 end # Stream tool call fallback for SSE transport (yields single result) @@ -455,7 +455,7 @@ def on_roots_list_request(&block) @roots_list_request_callback = block end - # Register a callback for sampling requests (MCP 2025-06-18) + # Register a callback for sampling requests (MCP 2025-11-25) # @param block [Proc] callback that receives (request_id, params) and returns response hash # @return [void] def on_sampling_request(&block) @@ -545,7 +545,7 @@ def send_roots_list_response(request_id, result) @logger.error("Error sending roots/list response: #{e.message}") end - # Handle sampling/createMessage request from server (MCP 2025-06-18) + # Handle sampling/createMessage request from server (MCP 2025-11-25) # @param request_id [String, Integer] the JSON-RPC request ID # @param params [Hash] the sampling parameters # @return [void] @@ -564,7 +564,7 @@ def handle_sampling_create_message(request_id, params) send_sampling_response(request_id, result) end - # Send sampling response back to server via HTTP POST (MCP 2025-06-18) + # Send sampling response back to server via HTTP POST (MCP 2025-11-25) # @param request_id [String, Integer] the JSON-RPC request ID # @param result [Hash] the sampling result (role, content, model, stopReason) # @return [void] diff --git a/lib/mcp_client/server_stdio.rb b/lib/mcp_client/server_stdio.rb index 286cf3b..37d5ec0 100644 --- a/lib/mcp_client/server_stdio.rb +++ b/lib/mcp_client/server_stdio.rb @@ -48,7 +48,7 @@ def initialize(command:, retries: 0, retry_backoff: 1, read_timeout: READ_TIMEOU @env = env || {} @elicitation_request_callback = nil # MCP 2025-06-18 @roots_list_request_callback = nil # MCP 2025-06-18 - @sampling_request_callback = nil # MCP 2025-06-18 + @sampling_request_callback = nil # MCP 2025-11-25 end # Server info from the initialize response @@ -406,7 +406,7 @@ def on_roots_list_request(&block) @roots_list_request_callback = block end - # Register a callback for sampling requests (MCP 2025-06-18) + # Register a callback for sampling requests (MCP 2025-11-25) # @param block [Proc] callback that receives (request_id, params) and returns response hash # @return [void] def on_sampling_request(&block) @@ -477,7 +477,7 @@ def handle_roots_list(request_id, params) send_roots_list_response(request_id, result) end - # Handle sampling/createMessage request from server (MCP 2025-06-18) + # Handle sampling/createMessage request from server (MCP 2025-11-25) # @param request_id [String, Integer] the JSON-RPC request ID # @param params [Hash] the sampling parameters # @return [void] @@ -509,7 +509,7 @@ def send_roots_list_response(request_id, result) send_message(response) end - # Send sampling response back to server (MCP 2025-06-18) + # Send sampling response back to server (MCP 2025-11-25) # @param request_id [String, Integer] the JSON-RPC request ID # @param result [Hash] the sampling result (role, content, model, stopReason) # @return [void] diff --git a/lib/mcp_client/server_streamable_http.rb b/lib/mcp_client/server_streamable_http.rb index 4950ce2..814196e 100644 --- a/lib/mcp_client/server_streamable_http.rb +++ b/lib/mcp_client/server_streamable_http.rb @@ -119,7 +119,7 @@ def initialize(base_url:, **options) @buffer = '' # Buffer for partial SSE event data @elicitation_request_callback = nil # MCP 2025-06-18 @roots_list_request_callback = nil # MCP 2025-06-18 - @sampling_request_callback = nil # MCP 2025-06-18 + @sampling_request_callback = nil # MCP 2025-11-25 end # Connect to the MCP server over Streamable HTTP @@ -496,7 +496,7 @@ def on_roots_list_request(&block) @roots_list_request_callback = block end - # Register a callback for sampling requests (MCP 2025-06-18) + # Register a callback for sampling requests (MCP 2025-11-25) # @param block [Proc] callback that receives (request_id, params) and returns response hash # @return [void] def on_sampling_request(&block) @@ -920,7 +920,7 @@ def handle_roots_list(request_id, params) send_roots_list_response(request_id, result) end - # Handle sampling/createMessage request from server (MCP 2025-06-18) + # Handle sampling/createMessage request from server (MCP 2025-11-25) # @param request_id [String, Integer] the JSON-RPC request ID # @param params [Hash] the sampling parameters # @return [void] @@ -956,7 +956,7 @@ def send_roots_list_response(request_id, result) @logger.error("Error sending roots/list response: #{e.message}") end - # Send sampling response back to server via HTTP POST (MCP 2025-06-18) + # Send sampling response back to server via HTTP POST (MCP 2025-11-25) # @param request_id [String, Integer] the JSON-RPC request ID # @param result [Hash] the sampling result (role, content, model, stopReason) # @return [void] diff --git a/spec/lib/mcp_client/client_sampling_spec.rb b/spec/lib/mcp_client/client_sampling_spec.rb index 966dcf6..91aacfd 100644 --- a/spec/lib/mcp_client/client_sampling_spec.rb +++ b/spec/lib/mcp_client/client_sampling_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe MCPClient::Client, 'Sampling (MCP 2025-06-18)' do +RSpec.describe MCPClient::Client, 'Sampling (MCP 2025-11-25)' do let(:mock_server) { instance_double(MCPClient::ServerStdio, name: 'stdio-server') } before do @@ -68,9 +68,18 @@ 'messages' => [ { 'role' => 'user', 'content' => { 'type' => 'text', 'text' => 'Hello, Claude!' } } ], - 'modelPreferences' => { 'hints' => [{ 'name' => 'claude-3-sonnet' }] }, + 'modelPreferences' => { + 'hints' => [{ 'name' => 'claude-3-sonnet' }], + 'costPriority' => 0.3, + 'speedPriority' => 0.8, + 'intelligencePriority' => 0.5 + }, 'systemPrompt' => 'You are a helpful assistant.', - 'maxTokens' => 1024 + 'maxTokens' => 1024, + 'includeContext' => 'thisServer', + 'temperature' => 0.7, + 'stopSequences' => ['END'], + 'metadata' => { 'requestId' => 'abc-123' } } end @@ -86,7 +95,7 @@ end context 'when sampling handler is configured' do - it 'calls the handler with messages' do + it 'calls the handler with messages (arity 1)' do handler_called = false handler = lambda do |messages| handler_called = true @@ -107,6 +116,9 @@ handler_called = true expect(messages).to be_an(Array) expect(model_prefs['hints'].first['name']).to eq('claude-3-sonnet') + expect(model_prefs['costPriority']).to eq(0.3) + expect(model_prefs['speedPriority']).to eq(0.8) + expect(model_prefs['intelligencePriority']).to eq(0.5) { 'role' => 'assistant', 'content' => { 'type' => 'text', 'text' => 'Hello!' } } end @@ -116,7 +128,23 @@ expect(handler_called).to be true end - it 'calls the handler with all parameters (arity 4)' do + it 'calls the handler with messages, model preferences, and system prompt (arity 3)' do + handler_called = false + handler = lambda do |messages, model_prefs, system_prompt| + handler_called = true + expect(messages).to be_an(Array) + expect(model_prefs['hints'].first['name']).to eq('claude-3-sonnet') + expect(system_prompt).to eq('You are a helpful assistant.') + { 'role' => 'assistant', 'content' => { 'type' => 'text', 'text' => 'Hello!' } } + end + + client = described_class.new(sampling_handler: handler) + client.send(:handle_sampling_request, request_id, params) + + expect(handler_called).to be true + end + + it 'calls the handler with all core parameters (arity 4)' do handler_called = false handler = lambda do |messages, model_prefs, system_prompt, max_tokens| handler_called = true @@ -133,6 +161,27 @@ expect(handler_called).to be true end + it 'calls the handler with extended parameters (arity 5+)' do + handler_called = false + handler = lambda do |messages, model_prefs, system_prompt, max_tokens, extra| + handler_called = true + expect(messages).to be_an(Array) + expect(model_prefs['hints'].first['name']).to eq('claude-3-sonnet') + expect(system_prompt).to eq('You are a helpful assistant.') + expect(max_tokens).to eq(1024) + expect(extra['includeContext']).to eq('thisServer') + expect(extra['temperature']).to eq(0.7) + expect(extra['stopSequences']).to eq(['END']) + expect(extra['metadata']).to eq({ 'requestId' => 'abc-123' }) + { 'role' => 'assistant', 'content' => { 'type' => 'text', 'text' => 'Hello!' } } + end + + client = described_class.new(sampling_handler: handler) + client.send(:handle_sampling_request, request_id, params) + + expect(handler_called).to be true + end + it 'formats valid response correctly' do handler = lambda do |_messages| { @@ -219,6 +268,246 @@ end end + describe '#normalize_model_preferences' do + let(:client) { described_class.new } + + it 'returns nil for nil input' do + result = client.send(:normalize_model_preferences, nil) + expect(result).to be_nil + end + + it 'returns nil for non-hash input' do + result = client.send(:normalize_model_preferences, 'invalid') + expect(result).to be_nil + end + + it 'normalizes hints array with name fields' do + prefs = { 'hints' => [{ 'name' => 'claude-3-sonnet' }, { 'name' => 'gpt-4' }] } + result = client.send(:normalize_model_preferences, prefs) + + expect(result['hints']).to eq([{ 'name' => 'claude-3-sonnet' }, { 'name' => 'gpt-4' }]) + end + + it 'filters out hints without name' do + prefs = { 'hints' => [{ 'name' => 'claude-3-sonnet' }, { 'other' => 'value' }, 'invalid'] } + result = client.send(:normalize_model_preferences, prefs) + + expect(result['hints']).to eq([{ 'name' => 'claude-3-sonnet' }]) + end + + it 'converts hint name to string' do + prefs = { 'hints' => [{ 'name' => :claude_sonnet }] } + result = client.send(:normalize_model_preferences, prefs) + + expect(result['hints']).to eq([{ 'name' => 'claude_sonnet' }]) + end + + it 'normalizes costPriority within 0.0 to 1.0' do + prefs = { 'costPriority' => 0.5 } + result = client.send(:normalize_model_preferences, prefs) + + expect(result['costPriority']).to eq(0.5) + end + + it 'clamps costPriority above 1.0 to 1.0' do + prefs = { 'costPriority' => 1.5 } + result = client.send(:normalize_model_preferences, prefs) + + expect(result['costPriority']).to eq(1.0) + end + + it 'clamps costPriority below 0.0 to 0.0' do + prefs = { 'costPriority' => -0.5 } + result = client.send(:normalize_model_preferences, prefs) + + expect(result['costPriority']).to eq(0.0) + end + + it 'normalizes speedPriority' do + prefs = { 'speedPriority' => 0.8 } + result = client.send(:normalize_model_preferences, prefs) + + expect(result['speedPriority']).to eq(0.8) + end + + it 'normalizes intelligencePriority' do + prefs = { 'intelligencePriority' => 0.9 } + result = client.send(:normalize_model_preferences, prefs) + + expect(result['intelligencePriority']).to eq(0.9) + end + + it 'sets non-numeric priority to nil' do + prefs = { 'costPriority' => 'high' } + result = client.send(:normalize_model_preferences, prefs) + + expect(result['costPriority']).to be_nil + end + + it 'handles all priority fields together' do + prefs = { + 'hints' => [{ 'name' => 'claude-3-opus' }], + 'costPriority' => 0.2, + 'speedPriority' => 0.7, + 'intelligencePriority' => 1.0 + } + result = client.send(:normalize_model_preferences, prefs) + + expect(result['hints']).to eq([{ 'name' => 'claude-3-opus' }]) + expect(result['costPriority']).to eq(0.2) + expect(result['speedPriority']).to eq(0.7) + expect(result['intelligencePriority']).to eq(1.0) + end + + it 'handles integer priority values by converting to float' do + prefs = { 'costPriority' => 1 } + result = client.send(:normalize_model_preferences, prefs) + + expect(result['costPriority']).to eq(1.0) + expect(result['costPriority']).to be_a(Float) + end + + it 'omits priority keys not present in input' do + prefs = { 'hints' => [{ 'name' => 'model-a' }] } + result = client.send(:normalize_model_preferences, prefs) + + expect(result).not_to have_key('costPriority') + expect(result).not_to have_key('speedPriority') + expect(result).not_to have_key('intelligencePriority') + end + end + + describe 'extended sampling parameters' do + let(:request_id) { 123 } + + context 'with includeContext parameter' do + it 'passes includeContext in extra params for arity 5+ handlers' do + received_extra = nil + handler = lambda do |_messages, _model_prefs, _system_prompt, _max_tokens, extra| + received_extra = extra + { 'role' => 'assistant', 'content' => { 'type' => 'text', 'text' => 'OK' } } + end + + client = described_class.new(sampling_handler: handler) + client.send(:handle_sampling_request, request_id, { + 'messages' => [{ 'role' => 'user', 'content' => { 'type' => 'text', 'text' => 'Hi' } }], + 'includeContext' => 'allServers' + }) + + expect(received_extra['includeContext']).to eq('allServers') + end + end + + context 'with temperature parameter' do + it 'passes temperature in extra params' do + received_extra = nil + handler = lambda do |_messages, _model_prefs, _system_prompt, _max_tokens, extra| + received_extra = extra + { 'role' => 'assistant', 'content' => { 'type' => 'text', 'text' => 'OK' } } + end + + client = described_class.new(sampling_handler: handler) + client.send(:handle_sampling_request, request_id, { + 'messages' => [{ 'role' => 'user', 'content' => { 'type' => 'text', 'text' => 'Hi' } }], + 'temperature' => 0.9 + }) + + expect(received_extra['temperature']).to eq(0.9) + end + end + + context 'with stopSequences parameter' do + it 'passes stopSequences in extra params' do + received_extra = nil + handler = lambda do |_messages, _model_prefs, _system_prompt, _max_tokens, extra| + received_extra = extra + { 'role' => 'assistant', 'content' => { 'type' => 'text', 'text' => 'OK' } } + end + + client = described_class.new(sampling_handler: handler) + client.send(:handle_sampling_request, request_id, { + 'messages' => [{ 'role' => 'user', 'content' => { 'type' => 'text', 'text' => 'Hi' } }], + 'stopSequences' => %w[STOP END] + }) + + expect(received_extra['stopSequences']).to eq(%w[STOP END]) + end + end + + context 'with metadata parameter' do + it 'passes metadata in extra params' do + received_extra = nil + handler = lambda do |_messages, _model_prefs, _system_prompt, _max_tokens, extra| + received_extra = extra + { 'role' => 'assistant', 'content' => { 'type' => 'text', 'text' => 'OK' } } + end + + client = described_class.new(sampling_handler: handler) + client.send(:handle_sampling_request, request_id, { + 'messages' => [{ 'role' => 'user', 'content' => { 'type' => 'text', 'text' => 'Hi' } }], + 'metadata' => { 'source' => 'test', 'priority' => 'high' } + }) + + expect(received_extra['metadata']).to eq({ 'source' => 'test', 'priority' => 'high' }) + end + end + + context 'when extended params are absent' do + it 'passes nil values in extra params hash' do + received_extra = nil + handler = lambda do |_messages, _model_prefs, _system_prompt, _max_tokens, extra| + received_extra = extra + { 'role' => 'assistant', 'content' => { 'type' => 'text', 'text' => 'OK' } } + end + + client = described_class.new(sampling_handler: handler) + client.send(:handle_sampling_request, request_id, { + 'messages' => [{ 'role' => 'user', 'content' => { 'type' => 'text', 'text' => 'Hi' } }] + }) + + expect(received_extra['includeContext']).to be_nil + expect(received_extra['temperature']).to be_nil + expect(received_extra['stopSequences']).to be_nil + expect(received_extra['metadata']).to be_nil + end + end + end + + describe 'systemPrompt handling' do + let(:request_id) { 123 } + + it 'passes systemPrompt to arity-3 handler' do + received_prompt = nil + handler = lambda do |_messages, _model_prefs, system_prompt| + received_prompt = system_prompt + { 'role' => 'assistant', 'content' => { 'type' => 'text', 'text' => 'OK' } } + end + + client = described_class.new(sampling_handler: handler) + client.send(:handle_sampling_request, request_id, { + 'messages' => [{ 'role' => 'user', 'content' => { 'type' => 'text', 'text' => 'Hi' } }], + 'systemPrompt' => 'Be concise and accurate.' + }) + + expect(received_prompt).to eq('Be concise and accurate.') + end + + it 'passes nil systemPrompt when not provided' do + received_prompt = :not_set + handler = lambda do |_messages, _model_prefs, system_prompt| + received_prompt = system_prompt + { 'role' => 'assistant', 'content' => { 'type' => 'text', 'text' => 'OK' } } + end + + client = described_class.new(sampling_handler: handler) + client.send(:handle_sampling_request, request_id, { + 'messages' => [{ 'role' => 'user', 'content' => { 'type' => 'text', 'text' => 'Hi' } }] + }) + + expect(received_prompt).to be_nil + end + end + describe 'sampling handler integration with servers' do it 'forwards handler method reference to server' do handler = ->(_messages) { { 'role' => 'assistant', 'content' => { 'type' => 'text', 'text' => 'Hello' } } } @@ -247,5 +536,39 @@ expect(result['role']).to eq('assistant') expect(result['content']['text']).to eq('Hello') end + + it 'passes full params including modelPreferences through server callback' do + received_prefs = nil + handler = lambda do |_messages, model_prefs| + received_prefs = model_prefs + { 'role' => 'assistant', 'content' => { 'type' => 'text', 'text' => 'OK' } } + end + + registered_callback = nil + allow(mock_server).to receive(:on_sampling_request) do |&block| + registered_callback = block + end + + described_class.new( + mcp_server_configs: [{ type: 'stdio', command: 'test' }], + sampling_handler: handler + ) + + registered_callback.call(789, { + 'messages' => [{ 'role' => 'user', + 'content' => { 'type' => 'text', 'text' => 'Hi' } }], + 'modelPreferences' => { + 'hints' => [{ 'name' => 'claude-3-opus' }], + 'costPriority' => 0.1, + 'speedPriority' => 0.3, + 'intelligencePriority' => 0.9 + } + }) + + expect(received_prefs['hints']).to eq([{ 'name' => 'claude-3-opus' }]) + expect(received_prefs['costPriority']).to eq(0.1) + expect(received_prefs['speedPriority']).to eq(0.3) + expect(received_prefs['intelligencePriority']).to eq(0.9) + end end end diff --git a/spec/lib/mcp_client/json_rpc_common_spec.rb b/spec/lib/mcp_client/json_rpc_common_spec.rb index 79e3f4b..4e097b8 100644 --- a/spec/lib/mcp_client/json_rpc_common_spec.rb +++ b/spec/lib/mcp_client/json_rpc_common_spec.rb @@ -134,9 +134,9 @@ def rpc_request(_method, _params = {}) expect(params).to include( 'protocolVersion' => MCPClient::PROTOCOL_VERSION, 'capabilities' => { - 'elicitation' => {}, # MCP 2025-06-18 - 'roots' => { 'listChanged' => true }, # MCP 2025-06-18 - 'sampling' => {} # MCP 2025-06-18 + 'elicitation' => {}, # MCP 2025-11-25 + 'roots' => { 'listChanged' => true }, # MCP 2025-11-25 + 'sampling' => {} # MCP 2025-11-25 }, 'clientInfo' => { 'name' => 'ruby-mcp-client',