Class: MCPClient::Client

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initialize(mcp_server_configs: [], logger: nil, elicitation_handler: nil, roots: nil, sampling_handler: nil) ⇒ Client

Initialize a new MCPClient::Client

Parameters:

  • mcp_server_configs (Array<Hash>) (defaults to: [])

    configurations for MCP servers

  • logger (Logger, nil) (defaults to: nil)

    optional logger, defaults to STDOUT

  • elicitation_handler (Proc, nil) (defaults to: nil)

    optional handler for elicitation requests (MCP 2025-06-18)

  • roots (Array<MCPClient::Root, Hash>, nil) (defaults to: nil)

    optional list of roots (MCP 2025-06-18)

  • sampling_handler (Proc, nil) (defaults to: nil)

    optional handler for sampling requests (MCP 2025-11-25)



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
66
67
68
69
70
71
72
73
74
# File 'lib/mcp_client/client.rb', line 29

def initialize(mcp_server_configs: [], logger: nil, elicitation_handler: nil, roots: nil, sampling_handler: nil)
  # 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 = {}
  @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
  # 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-11-25)
    server.on_sampling_request(&method(:handle_sampling_request)) if server.respond_to?(:on_sampling_request)
  end
end

Instance Attribute Details

#loggerLogger (readonly)

Returns logger for client operations.

Returns:

  • (Logger)

    logger for client operations



21
# File 'lib/mcp_client/client.rb', line 21

attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots

#prompt_cacheHash<String, MCPClient::Prompt> (readonly)

Returns cache of prompts by composite key (server_id:name).

Returns:

  • (Hash<String, MCPClient::Prompt>)

    cache of prompts by composite key (server_id:name)



21
# File 'lib/mcp_client/client.rb', line 21

attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots

#resource_cacheHash<String, MCPClient::Resource> (readonly)

Returns cache of resources by composite key (server_id:uri).

Returns:



21
# File 'lib/mcp_client/client.rb', line 21

attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots

#rootsObject

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

#serversArray<MCPClient::ServerBase> (readonly)

Returns list of servers.

Returns:



21
22
23
# File 'lib/mcp_client/client.rb', line 21

def servers
  @servers
end

#tool_cacheHash<String, MCPClient::Tool> (readonly)

Returns cache of tools by composite key (server_id:name).

Returns:

  • (Hash<String, MCPClient::Tool>)

    cache of tools by composite key (server_id:name)



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

Parameters:

  • tool_name (String)

    the name of the tool to call

  • parameters (Hash)

    the parameters to pass to the tool

  • server (String, Symbol, Integer, MCPClient::ServerBase, nil) (defaults to: nil)

    optional server to use

Returns:

  • (Object)

    the result of the tool invocation

Raises:



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
297
298
299
300
301
302
303
304
305
306
# File 'lib/mcp_client/client.rb', line 262

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)
  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
    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_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'.

Parameters:

  • tool_name (String)

    the name of the tool to call

  • parameters (Hash)

    the parameters to pass to the tool

  • ttl (Integer, nil) (defaults to: nil)

    optional requested task lifetime in milliseconds

  • server (String, Symbol, Integer, MCPClient::ServerBase, nil) (defaults to: nil)

    optional server to use

Returns:

Raises:



526
527
528
529
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
# File 'lib/mcp_client/client.rb', line 526

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_key = [:_meta, '_meta'].find { |k| parameters.key?(k) }
  arguments = meta_key ? parameters.reject { |k, _| k == meta_key } : parameters
  rpc_params = { name: tool_name, arguments: arguments, task: task_params }
  rpc_params[:_meta] = parameters[meta_key] if meta_key

  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.message}"
  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.

Parameters:

  • tool_name (String)

    the name of the tool to call

  • parameters (Hash)

    the parameters to pass to the tool

  • server (String, Symbol, Integer, MCPClient::ServerBase, nil) (defaults to: nil)

    optional server to use

Returns:

  • (Enumerator)

    streaming enumerator or single-value enumerator

