Class: MCPClient::Client
- Inherits:
-
Object
- Object
- MCPClient::Client
- Defined in:
- lib/mcp_client/client.rb
Overview
MCP Client for integrating with the Model Context Protocol This is the main entry point for using MCP tools
Constant Summary collapse
- SUPPORTED_ELICITATION_MODES =
Elicitation modes implemented by this client (MCP 2025-11-25). Requests with a mode outside this set are rejected with -32602.
%w[form url].freeze
- STRUCTURED_CONTENT_MODES =
Supported modes for structuredContent validation (MCP 2025-11-25): :warn logs a warning on mismatch, :strict raises a ValidationError.
%i[warn strict].freeze
Instance Attribute Summary collapse
-
#logger ⇒ Logger
readonly
Logger for client operations.
-
#prompt_cache ⇒ Hash<String, MCPClient::Prompt>
readonly
Cache of prompts by composite key (server_id:name).
-
#resource_cache ⇒ Hash<String, MCPClient::Resource>
readonly
Cache of resources by composite key (server_id:uri).
-
#roots ⇒ Object
Returns the value of attribute roots.
-
#servers ⇒ Array<MCPClient::ServerBase>
readonly
List of servers.
-
#tool_cache ⇒ Hash<String, MCPClient::Tool>
readonly
Cache of tools by composite key (server_id:name).
Instance Method Summary collapse
-
#call_tool(tool_name, parameters, server: nil, progress: nil) ⇒ Object
Calls a specific tool by name with the given parameters.
-
#call_tool_as_task(tool_name, parameters, ttl: nil, server: nil) ⇒ MCPClient::Task
Call a tool as a task (task-augmented tools/call, MCP 2025-11-25).
-
#call_tool_streaming(tool_name, parameters, server: nil) ⇒ Enumerator
Stream call of a specific tool by name with the given parameters.
-
#call_tools(calls) ⇒ Array<Object>
Call multiple tools in batch.
-
#cancel_task(task_id, server: nil) ⇒ MCPClient::Task
Cancel a task (tasks/cancel, MCP 2025-11-25).
-
#cleanup ⇒ Object
Clean up all server connections.
-
#clear_cache ⇒ void
Clear the cached tools so that next list_tools will fetch fresh data.
-
#complete(ref:, argument:, context: nil, server: nil) ⇒ Hash
Request completion suggestions from a server (MCP 2025-06-18).
-
#find_server(name) ⇒ MCPClient::ServerBase?
Find a server by name.
-
#find_tool(pattern) ⇒ MCPClient::Tool?
Find the first tool whose name matches the given pattern.
-
#find_tools(pattern) ⇒ Array<MCPClient::Tool>
Find all tools whose name matches the given pattern (String or Regexp).
-
#get_prompt(prompt_name, parameters, server: nil) ⇒ Object
Gets a specific prompt by name with the given parameters.
-
#get_task(task_id, server: nil) ⇒ MCPClient::Task
Get the current state of a task (tasks/get, MCP 2025-11-25).
-
#get_task_result(task_id, server: nil) ⇒ Object
Retrieve the result of a completed task (tasks/result, MCP 2025-11-25).
-
#initialize(mcp_server_configs: [], logger: nil, elicitation_handler: nil, roots: nil, sampling_handler: nil, sampling_supports_tools: false, client_info: nil, validate_structured_content: :warn) ⇒ Client
constructor
Initialize a new MCPClient::Client.
-
#list_prompts(cache: true) ⇒ Array<MCPClient::Prompt>
Lists all available prompts from all connected MCP servers.
-
#list_resources(cache: true, cursor: nil) ⇒ Hash
Lists all available resources from all connected MCP servers.
-
#list_tasks(cursor: nil, server: nil) ⇒ Hash
List tasks known to a server (tasks/list, paginated, MCP 2025-11-25).
-
#list_tools(cache: true) ⇒ Array<MCPClient::Tool>
Lists all available tools from all connected MCP servers.
-
#log_level=(level) ⇒ Array<Hash>
Set the logging level on all connected servers (MCP 2025-06-18) To set on a specific server, use: client.find_server('name').log_level = 'debug'.
-
#on_notification {|server, method, params| ... } ⇒ void
Register a callback for JSON-RPC notifications from servers.
-
#ping(server_index: nil) ⇒ Object
Ping the MCP server to check connectivity (zero-parameter heartbeat call).
-
#read_resource(uri, server: nil) ⇒ Object
Reads a specific resource by URI.
-
#send_notification(method, params: {}, server: nil) ⇒ void
Send a raw JSON-RPC notification to a server (no response expected).
-
#send_rpc(method, params: {}, server: nil, timeout: nil) ⇒ Object
Send a raw JSON-RPC request to a server.
-
#to_anthropic_tools(tool_names: nil) ⇒ Array<Hash>
Convert MCP tools to Anthropic Claude tool specifications.
-
#to_google_tools(tool_names: nil) ⇒ Array<Hash>
Convert MCP tools to Google Vertex AI tool specifications.
-
#to_openai_tools(tool_names: nil) ⇒ Array<Hash>
Convert MCP tools to OpenAI function specifications.
Constructor Details
#initialize(mcp_server_configs: [], logger: nil, elicitation_handler: nil, roots: nil, sampling_handler: nil, sampling_supports_tools: false, client_info: nil, validate_structured_content: :warn) ⇒ Client
Initialize a new MCPClient::Client
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/mcp_client/client.rb', line 47 def initialize(mcp_server_configs: [], logger: nil, elicitation_handler: nil, roots: nil, sampling_handler: nil, sampling_supports_tools: false, client_info: nil, validate_structured_content: :warn) unless STRUCTURED_CONTENT_MODES.include?(validate_structured_content) raise ArgumentError, "validate_structured_content must be one of #{STRUCTURED_CONTENT_MODES.inspect}, " \ "got #{validate_structured_content.inspect}" end @validate_structured_content = validate_structured_content # Preserve a caller-supplied logger's formatter (only tag progname), and # install the default formatter solely on a logger we create ourselves. # Overwriting the formatter of an application's logger would silently # reformat every log line it emits elsewhere. if logger @logger = logger @logger.progname = self.class.name else @logger = Logger.new($stdout, level: Logger::WARN) @logger.progname = self.class.name @logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" } end @servers = mcp_server_configs.map do |config| @logger.debug("Creating server with config: #{config.inspect}") MCPClient::ServerFactory.create(config, logger: @logger) end @tool_cache = {} # Active progressToken -> callback registrations (MCP progress utility) @progress_callbacks = {} @progress_mutex = Mutex.new @prompt_cache = {} @resource_cache = {} # JSON-RPC notification listeners @notification_listeners = [] # Elicitation handler (MCP 2025-06-18) @elicitation_handler = elicitation_handler # Sampling handler (MCP 2025-11-25) @sampling_handler = sampling_handler # Whether the sampling handler supports tool use (SEP-1577) @sampling_supports_tools = sampling_supports_tools # Roots (MCP 2025-06-18) @roots = normalize_roots(roots) # Register default and user-defined notification handlers on each server @servers.each do |server| # Host-provided Implementation info for the initialize clientInfo server.client_info = client_info if client_info && server.respond_to?(:client_info=) server.on_notification do |method, params| # Default notification processing (e.g., cache invalidation, logging) process_notification(server, method, params) # Invoke user-defined listeners @notification_listeners.each { |cb| cb.call(server, method, params) } end # Register feature callbacks only for features the host actually # supports: transports derive their declared client capabilities from # the callbacks registered before connecting, and MCP forbids using # capabilities that were not negotiated. if @elicitation_handler && server.respond_to?(:on_elicitation_request) server.on_elicitation_request(&method(:handle_elicitation_request)) end # The client always implements the roots feature (roots/list and # list_changed notifications), independent of the current roots list. server.on_roots_list_request(&method(:handle_roots_list_request)) if server.respond_to?(:on_roots_list_request) next unless @sampling_handler && server.respond_to?(:on_sampling_request) server.on_sampling_request(&method(:handle_sampling_request)) # Declare the sampling.tools sub-capability (SEP-1577) only when the # host opted in; the transport derives its initialize declaration # from this before connecting. server.declare_sampling_tools if @sampling_supports_tools && server.respond_to?(:declare_sampling_tools) end end |
Instance Attribute Details
#logger ⇒ Logger (readonly)
Returns logger for client operations.
26 |
# File 'lib/mcp_client/client.rb', line 26 attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots |
#prompt_cache ⇒ Hash<String, MCPClient::Prompt> (readonly)
Returns cache of prompts by composite key (server_id:name).
26 |
# File 'lib/mcp_client/client.rb', line 26 attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots |
#resource_cache ⇒ Hash<String, MCPClient::Resource> (readonly)
Returns cache of resources by composite key (server_id:uri).
26 |
# File 'lib/mcp_client/client.rb', line 26 attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots |
#roots ⇒ Object
Returns the value of attribute roots.
26 |
# File 'lib/mcp_client/client.rb', line 26 attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots |
#servers ⇒ Array<MCPClient::ServerBase> (readonly)
Returns list of servers.
26 27 28 |
# File 'lib/mcp_client/client.rb', line 26 def servers @servers end |
#tool_cache ⇒ Hash<String, MCPClient::Tool> (readonly)
Returns cache of tools by composite key (server_id:name).
26 |
# File 'lib/mcp_client/client.rb', line 26 attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots |
Instance Method Details
#call_tool(tool_name, parameters, server: nil, progress: nil) ⇒ Object
Calls a specific tool by name with the given parameters
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 |
# File 'lib/mcp_client/client.rb', line 303 def call_tool(tool_name, parameters, server: nil, progress: nil) tool = resolve_tool(tool_name, server: server) # Validate parameters against tool schema validate_params!(tool, parameters) reject_task_required!(tool, tool_name) # Use the tool's associated server server = tool.server raise MCPClient::Errors::ServerNotFound, "No server found for tool '#{tool_name}'" unless server # MCP progress utility: attach an auto-generated progressToken to the # request _meta and route matching notifications/progress to the # caller's callback while the request is active. parameters, token = setup_progress_tracking(parameters, progress) result = begin server.call_tool(tool_name, parameters) rescue MCPClient::Errors::ConnectionError => e # Add server identity information to the error for better context server_id = server.name ? "#{server.class}[#{server.name}]" : server.class.name raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.} (Server: #{server_id})" ensure # Tokens are only valid for the lifetime of the request: dropping the # registration filters out stale post-completion notifications. unregister_progress_callback(token) if token end validate_structured_content!(tool, result) end |
#call_tool_as_task(tool_name, parameters, ttl: nil, server: nil) ⇒ MCPClient::Task
Call a tool as a task (task-augmented tools/call, MCP 2025-11-25).
Instead of blocking for the result, the server accepts the request and immediately returns a task handle; the actual result is retrieved later via #get_task_result once the task reaches a terminal status. The server must advertise the tasks.requests.tools.call capability, and the tool must declare execution.taskSupport of 'optional' or 'required'.
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 |
# File 'lib/mcp_client/client.rb', line 530 def call_tool_as_task(tool_name, parameters, ttl: nil, server: nil) tool = resolve_tool(tool_name, server: server) validate_params!(tool, parameters) srv = tool.server raise MCPClient::Errors::ServerNotFound, "No server found for tool '#{tool_name}'" unless srv unless server_supports_task_tool_call?(srv) raise MCPClient::Errors::TaskError, 'Server does not support task-augmented tools/call (no tasks.requests.tools.call capability)' end unless tool.supports_task? raise MCPClient::Errors::TaskError, "Tool '#{tool_name}' does not support task execution (execution.taskSupport is forbidden/unset)" end task_params = {} task_params[:ttl] = ttl if ttl # Keep _meta (string or symbol key) as a top-level request field rather # than a tool argument, so request metadata is preserved and does not fail # tool input-schema validation. = [:_meta, '_meta'].find { |k| parameters.key?(k) } arguments = ? parameters.reject { |k, _| k == } : parameters rpc_params = { name: tool_name, arguments: arguments, task: task_params } rpc_params[:_meta] = parameters[] if begin result = srv.rpc_request('tools/call', rpc_params) MCPClient::Task.from_create_result(result, server: srv) rescue MCPClient::Errors::ServerError, MCPClient::Errors::TransportError, MCPClient::Errors::ConnectionError => e raise MCPClient::Errors::TaskError, "Error creating task for tool '#{tool_name}': #{e.}" end end |
#call_tool_streaming(tool_name, parameters, server: nil) ⇒ Enumerator
Stream call of a specific tool by name with the given parameters. Returns an Enumerator yielding streaming updates if supported.
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 |
# File 'lib/mcp_client/client.rb', line 434 def call_tool_streaming(tool_name, parameters, server: nil) tool = resolve_tool(tool_name, server: server) # Validate parameters against tool schema validate_params!(tool, parameters) reject_task_required!(tool, tool_name) # Use the tool's associated server server = tool.server raise MCPClient::Errors::ServerNotFound, "No server found for tool '#{tool_name}'" unless server begin # Use the streaming API if it's available server.call_tool_streaming(tool_name, parameters) rescue MCPClient::Errors::ConnectionError => e # Add server identity information to the error for better context server_id = server.name ? "#{server.class}[#{server.name}]" : server.class.name msg = "Error calling streaming tool '#{tool_name}': #{e.} (Server: #{server_id})" raise MCPClient::Errors::ToolCallError, msg end end |
#call_tools(calls) ⇒ Array<Object>
Call multiple tools in batch
419 420 421 422 423 424 425 426 |
# File 'lib/mcp_client/client.rb', line 419 def call_tools(calls) calls.map do |call| name = call[:name] || call['name'] params = call[:parameters] || call['parameters'] || {} server = call[:server] || call['server'] call_tool(name, params, server: server) end end |
#cancel_task(task_id, server: nil) ⇒ MCPClient::Task
Cancel a task (tasks/cancel, MCP 2025-11-25)
638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 |
# File 'lib/mcp_client/client.rb', line 638 def cancel_task(task_id, server: nil) srv = select_server(server) ensure_task_capability!(srv, 'cancel') begin result = srv.rpc_request('tasks/cancel', { taskId: task_id }) MCPClient::Task.from_json(result, server: srv) rescue MCPClient::Errors::ServerError => e # A terminal task cannot be cancelled (-32602); that is an error, not a # missing task, so keep it as a TaskError. if e..match?(/terminal/i) raise MCPClient::Errors::TaskError, "Error cancelling task '#{task_id}': #{e.}" end raise task_error_from(e, task_id, 'cancelling') rescue MCPClient::Errors::TransportError, MCPClient::Errors::ConnectionError => e raise MCPClient::Errors::TaskError, "Error cancelling task '#{task_id}': #{e.}" end end |
#cleanup ⇒ Object
Clean up all server connections
362 363 364 |
# File 'lib/mcp_client/client.rb', line 362 def cleanup servers.each(&:cleanup) end |
#clear_cache ⇒ void
This method returns an undefined value.
Clear the cached tools so that next list_tools will fetch fresh data
368 369 370 371 372 |
# File 'lib/mcp_client/client.rb', line 368 def clear_cache @tool_cache.clear @prompt_cache.clear @resource_cache.clear end |
#complete(ref:, argument:, context: nil, server: nil) ⇒ Hash
Request completion suggestions from a server (MCP 2025-06-18)
510 511 512 513 |
# File 'lib/mcp_client/client.rb', line 510 def complete(ref:, argument:, context: nil, server: nil) srv = select_server(server) srv.complete(ref: ref, argument: argument, context: context) end |
#find_server(name) ⇒ MCPClient::ServerBase?
Find a server by name
394 395 396 |
# File 'lib/mcp_client/client.rb', line 394 def find_server(name) @servers.find { |s| s.name == name } end |
#find_tool(pattern) ⇒ MCPClient::Tool?
Find the first tool whose name matches the given pattern
409 410 411 |
# File 'lib/mcp_client/client.rb', line 409 def find_tool(pattern) find_tools(pattern).first end |
#find_tools(pattern) ⇒ Array<MCPClient::Tool>
Find all tools whose name matches the given pattern (String or Regexp)
401 402 403 404 |
# File 'lib/mcp_client/client.rb', line 401 def find_tools(pattern) rx = pattern.is_a?(Regexp) ? pattern : /#{Regexp.escape(pattern)}/ list_tools.select { |t| t.name.match(rx) } end |
#get_prompt(prompt_name, parameters, server: nil) ⇒ Object
Gets a specific prompt by name with the given parameters
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/mcp_client/client.rb', line 153 def get_prompt(prompt_name, parameters, server: nil) prompts = list_prompts if server # Use the specified server srv = select_server(server) # Find the prompt on this specific server prompt = prompts.find { |t| t.name == prompt_name && t.server == srv } unless prompt raise MCPClient::Errors::PromptNotFound, "Prompt '#{prompt_name}' not found on server '#{srv.name || srv.class.name}'" end else # Find the prompt across all servers matching_prompts = prompts.select { |t| t.name == prompt_name } if matching_prompts.empty? raise MCPClient::Errors::PromptNotFound, "Prompt '#{prompt_name}' not found" elsif matching_prompts.size > 1 # If multiple matches, disambiguate with server names server_names = matching_prompts.map { |t| t.server&.name || 'unnamed' } raise MCPClient::Errors::AmbiguousPromptName, "Multiple prompts named '#{prompt_name}' found across servers (#{server_names.join(', ')}). " \ "Please specify a server using the 'server' parameter." end prompt = matching_prompts.first end # Use the prompt's associated server server = prompt.server raise MCPClient::Errors::ServerNotFound, "No server found for prompt '#{prompt_name}'" unless server begin server.get_prompt(prompt_name, parameters) rescue MCPClient::Errors::ConnectionError => e # Add server identity information to the error for better context server_id = server.name ? "#{server.class}[#{server.name}]" : server.class.name raise MCPClient::Errors::PromptGetError, "Error getting prompt '#{prompt_name}': #{e.} (Server: #{server_id})" end end |
#get_task(task_id, server: nil) ⇒ MCPClient::Task
Get the current state of a task (tasks/get, MCP 2025-11-25)
571 572 573 574 575 576 577 578 579 580 581 582 |
# File 'lib/mcp_client/client.rb', line 571 def get_task(task_id, server: nil) srv = select_server(server) begin result = srv.rpc_request('tasks/get', { taskId: task_id }) MCPClient::Task.from_json(result, server: srv) rescue MCPClient::Errors::ServerError => e raise task_error_from(e, task_id, 'getting') rescue MCPClient::Errors::TransportError, MCPClient::Errors::ConnectionError => e raise MCPClient::Errors::TaskError, "Error getting task '#{task_id}': #{e.}" end end |
#get_task_result(task_id, server: nil) ⇒ Object
Retrieve the result of a completed task (tasks/result, MCP 2025-11-25). Returns exactly what the underlying request would have returned (e.g. a CallToolResult hash with 'content'/'isError'/'structuredContent'); it is NOT wrapped in a Task. Blocks on the server until the task is terminal.
NOTE: structured-content validation (see #validate_structured_content!) does not cover task-delivered results yet: a task ID alone does not identify which tool (and therefore which outputSchema) produced the result, and the client keeps no task-to-tool registry. Callers who need validation here can run MCPClient::SchemaValidator.validate themselves.
599 600 601 602 603 604 605 606 607 608 609 |
# File 'lib/mcp_client/client.rb', line 599 def get_task_result(task_id, server: nil) srv = select_server(server) begin srv.rpc_request('tasks/result', { taskId: task_id }) rescue MCPClient::Errors::ServerError => e raise task_error_from(e, task_id, 'getting result for') rescue MCPClient::Errors::TransportError, MCPClient::Errors::ConnectionError => e raise MCPClient::Errors::TaskError, "Error getting result for task '#{task_id}': #{e.}" end end |
#list_prompts(cache: true) ⇒ Array<MCPClient::Prompt>
Lists all available prompts from all connected MCP servers
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/mcp_client/client.rb', line 122 def list_prompts(cache: true) return @prompt_cache.values if cache && !@prompt_cache.empty? prompts = [] connection_errors = [] servers.each do |server| server.list_prompts.each do |prompt| cache_key = cache_key_for(server, prompt.name) @prompt_cache[cache_key] = prompt prompts << prompt end rescue MCPClient::Errors::ConnectionError => e # Fast-fail on authorization errors for better user experience # If this is the first server or we haven't collected any prompts yet, # raise the auth error directly to avoid cascading error messages raise e if e..include?('Authorization failed') && prompts.empty? # Store the error and try other servers connection_errors << e @logger.error("Server error: #{e.}") end prompts end |
#list_resources(cache: true, cursor: nil) ⇒ Hash
Lists all available resources from all connected MCP servers
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/mcp_client/client.rb', line 202 def list_resources(cache: true, cursor: nil) # If cursor is provided, we can only query one server (the one that provided the cursor) # This is a limitation of aggregating multiple servers if cursor # For now, just use the first server when cursor is provided # In a real implementation, you'd need to track which server the cursor came from return servers.first.list_resources(cursor: cursor) if servers.any? return { 'resources' => [], 'nextCursor' => nil } end # Use cache if available and no cursor return { 'resources' => @resource_cache.values, 'nextCursor' => nil } if cache && !@resource_cache.empty? resources = [] connection_errors = [] servers.each do |server| result = server.list_resources resource_list = result['resources'] || [] resource_list.each do |resource| cache_key = cache_key_for(server, resource.uri) @resource_cache[cache_key] = resource resources << resource end rescue MCPClient::Errors::ConnectionError => e # Fast-fail on authorization errors for better user experience # If this is the first server or we haven't collected any resources yet, # raise the auth error directly to avoid cascading error messages raise e if e..include?('Authorization failed') && resources.empty? # Store the error and try other servers connection_errors << e @logger.error("Server error: #{e.}") end # Return hash format consistent with server methods { 'resources' => resources, 'nextCursor' => nil } end |
#list_tasks(cursor: nil, server: nil) ⇒ Hash
List tasks known to a server (tasks/list, paginated, MCP 2025-11-25)
616 617 618 619 620 621 622 623 624 625 626 627 628 629 |
# File 'lib/mcp_client/client.rb', line 616 def list_tasks(cursor: nil, server: nil) srv = select_server(server) ensure_task_capability!(srv, 'list') params = cursor ? { cursor: cursor } : {} begin result = srv.rpc_request('tasks/list', params) || {} tasks = (result['tasks'] || []).map { |t| MCPClient::Task.from_json(t, server: srv) } { tasks: tasks, next_cursor: result['nextCursor'] } rescue MCPClient::Errors::ServerError, MCPClient::Errors::TransportError, MCPClient::Errors::ConnectionError => e raise MCPClient::Errors::TaskError, "Error listing tasks: #{e.}" end end |
#list_tools(cache: true) ⇒ Array<MCPClient::Tool>
Lists all available tools from all connected MCP servers
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 |
# File 'lib/mcp_client/client.rb', line 265 def list_tools(cache: true) return @tool_cache.values if cache && !@tool_cache.empty? tools = [] connection_errors = [] servers.each do |server| server.list_tools.each do |tool| cache_key = cache_key_for(server, tool.name) @tool_cache[cache_key] = tool tools << tool end rescue MCPClient::Errors::ConnectionError => e # Fast-fail on authorization errors for better user experience # If this is the first server or we haven't collected any tools yet, # raise the auth error directly to avoid cascading error messages raise e if e..include?('Authorization failed') && tools.empty? # Store the error and try other servers connection_errors << e @logger.error("Server error: #{e.}") end # If we didn't get any tools from any server but have servers configured, report failure if tools.empty? && !servers.empty? raise connection_errors.first if connection_errors.any? @logger.warn('No tools found from any server.') end tools end |
#log_level=(level) ⇒ Array<Hash>
Set the logging level on all connected servers (MCP 2025-06-18) To set on a specific server, use: client.find_server('name').log_level = 'debug'
664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 |
# File 'lib/mcp_client/client.rb', line 664 def log_level=(level) @servers.filter_map do |srv| # MCP lifecycle: only use capabilities that were successfully # negotiated — skip servers whose NEGOTIATED set lacks logging. # Unconnected servers proceed: the transport-level gate re-checks # after its handshake establishes the capability set. unless !capabilities_known?(srv) || srv.capability?('logging') @logger.debug("Skipping logging/setLevel for #{srv.name || srv.class.name}: " \ 'logging capability not negotiated') next end srv.log_level = level end end |
#on_notification {|server, method, params| ... } ⇒ void
This method returns an undefined value.
Register a callback for JSON-RPC notifications from servers
377 378 379 |
# File 'lib/mcp_client/client.rb', line 377 def on_notification(&block) @notification_listeners << block end |
#ping(server_index: nil) ⇒ Object
Ping the MCP server to check connectivity (zero-parameter heartbeat call)
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 |
# File 'lib/mcp_client/client.rb', line 460 def ping(server_index: nil) if server_index.nil? # Ping first available server raise MCPClient::Errors::ServerNotFound, 'No server available for ping' if @servers.empty? @servers.first.ping else # Ping specified server if server_index >= @servers.length raise MCPClient::Errors::ServerNotFound, "Server at index #{server_index} not found" end @servers[server_index].ping end end |
#read_resource(uri, server: nil) ⇒ Object
Reads a specific resource by URI
247 248 249 250 251 252 253 254 255 256 257 258 |
# File 'lib/mcp_client/client.rb', line 247 def read_resource(uri, server: nil) result = list_resources resources = result['resources'] || [] resource = if server find_resource_on_server(uri, resources, server) else find_resource_across_servers(uri, resources) end execute_resource_read(resource, uri) end |
#send_notification(method, params: {}, server: nil) ⇒ void
This method returns an undefined value.
Send a raw JSON-RPC notification to a server (no response expected)
496 497 498 499 |
# File 'lib/mcp_client/client.rb', line 496 def send_notification(method, params: {}, server: nil) srv = select_server(server) srv.rpc_notify(method, params) end |
#send_rpc(method, params: {}, server: nil, timeout: nil) ⇒ Object
Send a raw JSON-RPC request to a server
482 483 484 485 486 487 488 489 |
# File 'lib/mcp_client/client.rb', line 482 def send_rpc(method, params: {}, server: nil, timeout: nil) srv = select_server(server) # Only pass the per-request timeout when set, so transports (and test # doubles) with the two-argument signature keep working. return srv.rpc_request(method, params) unless timeout srv.rpc_request(method, params, timeout: timeout) end |
#to_anthropic_tools(tool_names: nil) ⇒ Array<Hash>
Convert MCP tools to Anthropic Claude tool specifications
346 347 348 349 350 |
# File 'lib/mcp_client/client.rb', line 346 def to_anthropic_tools(tool_names: nil) tools = list_tools tools = tools.select { |t| tool_names.include?(t.name) } if tool_names tools.map(&:to_anthropic_tool) end |
#to_google_tools(tool_names: nil) ⇒ Array<Hash>
Convert MCP tools to Google Vertex AI tool specifications
355 356 357 358 359 |
# File 'lib/mcp_client/client.rb', line 355 def to_google_tools(tool_names: nil) tools = list_tools tools = tools.select { |t| tool_names.include?(t.name) } if tool_names tools.map(&:to_google_tool) end |
#to_openai_tools(tool_names: nil) ⇒ Array<Hash>
Convert MCP tools to OpenAI function specifications
337 338 339 340 341 |
# File 'lib/mcp_client/client.rb', line 337 def to_openai_tools(tool_names: nil) tools = list_tools tools = tools.select { |t| tool_names.include?(t.name) } if tool_names tools.map(&:to_openai_tool) end |