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
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) ⇒ Object
Calls a specific tool by name with the given parameters.
-
#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.
-
#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:, 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.
-
#initialize(mcp_server_configs: [], logger: nil, elicitation_handler: nil, roots: nil, sampling_handler: nil) ⇒ 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_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) ⇒ 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) ⇒ Client
Initialize a new MCPClient::Client
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/mcp_client/client.rb', line 29 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 @logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" } @servers = mcp_server_configs.map do |config| @logger.debug("Creating server with config: #{config.inspect}") MCPClient::ServerFactory.create(config, logger: @logger) end @tool_cache = {} @prompt_cache = {} @resource_cache = {} # JSON-RPC notification listeners @notification_listeners = [] # Elicitation handler (MCP 2025-06-18) @elicitation_handler = elicitation_handler # Sampling handler (MCP 2025-06-18) @sampling_handler = sampling_handler # Roots (MCP 2025-06-18) @roots = normalize_roots(roots) # Register default and user-defined notification handlers on each server @servers.each do |server| 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 elicitation handler on each server if server.respond_to?(:on_elicitation_request) server.on_elicitation_request(&method(:handle_elicitation_request)) 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) server.on_sampling_request(&method(:handle_sampling_request)) if server.respond_to?(:on_sampling_request) end end |
Instance Attribute Details
#logger ⇒ Logger (readonly)
21 |
# File 'lib/mcp_client/client.rb', line 21 attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots |
#prompt_cache ⇒ Hash<String, MCPClient::Prompt> (readonly)
21 |
# File 'lib/mcp_client/client.rb', line 21 attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots |
#resource_cache ⇒ Hash<String, MCPClient::Resource> (readonly)
21 |
# File 'lib/mcp_client/client.rb', line 21 attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots |
#roots ⇒ Object
Returns the value of attribute roots.
21 |
# File 'lib/mcp_client/client.rb', line 21 attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots |
#servers ⇒ Array<MCPClient::ServerBase> (readonly)
21 22 23 |
# File 'lib/mcp_client/client.rb', line 21 def servers @servers end |
#tool_cache ⇒ Hash<String, MCPClient::Tool> (readonly)
21 |
# File 'lib/mcp_client/client.rb', line 21 attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots |
Instance Method Details
#call_tool(tool_name, parameters, server: nil) ⇒ Object
Calls a specific tool by name with the given parameters
253 254 255 256 257 258 259 260 261 262 263 264 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 253 def call_tool(tool_name, parameters, server: nil) tools = list_tools if server # Use the specified server srv = select_server(server) # Find the tool on this specific server tool = tools.find { |t| t.name == tool_name && t.server == srv } unless tool raise MCPClient::Errors::ToolNotFound, "Tool '#{tool_name}' not found on server '#{srv.name || srv.class.name}'" end else # Find the tool across all servers matching_tools = tools.select { |t| t.name == tool_name } if matching_tools.empty? raise MCPClient::Errors::ToolNotFound, "Tool '#{tool_name}' not found" elsif matching_tools.size > 1 # If multiple matches, disambiguate with server names server_names = matching_tools.map { |t| t.server&.name || 'unnamed' } raise MCPClient::Errors::AmbiguousToolName, "Multiple tools named '#{tool_name}' found across servers (#{server_names.join(', ')}). " \ "Please specify a server using the 'server' parameter." end tool = matching_tools.first end # Validate parameters against tool schema validate_params!(tool, parameters) # Use the tool's associated server server = tool.server raise MCPClient::Errors::ServerNotFound, "No server found for tool '#{tool_name}'" unless server 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.message} (Server: #{server_id})" 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.
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 |
# File 'lib/mcp_client/client.rb', line 398 def call_tool_streaming(tool_name, parameters, server: nil) tools = list_tools if server # Use the specified server srv = select_server(server) # Find the tool on this specific server tool = tools.find { |t| t.name == tool_name && t.server == srv } unless tool raise MCPClient::Errors::ToolNotFound, "Tool '#{tool_name}' not found on server '#{srv.name || srv.class.name}'" end else # Find the tool across all servers matching_tools = tools.select { |t| t.name == tool_name } if matching_tools.empty? raise MCPClient::Errors::ToolNotFound, "Tool '#{tool_name}' not found" elsif matching_tools.size > 1 # If multiple matches, disambiguate with server names server_names = matching_tools.map { |t| t.server&.name || 'unnamed' } raise MCPClient::Errors::AmbiguousToolName, "Multiple tools named '#{tool_name}' found across servers (#{server_names.join(', ')}). " \ "Please specify a server using the 'server' parameter." end tool = matching_tools.first end # Validate parameters against tool schema validate_params!(tool, parameters) # 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.message} (Server: #{server_id})" raise MCPClient::Errors::ToolCallError, msg end end |
#call_tools(calls) ⇒ Array<Object>
Call multiple tools in batch
383 384 385 386 387 388 389 390 |
# File 'lib/mcp_client/client.rb', line 383 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 |
#cleanup ⇒ Object
Clean up all server connections
326 327 328 |
# File 'lib/mcp_client/client.rb', line 326 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
332 333 334 335 336 |
# File 'lib/mcp_client/client.rb', line 332 def clear_cache @tool_cache.clear @prompt_cache.clear @resource_cache.clear end |
#complete(ref:, argument:, server: nil) ⇒ Hash
Request completion suggestions from a server (MCP 2025-06-18)
493 494 495 496 |
# File 'lib/mcp_client/client.rb', line 493 def complete(ref:, argument:, server: nil) srv = select_server(server) srv.complete(ref: ref, argument: argument) end |
#find_server(name) ⇒ MCPClient::ServerBase?
Find a server by name
358 359 360 |
# File 'lib/mcp_client/client.rb', line 358 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
373 374 375 |
# File 'lib/mcp_client/client.rb', line 373 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)
365 366 367 368 |
# File 'lib/mcp_client/client.rb', line 365 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
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/mcp_client/client.rb', line 103 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.message} (Server: #{server_id})" end end |
#list_prompts(cache: true) ⇒ Array<MCPClient::Prompt>
Lists all available prompts from all connected MCP servers
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 |
# File 'lib/mcp_client/client.rb', line 72 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.message}") end prompts end |
#list_resources(cache: true, cursor: nil) ⇒ Hash
Lists all available resources from all connected MCP servers
152 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 |
# File 'lib/mcp_client/client.rb', line 152 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.message}") end # Return hash format consistent with server methods { 'resources' => resources, 'nextCursor' => nil } end |
#list_tools(cache: true) ⇒ Array<MCPClient::Tool>
Lists all available tools from all connected MCP servers
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 242 243 244 245 246 |
# File 'lib/mcp_client/client.rb', line 215 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.message}") 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’
504 505 506 |
# File 'lib/mcp_client/client.rb', line 504 def log_level=(level) @servers.map { |srv| srv.log_level = level } end |
#on_notification {|server, method, params| ... } ⇒ void
This method returns an undefined value.
Register a callback for JSON-RPC notifications from servers
341 342 343 |
# File 'lib/mcp_client/client.rb', line 341 def on_notification(&block) @notification_listeners << block end |
#ping(server_index: nil) ⇒ Object
Ping the MCP server to check connectivity (zero-parameter heartbeat call)
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 |
# File 'lib/mcp_client/client.rb', line 449 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
197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/mcp_client/client.rb', line 197 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)
481 482 483 484 |
# File 'lib/mcp_client/client.rb', line 481 def send_notification(method, params: {}, server: nil) srv = select_server(server) srv.rpc_notify(method, params) end |
#send_rpc(method, params: {}, server: nil) ⇒ Object
Send a raw JSON-RPC request to a server
471 472 473 474 |
# File 'lib/mcp_client/client.rb', line 471 def send_rpc(method, params: {}, server: nil) srv = select_server(server) srv.rpc_request(method, params) end |
#to_anthropic_tools(tool_names: nil) ⇒ Array<Hash>
Convert MCP tools to Anthropic Claude tool specifications
310 311 312 313 314 |
# File 'lib/mcp_client/client.rb', line 310 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
319 320 321 322 323 |
# File 'lib/mcp_client/client.rb', line 319 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
301 302 303 304 305 |
# File 'lib/mcp_client/client.rb', line 301 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 |