Raises:



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
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/mcp_client/client.rb', line 408

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)
  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.message} (Server: #{server_id})"
    raise MCPClient::Errors::ToolCallError, msg
  end
end

#call_tools(calls) ⇒ Array<Object>

Call multiple tools in batch

Parameters:

  • calls (Array<Hash>)

    array of call hashes with keys:

    • name: tool name (required)
    • parameters: tool parameters (optional, default empty hash)
    • server: server name for routing (optional)

Returns:

  • (Array<Object>)

    array of results for each tool invocation



393
394
395
396
397
398
399
400
# File 'lib/mcp_client/client.rb', line 393

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)

Parameters:

  • task_id (String)

    the ID of the task to cancel

  • server (Integer, String, Symbol, MCPClient::ServerBase, nil) (defaults to: nil)

    server selector

Returns:

Raises:



626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
# File 'lib/mcp_client/client.rb', line 626

def cancel_task(task_id, server: nil)
  srv = select_server(server)

  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.message.match?(/terminal/i)
      raise MCPClient::Errors::TaskError, "Error cancelling task '#{task_id}': #{e.message}"
    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.message}"
  end
end

#cleanupObject

Clean up all server connections



336
337
338
# File 'lib/mcp_client/client.rb', line 336

def cleanup
  servers.each(&:cleanup)
end

#clear_cachevoid

This method returns an undefined value.

Clear the cached tools so that next list_tools will fetch fresh data



342
343
344
345
346
# File 'lib/mcp_client/client.rb', line 342

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)

Parameters:

  • ref (Hash)

    reference object (e.g., { 'type' => 'ref/prompt', 'name' => 'prompt_name' })

  • argument (Hash)

    the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })

  • context (Hash, nil) (defaults to: nil)

    optional context for the completion (MCP 2025-11-25), e.g., { 'arguments' => { 'arg1' => 'value1' } } for previously-resolved arguments

  • server (Integer, String, Symbol, MCPClient::ServerBase, nil) (defaults to: nil)

    server selector

Returns:

  • (Hash)

    completion result with 'values', optional 'total', and 'hasMore' fields

Raises:



506
507
508
509
# File 'lib/mcp_client/client.rb', line 506

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

Parameters:

  • name (String)

    the name of the server to find

Returns:



368
369
370
# File 'lib/mcp_client/client.rb', line 368

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

Parameters:

  • pattern (String, Regexp)

    pattern to match tool names

Returns:



383
384
385
# File 'lib/mcp_client/client.rb', line 383

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)

Parameters:

  • pattern (String, Regexp)

    pattern to match tool names

Returns:



375
376
377
378
# File 'lib/mcp_client/client.rb', line 375

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

Parameters:

  • prompt_name (String)

    the name of the prompt to get

  • parameters (Hash)

    the parameters to pass to the prompt

  • server (String, Symbol, Integer, MCPClient::ServerBase, nil) (defaults to: nil)

    optional server to use

Returns:

  • (Object)

    the final prompt

Raises:



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
145
146
147
148
149
150
151
152
153
# File 'lib/mcp_client/client.rb', line 112

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

#get_task(task_id, server: nil) ⇒ MCPClient::Task

Get the current state of a task (tasks/get, MCP 2025-11-25)

Parameters:

  • task_id (String)

    the ID of the task to query

  • server (Integer, String, Symbol, MCPClient::ServerBase, nil) (defaults to: nil)

    server selector

Returns:

Raises:



567
568
569
570
571
572
573
574
575
576
577
578
# File 'lib/mcp_client/client.rb', line 567

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.message}"
  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.

Parameters:

  • task_id (String)

    the ID of the task

  • server (Integer, String, Symbol, MCPClient::ServerBase, nil) (defaults to: nil)

    server selector

Returns:

  • (Object)

    the underlying task result

Raises:



589
590
591
592
593
594
595
596
597
598
599
# File 'lib/mcp_client/client.rb', line 589

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.message}"
  end
end

#list_prompts(cache: true) ⇒ Array<MCPClient::Prompt>

Lists all available prompts from all connected MCP servers

