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 ⇒ Object
readonly
Returns the value of attribute logger.
-
#servers ⇒ Array<MCPClient::ServerBase>
readonly
List of servers.
-
#tool_cache ⇒ Hash<String, MCPClient::Tool>
readonly
Cache of tools by 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.
-
#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).
-
#initialize(mcp_server_configs: [], logger: nil) ⇒ Client
constructor
Initialize a new MCPClient::Client.
-
#list_tools(cache: true) ⇒ Array<MCPClient::Tool>
Lists all available tools from all connected MCP servers.
-
#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).
-
#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) ⇒ Client
Initialize a new MCPClient::Client
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/mcp_client/client.rb', line 20 def initialize(mcp_server_configs: [], logger: 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 = {} # JSON-RPC notification listeners @notification_listeners = [] # 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 end end |
Instance Attribute Details
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
15 |
# File 'lib/mcp_client/client.rb', line 15 attr_reader :servers, :tool_cache, :logger |
#servers ⇒ Array<MCPClient::ServerBase> (readonly)
Returns list of servers.
15 16 17 |
# File 'lib/mcp_client/client.rb', line 15 def servers @servers end |
#tool_cache ⇒ Hash<String, MCPClient::Tool> (readonly)
Returns cache of tools by name.
15 |
# File 'lib/mcp_client/client.rb', line 15 attr_reader :servers, :tool_cache, :logger |
Instance Method Details
#call_tool(tool_name, parameters, server: nil) ⇒ Object
Calls a specific tool by name with the given parameters
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 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/mcp_client/client.rb', line 84 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.} (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.
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 247 248 249 250 251 252 253 254 255 256 257 258 259 260 |
# File 'lib/mcp_client/client.rb', line 215 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.} (Server: #{server_id})" raise MCPClient::Errors::ToolCallError, msg end end |
#call_tools(calls) ⇒ Array<Object>
Call multiple tools in batch
200 201 202 203 204 205 206 207 |
# File 'lib/mcp_client/client.rb', line 200 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
157 158 159 |
# File 'lib/mcp_client/client.rb', line 157 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
163 164 165 |
# File 'lib/mcp_client/client.rb', line 163 def clear_cache @tool_cache.clear end |
#find_server(name) ⇒ MCPClient::ServerBase?
Find a server by name
177 178 179 |
# File 'lib/mcp_client/client.rb', line 177 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
192 193 194 |
# File 'lib/mcp_client/client.rb', line 192 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)
184 185 186 187 |
# File 'lib/mcp_client/client.rb', line 184 def find_tools(pattern) rx = pattern.is_a?(Regexp) ? pattern : /#{Regexp.escape(pattern)}/ list_tools.select { |t| t.name.match(rx) } end |
#list_tools(cache: true) ⇒ Array<MCPClient::Tool>
Lists all available tools from all connected MCP servers
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 |
# File 'lib/mcp_client/client.rb', line 47 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| @tool_cache[tool.name] = 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 |
#on_notification {|server, method, params| ... } ⇒ void
This method returns an undefined value.
Register a callback for JSON-RPC notifications from servers
170 171 172 |
# File 'lib/mcp_client/client.rb', line 170 def on_notification(&block) @notification_listeners << block end |
#ping(server_index: nil) ⇒ Object
Ping the MCP server to check connectivity (zero-parameter heartbeat call)
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/mcp_client/client.rb', line 266 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 |
#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)
298 299 300 301 |
# File 'lib/mcp_client/client.rb', line 298 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
288 289 290 291 |
# File 'lib/mcp_client/client.rb', line 288 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
141 142 143 144 145 |
# File 'lib/mcp_client/client.rb', line 141 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
150 151 152 153 154 |
# File 'lib/mcp_client/client.rb', line 150 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
132 133 134 135 136 |
# File 'lib/mcp_client/client.rb', line 132 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 |