Class: MCPClient::Client
- Inherits:
-
Object
- Object
- MCPClient::Client
- Includes:
- CacheSlices, ListAggregation, NotificationRouting, SamplingValidation, TaskApi, TaskSupport
- Defined in:
- lib/mcp_client/client.rb,
lib/mcp_client/client/task_api.rb,
lib/mcp_client/client/task_shape.rb,
lib/mcp_client/client/cache_slices.rb,
lib/mcp_client/client/task_support.rb,
lib/mcp_client/client/task_updates.rb,
lib/mcp_client/client/task_workers.rb,
lib/mcp_client/client/task_registry.rb,
lib/mcp_client/client/task_lifetimes.rb,
lib/mcp_client/client/list_aggregation.rb,
lib/mcp_client/client/sampling_validation.rb,
lib/mcp_client/client/notification_routing.rb,
lib/mcp_client/client/task_wait_boundaries.rb
Overview
MCP Client for integrating with the Model Context Protocol This is the main entry point for using MCP tools
Defined Under Namespace
Modules: CacheSlices, ListAggregation, NotificationRouting, SamplingValidation, TaskApi, TaskLifetimes, TaskRegistry, TaskShape, TaskSupport, TaskUpdates, TaskWaitBoundaries, TaskWorkers
Constant Summary collapse
- MAX_VIOLATION_TEXT =
Ceiling on the schema-violation text that reaches a log line or an exception (the validator already bounds its error count).
4000- 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
- SENSITIVE_CONFIG_KEYS =
Server-config keys whose values carry credentials (HTTP headers, the subprocess environment, inline tokens). Their values are replaced before a config is written to the log.
%i[headers env token access_token api_key auth authorization password secret client_secret oauth_provider].freeze
- REDACTED =
Placeholder written in place of a redacted value.
'[REDACTED]'- CACHE_INVALIDATION_MARK =
Where NotificationRouting#register_notification_handlers leaves word, on the thread that is routing, that the caches for the notification in hand have already been dropped by the transport's invalidation hook.
:mcp_client_cache_invalidation- MAX_PEER_LOG_MESSAGE_LENGTH =
Maximum characters of a peer-supplied log message written to the host log. The remote server controls this content, so an unbounded message would let it inflate log storage at will.
4096- CACHED_LIST_KINDS =
The list kinds this client caches, each with the transport-level cache behind it.
%i[tools prompts resources].freeze
Constants included from TaskSupport
TaskSupport::DEFAULT_TASK_POLL_INTERVAL, TaskSupport::MAX_TASK_INPUT_ROUNDS, TaskSupport::MAX_TASK_POLL_INTERVAL, TaskSupport::MAX_TASK_REQUEST_TIMEOUT, TaskSupport::MIN_TASK_POLL_INTERVAL, TaskSupport::MIN_TASK_REQUEST_TIMEOUT
Constants included from TaskLifetimes
TaskLifetimes::MAX_TRACKED_TASK_LIFETIMES, TaskLifetimes::TRACKED_TASK_LIFETIMES_LOW_WATER
Constants included from TaskWorkers
TaskWorkers::MAX_PENDING_TASK_REQUESTS
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 ⇒ Array<MCPClient::Root>
deprecated
Deprecated.
Roots is deprecated since MCP 2026-07-28 (SEP-2577); earliest removal is the first revision released on or after 2027-07-28. Reading the list is not itself a first use of the feature — the notice follows configuring a root or serving one — but the list is a deprecated feature's state, and a host reading it is holding one. Pass directories or files through tool parameters, resource URIs or server configuration instead.
-
#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_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 lists so that the next list_tools, list_prompts or list_resources fetches 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.
-
#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, request_meta: nil, extensions: 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.
-
#listen(notifications:, server: nil, ack_timeout: nil) {|method, params| ... } ⇒ MCPClient::Subscription
Open a long-lived notification stream on a server (MCP 2026-07-28 subscriptions/listen).
-
#log_level=(level) ⇒ Array<Hash>
deprecated
Deprecated.
Logging is deprecated since MCP 2026-07-28 (SEP-2577); earliest removal is the first revision released on or after 2027-07-28. Have the server log to stderr (stdio) or use OpenTelemetry instead.
-
#on_input_required_wait(&block) ⇒ void
Register the host's control over a multi round-trip request's out-of-band wait on every server (MCP 2026-07-28 client/elicitation "URL Mode": manual retry/cancel controls).
-
#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.
-
#resume_input_required(error, timeout: nil) ⇒ Object
Resume a multi round-trip request from the continuation an Errors::InputRequiredError carries, on the transport that raised it.
-
#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.
Methods included from TaskApi
#call_tool_as_task, #cancel_task, #get_task, #get_task_result, #list_tasks
Methods included from TaskSupport
#tasks_extension?, #update_task, #wait_for_task
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, request_meta: nil, extensions: nil) ⇒ Client
Initialize a new MCPClient::Client
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 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 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/mcp_client/client.rb', line 116 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, request_meta: nil, extensions: nil) 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 @extensions = normalize_extensions(extensions) # 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: #{redact_config(config).inspect}") MCPClient::ServerFactory.create(config, logger: @logger) end @tool_cache = {} # Bumped whenever the tool cache is emptied, so a list_tools that was # already in flight can tell that its definitions were superseded while # it ran (MCP 2026-07-28: a HeaderMismatch refresh announces itself as a # tools/list_changed). @tool_cache_generation = 0 @cache_mutex = Mutex.new # The effective-parameter fingerprint each server's slice of a list # cache was filled under (MCP 2026-07-28 caching: a result is served # only to a request that would carry the same parameters). @cache_params = Hash.new { |h, k| h[k] = {}.compare_by_identity } # Which servers have filled their slice of a list cache, so a snapshot # is known to be complete however few items it holds: a server that # legitimately lists nothing must be served from the cache too, not # asked again on every call. @cache_filled = {} # One lock for the list caches and their parameter tags: a freshness # check and the copy it approves are one snapshot, and the notification # thread's clears wait for it. @cache_mutex = Mutex.new # Bumped by every write under @cache_mutex, so a freshness verdict # reached outside the lock can be revalidated before a copy is served. @cache_version = 0 # 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; deprecated in 2026-07-28, SEP-2577) @sampling_handler = sampling_handler MCPClient::Deprecations.warn(:sampling, @logger) if sampling_handler # Whether the sampling handler supports tool use (SEP-1577) @sampling_supports_tools = sampling_supports_tools # Roots (MCP 2025-06-18; deprecated in 2026-07-28, SEP-2577) @roots = normalize_roots(roots) MCPClient::Deprecations.warn(:roots, @logger) unless @roots.empty? # Register default and user-defined notification handlers on each server @servers.each do |server| configure_server_identity(server, client_info, ) register_notification_handlers(server) # 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. # The transports call the callback with (request_id, params) only, so # the asking server is closed over here: the URL-mode host contract # depends on its protocol era (MCP 2026-07-28 removed elicitationId). if @elicitation_handler && server.respond_to?(:on_elicitation_request) server.on_elicitation_request do |request_id, request_params| handle_elicitation_request(request_id, request_params, server) end 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.
55 56 57 |
# File 'lib/mcp_client/client.rb', line 55 def logger @logger end |
#prompt_cache ⇒ Hash<String, MCPClient::Prompt> (readonly)
Returns cache of prompts by composite key (server_id:name).
49 50 51 |
# File 'lib/mcp_client/client.rb', line 49 def prompt_cache @prompt_cache end |
#resource_cache ⇒ Hash<String, MCPClient::Resource> (readonly)
Returns cache of resources by composite key (server_id:uri).
52 53 54 |
# File 'lib/mcp_client/client.rb', line 52 def resource_cache @resource_cache end |
#roots ⇒ Array<MCPClient::Root>
Roots is deprecated since MCP 2026-07-28 (SEP-2577); earliest removal is the first revision released on or after 2027-07-28. Reading the list is not itself a first use of the feature — the notice follows configuring a root or serving one — but the list is a deprecated feature's state, and a host reading it is holding one. Pass directories or files through tool parameters, resource URIs or server configuration instead.
Returns list of MCP roots (MCP 2025-06-18).
64 65 66 |
# File 'lib/mcp_client/client.rb', line 64 def roots @roots end |
#servers ⇒ Array<MCPClient::ServerBase> (readonly)
Returns list of servers.
43 44 45 |
# File 'lib/mcp_client/client.rb', line 43 def servers @servers end |
#tool_cache ⇒ Hash<String, MCPClient::Tool> (readonly)
Returns cache of tools by composite key (server_id:name).
46 47 48 |
# File 'lib/mcp_client/client.rb', line 46 def tool_cache @tool_cache end |
Instance Method Details
#call_tool(tool_name, parameters, server: nil, progress: nil) ⇒ Object
Calls a specific tool by name with the given parameters
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 |
# File 'lib/mcp_client/client.rb', line 343 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) # The session the call is made in: a task it comes back as belongs to # that session, not to one that replaced it while the answer was read. # The call goes into that very session and no other — a transport that # reconnects inside the request would otherwise run the tool in the # replacement session while the task is stamped with the sampled one, # and the wait would then refuse a task whose (possibly non-idempotent) # tool has already run, inviting a duplicate retry. task_epoch = tasks_extension? ? invocation_session_epoch(server) : nil # The call and the re-resolve that follows it share one slot for the # definition the transport's request goes out under, so a call that a # notification listener nests inside this one cannot leave its own # there. with_called_tool_definition(server) do result = begin pinned_to_session(server, task_epoch) { 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 # MCP 2026-07-28 HeaderMismatch recovery re-derives a call's # Mcp-Param-* headers from a refreshed tools/list, so the attempt that # was answered may have gone out under a definition this client never # resolved. Validate against that one -- never against the transport's # current list, which a tools/list_changed racing the call may already # have replaced with a definition the server never used. It is read # here, before a task's result is waited for: a refresh that lands # during a wait that may take minutes belongs to another invocation and # says nothing about the definition this one was answered under. called = called_tool_definition(server, tool_name) # MCP 2026-07-28 tasks extension: the server may have turned the call # into a task; drive it to its final result so the contract of this # method does not change. result = complete_task_result(tool_name, server, result, task_epoch) validate_called_result!(called || tool, result) 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.
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 |
# File 'lib/mcp_client/client.rb', line 562 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 task_epoch = tasks_extension? ? invocation_session_epoch(server) : nil # Use the streaming API if it's available, opened in the session the # epoch was sampled for: a chunk that comes back as a task is stamped # with that session, so the call must not have been written into the # one that replaced it (see #call_tool). The stream every built-in # transport hands back is lazy, so the call itself goes out under the # pin the enumeration takes (see #streamed_call_chunks) — this one # covers a transport that sends while building it. stream = pinned_to_session(server, task_epoch) { server.call_tool_streaming(tool_name, parameters) } # Every stream goes through the wrapper, whether or not tasks are in # play: "Clients SHOULD validate structured results against this # schema" is about a result, not about the method that fetched it, and # so is the dialect a result's schema declares. streamed_call_chunks(stream, tool, tool_name, server, epoch: task_epoch) 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
547 548 549 550 551 552 553 554 |
# File 'lib/mcp_client/client.rb', line 547 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
433 434 435 436 437 438 |
# File 'lib/mcp_client/client.rb', line 433 def cleanup servers.each(&:cleanup) # The transports forgot their results; the slices built from them go too. clear_cache clear_task_states end |
#clear_cache ⇒ void
This method returns an undefined value.
Clear the cached lists so that the next list_tools, list_prompts or list_resources fetches fresh data.
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 |
# File 'lib/mcp_client/client.rb', line 447 def clear_cache clear_tool_cache @cache_mutex.synchronize do @cache_version += 1 @prompt_cache.clear @resource_cache.clear # A slice's tag goes with the slice: a leftover tag must not vouch # for a server whose slice a later, partial refill never rebuilt. @cache_params.clear @cache_filled.clear end # The promise is fresh data, and a transport holding a list the server # bounded with a positive `ttlMs` (MCP 2026-07-28 # server/utilities/caching) would answer the next listing from it # without sending anything at all. Dropped outside this client's lock: # each transport takes its own. servers.each do |server| CACHED_LIST_KINDS.each { |kind| refresh_server_cache(server, kind) } forget_schema_checks end end |
#complete(ref:, argument:, context: nil, server: nil) ⇒ Hash
Request completion suggestions from a server (MCP 2025-06-18)
650 651 652 653 |
# File 'lib/mcp_client/client.rb', line 650 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
522 523 524 |
# File 'lib/mcp_client/client.rb', line 522 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
537 538 539 |
# File 'lib/mcp_client/client.rb', line 537 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)
529 530 531 532 |
# File 'lib/mcp_client/client.rb', line 529 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
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 261 262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/mcp_client/client.rb', line 232 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 |
#list_prompts(cache: true) ⇒ Array<MCPClient::Prompt>
Lists all available prompts from all connected MCP servers
216 217 218 219 220 221 222 223 224 225 |
# File 'lib/mcp_client/client.rb', line 216 def list_prompts(cache: true) ('prompts/list') do if cache && (snapshot = cached_snapshot(:prompts, @prompt_cache)) return snapshot end collect_prompts_from_servers(cache) end end |
#list_resources(cache: true, cursor: nil) ⇒ Hash
Lists all available resources from all connected MCP servers
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 |
# File 'lib/mcp_client/client.rb', line 281 def list_resources(cache: true, cursor: nil) ('resources/list') do # 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 return servers.first.list_resources(cursor: cursor) if servers.any? return { 'resources' => [], 'nextCursor' => nil } end # Use cache if available and no cursor if cache && (snapshot = cached_snapshot(:resources, @resource_cache)) return { 'resources' => snapshot, 'nextCursor' => nil } end collect_resources_from_servers(cache) end end |
#list_tools(cache: true) ⇒ Array<MCPClient::Tool>
Lists all available tools from all connected MCP servers
324 325 326 327 328 329 330 331 332 333 |
# File 'lib/mcp_client/client.rb', line 324 def list_tools(cache: true) ('tools/list') do if cache && (snapshot = cached_snapshot(:tools, @tool_cache)) return snapshot end collect_tools_from_servers(cache) end end |
#listen(notifications:, server: nil, ack_timeout: nil) {|method, params| ... } ⇒ MCPClient::Subscription
Open a long-lived notification stream on a server (MCP 2026-07-28 subscriptions/listen). The subscription's notifications also flow through the client's regular notification handling (cache invalidation, on_notification listeners).
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 |
# File 'lib/mcp_client/client.rb', line 669 def listen(notifications:, server: nil, ack_timeout: nil, &listener) srv = select_server(server) filter = MCPClient::Subscription.normalize_filter(notifications) if filter.key?('taskIds') unless tasks_extension? raise MCPClient::Errors::CapabilityError, 'Task notifications (taskIds) require the tasks extension: pass ' \ "extensions: ['#{MCPClient::JsonRpcCommon::TASKS_EXTENSION}'] to MCPClient::Client.new" end # The server must have negotiated the extension too (it answers a # taskIds filter from a non-declaring client with -32021). ensure_task_capability!(srv, 'listen') end srv.listen(notifications: notifications, ack_timeout: ack_timeout, &listener) end |
#log_level=(level) ⇒ Array<Hash>
Logging is deprecated since MCP 2026-07-28 (SEP-2577); earliest removal is the first revision released on or after 2027-07-28. Have the server log to stderr (stdio) or use OpenTelemetry instead.
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'
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 |
# File 'lib/mcp_client/client.rb', line 696 def log_level=(level) MCPClient::Deprecations.warn(:logging, @logger) @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. A 2026-07-28 # server needs no capability at all: the level is a per-request # field of every request's _meta, not a logging/setLevel call. unless !capabilities_known?(srv) || srv.capability?('logging') || modern_server?(srv) @logger.debug("Skipping logging/setLevel for #{srv.name || srv.class.name}: " \ 'logging capability not negotiated') next end srv.log_level = level end end |
#on_input_required_wait(&block) ⇒ void
This method returns an undefined value.
Register the host's control over a multi round-trip request's out-of-band wait on every server (MCP 2026-07-28 client/elicitation "URL Mode": manual retry/cancel controls). See JsonRpcCommon#on_input_required_wait for the contract.
482 483 484 485 486 487 |
# File 'lib/mcp_client/client.rb', line 482 def on_input_required_wait(&block) @input_required_wait_handler = block @servers.each do |server| server.on_input_required_wait(&block) if server.respond_to?(:on_input_required_wait) end end |
#on_notification {|server, method, params| ... } ⇒ void
This method returns an undefined value.
Register a callback for JSON-RPC notifications from servers
472 473 474 |
# File 'lib/mcp_client/client.rb', line 472 def on_notification(&block) @notification_listeners << block end |
#ping(server_index: nil) ⇒ Object
Ping the MCP server to check connectivity (zero-parameter heartbeat call)
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 |
# File 'lib/mcp_client/client.rb', line 600 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
306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/mcp_client/client.rb', line 306 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 |
#resume_input_required(error, timeout: nil) ⇒ Object
Resume a multi round-trip request from the continuation an Errors::InputRequiredError carries, on the transport that raised it. The result is the transport's, as #call_tool would have returned it before validation.
497 498 499 500 501 502 |
# File 'lib/mcp_client/client.rb', line 497 def resume_input_required(error, timeout: nil) transport = error.respond_to?(:transport) ? error.transport : nil raise ArgumentError, 'the error names no transport to resume on' unless transport transport.resume_input_required(error, timeout: timeout) 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)
636 637 638 639 |
# File 'lib/mcp_client/client.rb', line 636 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
622 623 624 625 626 627 628 629 |
# File 'lib/mcp_client/client.rb', line 622 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
417 418 419 420 421 |
# File 'lib/mcp_client/client.rb', line 417 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
426 427 428 429 430 |
# File 'lib/mcp_client/client.rb', line 426 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
408 409 410 411 412 |
# File 'lib/mcp_client/client.rb', line 408 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 |