Parameters:

  • cache (Boolean) (defaults to: true)

    whether to use cached prompts or fetch fresh

Returns:

Raises:



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
# File 'lib/mcp_client/client.rb', line 81

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.message.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

Parameters:

  • cache (Boolean) (defaults to: true)

    whether to use cached resources or fetch fresh

  • cursor (String, nil) (defaults to: nil)

    optional cursor for pagination (only works with single server)

Returns:

  • (Hash)

    result containing 'resources' array and optional 'nextCursor'

Raises:



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
195
196
197
198
199
200
# File 'lib/mcp_client/client.rb', line 161

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.message.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_tasks(cursor: nil, server: nil) ⇒ Hash

List tasks known to a server (tasks/list, paginated, MCP 2025-11-25)

Parameters:

  • cursor (String, nil) (defaults to: nil)

    optional pagination cursor

  • server (Integer, String, Symbol, MCPClient::ServerBase, nil) (defaults to: nil)

    server selector

Returns:

Raises:



606
607
608
609
610
611
612
613
614
615
616
617
# File 'lib/mcp_client/client.rb', line 606

def list_tasks(cursor: nil, server: nil)
  srv = select_server(server)
  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.message}"
  end
end

#list_tools(cache: true) ⇒ Array<MCPClient::Tool>

Lists all available tools from all connected MCP servers

Parameters:

  • cache (Boolean) (defaults to: true)

    whether to use cached tools or fetch fresh

Returns:

Raises:



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
# File 'lib/mcp_client/client.rb', line 224

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.message.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'

Parameters:

  • level (String)

    the log level ('debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency')

Returns:

  • (Array<Hash>)

    results from servers

Raises:



651
652
653
# File 'lib/mcp_client/client.rb', line 651

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

Yields:

  • (server, method, params)


351
352
353
# File 'lib/mcp_client/client.rb', line 351

def on_notification(&block)
  @notification_listeners << block
end

#ping(server_index: nil) ⇒ Object

Ping the MCP server to check connectivity (zero-parameter heartbeat call)

Parameters:

  • server_index (Integer, nil) (defaults to: nil)

    optional index of a specific server to ping, nil for first available

Returns:

  • (Object)

    result from the ping request

Raises:



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

Parameters:

  • uri (String)

    the URI of the resource to read

  • server (String, Symbol, Integer, MCPClient::ServerBase, nil) (defaults to: nil)

    optional server to use

Returns:

  • (Object)

    the resource contents



206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/mcp_client/client.rb', line 206

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)

Parameters:

  • method (String)

    JSON-RPC method name

  • params (Hash) (defaults to: {})

    parameters for the notification

  • server (Integer, String, Symbol, MCPClient::ServerBase, nil) (defaults to: nil)

    server selector



492
493
494
495
# File 'lib/mcp_client/client.rb', line 492

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

Parameters:

  • method (String)

    JSON-RPC method name

  • params (Hash) (defaults to: {})

    parameters for the request

  • server (Integer, String, Symbol, MCPClient::ServerBase, nil) (defaults to: nil)

    server selector

Returns:

  • (Object)

    result from the JSON-RPC response



482
483
484
485
# File 'lib/mcp_client/client.rb', line 482

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

Parameters:

  • tool_names (Array<String>, nil) (defaults to: nil)

    optional list of tool names to include, nil means all tools

Returns:

  • (Array<Hash>)

    Anthropic Claude tool specifications



320
321
322
323
324
# File 'lib/mcp_client/client.rb', line 320

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

Parameters:

  • tool_names (Array<String>, nil) (defaults to: nil)

    optional list of tool names to include, nil means all tools

Returns:

  • (Array<Hash>)

    Google Vertex AI tool specifications with cleaned schemas



329
330
331
332
333
# File 'lib/mcp_client/client.rb', line 329

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

Parameters:

  • tool_names (Array<String>, nil) (defaults to: nil)

    optional list of tool names to include, nil means all tools

Returns:

  • (Array<Hash>)

    OpenAI function specifications



311
312
313
314
315
# File 'lib/mcp_client/client.rb', line 311

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