Class: MCP::Client
- Inherits:
-
Object
- Object
- MCP::Client
- Defined in:
- lib/mcp/client.rb,
lib/mcp/client/http.rb,
lib/mcp/client/tool.rb,
lib/mcp/client/oauth.rb,
lib/mcp/client/stdio.rb,
lib/mcp/client/oauth/flow.rb,
lib/mcp/client/oauth/pkce.rb,
lib/mcp/client/elicitation.rb,
lib/mcp/client/oauth/provider.rb,
lib/mcp/client/modern_envelope.rb,
lib/mcp/client/oauth/discovery.rb,
lib/mcp/client/paginated_result.rb,
lib/mcp/client/mcp_param_headers.rb,
lib/mcp/client/oauth/in_memory_storage.rb,
lib/mcp/client/oauth/jwt_client_assertion.rb,
lib/mcp/client/oauth/id_jag_token_exchange.rb,
lib/mcp/client/oauth/storage_backed_provider.rb,
lib/mcp/client/oauth/cross_app_access_provider.rb,
lib/mcp/client/oauth/client_credentials_provider.rb
Defined Under Namespace
Modules: Elicitation, McpParamHeaders, ModernEnvelope, OAuth Classes: DiscoverResult, HTTP, InputRequiredError, ListPromptsResult, ListResourceTemplatesResult, ListResourcesResult, ListToolsResult, PaginationLimitError, RequestHandlerError, ServerError, ServerRequestError, SessionExpiredError, Stdio, Tool, ValidationError
Constant Summary collapse
- MAX_PAGES =
Upper bound on the number of pages the all-pages methods (
tools,resources,resource_templates,prompts) will walk. The cursor guard infetch_all_pagesonly stops a server that repeats or cycles cursors; one that returns a freshnextCursoron every response would otherwise be followed indefinitely, growing the retained pages with it. 1_000- DEFAULT_INPUT_REQUIRED_MAX_ROUNDS =
Rounds the SEP-2322 driver runs before giving up, matching the TypeScript and Python SDK defaults. Every leg counts, including
requestState-only retries. 10- STATE_ONLY_BACKOFF_INITIAL_SECONDS =
Backoff for
requestState-only (load shedding) legs: exponential from 50ms to a 250ms cap, matching the Python SDK (the TypeScript SDK uses a fixed 250ms). 0.05- STATE_ONLY_BACKOFF_CAP_SECONDS =
0.25
Instance Attribute Summary collapse
-
#transport ⇒ Object
readonly
The user may want to access additional transport-specific methods/attributes So keeping it public.
Instance Method Summary collapse
-
#call_tool(name: nil, tool: nil, arguments: nil, progress_token: nil, meta: nil, cancellation: nil, input_responses: nil, request_state: nil) ⇒ Hash
Calls a tool via the transport layer and returns the full response from the server.
-
#complete(ref:, argument:, context: nil, meta: nil, cancellation: nil) ⇒ Hash
Requests completion suggestions from the server for a prompt argument or resource template URI.
-
#connect(client_info: nil, protocol_version: nil, capabilities: {}, mode: nil) ⇒ Hash?
Performs the MCP
initializehandshake by delegating to the transport (e.g.MCP::Client::HTTP,MCP::Client::Stdio). -
#connected? ⇒ Boolean
Returns true once
connecthas completed the handshake on the underlying transport. -
#discover(meta: nil, cancellation: nil) ⇒ MCP::Client::DiscoverResult
Sends
server/discover(MCP 2026-07-28, SEP-2575): sessionless capability discovery that works before (or instead of)connect. -
#get_prompt(name:, meta: nil, cancellation: nil, input_responses: nil, request_state: nil) ⇒ Hash
Gets a prompt from the server by name and returns its details.
-
#initialize(transport:, input_required_max_rounds: DEFAULT_INPUT_REQUIRED_MAX_ROUNDS, max_pages: MAX_PAGES) ⇒ Client
constructor
Initializes a new MCP::Client instance.
-
#instructions ⇒ Object
The server's instructions text, present in both eras when provided.
-
#list_prompts(cursor: nil, meta: nil, cancellation: nil) ⇒ MCP::Client::ListPromptsResult
Returns a single page of prompts from the server.
-
#list_resource_templates(cursor: nil, meta: nil, cancellation: nil) ⇒ MCP::Client::ListResourceTemplatesResult
Returns a single page of resource templates from the server.
-
#list_resources(cursor: nil, meta: nil, cancellation: nil) ⇒ MCP::Client::ListResourcesResult
Returns a single page of resources from the server.
-
#list_tools(cursor: nil, meta: nil, cancellation: nil) ⇒ MCP::Client::ListToolsResult
Returns a single page of tools from the server.
-
#on_elicitation(&handler) ⇒ Object
Registers a handler for
elicitation/createrequests the server sends while one of this client's requests is in flight. -
#on_roots(&handler) ⇒ Object
deprecated
Deprecated.
MCP Roots (
roots/list) is deprecated as of MCP protocol version 2026-07-28 (SEP-2577). Register this handler only to interoperate with servers that still ask for roots. -
#on_sampling(&handler) ⇒ Object
deprecated
Deprecated.
MCP Sampling (
sampling/createMessage) is deprecated as of MCP protocol version 2026-07-28 (SEP-2577). Register this handler only to interoperate with servers that still send sampling requests during the deprecation window; new servers should call LLM provider APIs directly. -
#ping(meta: nil, cancellation: nil) ⇒ Hash
Sends a
pingrequest to the server to verify the connection is alive. -
#prompts(cancellation: nil) ⇒ Array<Hash>
Returns every prompt available on the server.
-
#protocol_version ⇒ Object
The protocol version in use on this connection, independent of its era: the version negotiated by
initialize(legacy) or adopted viaserver/discover(modern). -
#read_resource(uri:, meta: nil, cancellation: nil, input_responses: nil, request_state: nil) ⇒ Array<Hash>
Reads a resource from the server by URI and returns the contents.
-
#resource_templates(cancellation: nil) ⇒ Array<Hash>
Returns every resource template available on the server.
-
#resources(cancellation: nil) ⇒ Array<Hash>
Returns every resource available on the server.
-
#server_capabilities ⇒ Object
The server's capabilities Hash, present in both eras.
-
#server_implementation ⇒ Object
The server's identity (
name/version), independent of where the era puts it: top-levelserverInfoon legacy results, the optional_metaio.modelcontextprotocol/serverInfostamp on modern results. -
#server_info ⇒ Object
The raw handshake result exactly as the server returned it, so its shape depends on the connection's era (SEP-2575): after the legacy handshake it is an
InitializeResult(protocolVersion, top-levelserverInfo), after modern adoption it is aDiscoverResult(supportedVersions,ttlMs/cacheScope,serverInfooptionally under_meta). -
#tools(cancellation: nil) ⇒ Array<MCP::Client::Tool>
Returns every tool available on the server.
Constructor Details
#initialize(transport:, input_required_max_rounds: DEFAULT_INPUT_REQUIRED_MAX_ROUNDS, max_pages: MAX_PAGES) ⇒ Client
Initializes a new MCP::Client instance.
Once a handler is registered through on_elicitation, on_sampling, or on_roots, call_tool,
get_prompt, and read_resource resume input_required results automatically; without handlers
(or when a requested kind has no handler) they raise InputRequiredError for manual driving,
exactly as before.
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/mcp/client.rb', line 133 def initialize(transport:, input_required_max_rounds: DEFAULT_INPUT_REQUIRED_MAX_ROUNDS, max_pages: MAX_PAGES) # `nil` or a non-positive value would make the pagination unbounded and silently # disable the protection, so reject it up front. unless max_pages.is_a?(Integer) && max_pages > 0 raise ArgumentError, "max_pages must be a positive Integer" end @transport = transport @max_pages = max_pages # Populated by `on_elicitation`, `on_sampling`, and `on_roots`. The same handler answers both ways # the server can ask for input: a real server-to-client request, and an embedded request inside # a SEP-2322 `input_required` result. @input_required_handlers = {} @input_required_max_rounds = input_required_max_rounds end |
Instance Attribute Details
#transport ⇒ Object (readonly)
The user may want to access additional transport-specific methods/attributes So keeping it public
151 152 153 |
# File 'lib/mcp/client.rb', line 151 def transport @transport end |
Instance Method Details
#call_tool(name: nil, tool: nil, arguments: nil, progress_token: nil, meta: nil, cancellation: nil, input_responses: nil, request_state: nil) ⇒ Hash
The exact requirements for arguments are determined by the transport layer in use.
Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details.
Calls a tool via the transport layer and returns the full response from the server.
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 |
# File 'lib/mcp/client.rb', line 482 def call_tool(name: nil, tool: nil, arguments: nil, progress_token: nil, meta: nil, cancellation: nil, input_responses: nil, request_state: nil) tool_name = name || tool&.name raise ArgumentError, "Either `name:` or `tool:` must be provided." unless tool_name params = { name: tool_name, arguments: arguments } = ? .dup : {} if progress_token .delete("progressToken") [:progressToken] = progress_token end params[:_meta] = unless .empty? params[:inputResponses] = input_responses if input_responses params[:requestState] = request_state if request_state drive_input_required(method: "tools/call", params: params, cancellation: cancellation) end |
#complete(ref:, argument:, context: nil, meta: nil, cancellation: nil) ⇒ Hash
Requests completion suggestions from the server for a prompt argument or resource template URI.
545 546 547 548 549 550 551 552 |
# File 'lib/mcp/client.rb', line 545 def complete(ref:, argument:, context: nil, meta: nil, cancellation: nil) params = { ref: ref, argument: argument } params[:context] = context if context response = request(method: "completion/complete", params: params, meta: , cancellation: cancellation) response.dig("result", "completion") || { "values" => [], "hasMore" => false } end |
#connect(client_info: nil, protocol_version: nil, capabilities: {}, mode: nil) ⇒ Hash?
Performs the MCP initialize handshake by delegating to the transport
(e.g. MCP::Client::HTTP, MCP::Client::Stdio). Returns the server's
InitializeResult.
When the transport does not respond to :connect, this is a no-op and
returns nil.
https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/mcp/client.rb', line 215 def connect(client_info: nil, protocol_version: nil, capabilities: {}, mode: nil) return unless transport.respond_to?(:connect) effective_mode = resolve_connect_mode(mode, protocol_version) if effective_mode == :legacy transport.connect( client_info: client_info, protocol_version: protocol_version, capabilities: capabilities, ) else transport.connect( client_info: client_info, protocol_version: protocol_version, capabilities: capabilities, mode: effective_mode, ) end end |
#connected? ⇒ Boolean
Returns true once connect has completed the handshake on the underlying
transport. Transports that do not expose connection state are assumed
connected and return true.
264 265 266 267 268 |
# File 'lib/mcp/client.rb', line 264 def connected? return transport.connected? if transport.respond_to?(:connected?) true end |
#discover(meta: nil, cancellation: nil) ⇒ MCP::Client::DiscoverResult
Sends server/discover (MCP 2026-07-28, SEP-2575): sessionless capability discovery
that works before (or instead of) connect.
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/mcp/client.rb', line 244 def discover(meta: nil, cancellation: nil) response = request(method: Methods::SERVER_DISCOVER, meta: , cancellation: cancellation) result = response.is_a?(Hash) ? response["result"] : nil raise ValidationError, "Response validation failed: missing or invalid `result`" unless result.is_a?(Hash) DiscoverResult.new( supported_versions: result["supportedVersions"], capabilities: result["capabilities"], # The finalized spec (PR #3002) stamps the server identity into the result `_meta`; # the top-level fallback tolerates servers built against the frozen SEP text. server_info: result.dig("_meta", RequestEnvelope::SERVER_INFO_META_KEY) || result["serverInfo"], instructions: result["instructions"], ttl_ms: result["ttlMs"], cache_scope: result["cacheScope"], ) end |
#get_prompt(name:, meta: nil, cancellation: nil, input_responses: nil, request_state: nil) ⇒ Hash
Gets a prompt from the server by name and returns its details.
524 525 526 527 528 529 530 531 532 533 |
# File 'lib/mcp/client.rb', line 524 def get_prompt(name:, meta: nil, cancellation: nil, input_responses: nil, request_state: nil) params = { name: name } params = params.merge(_meta: ) if && !.empty? params[:inputResponses] = input_responses if input_responses params[:requestState] = request_state if request_state response = drive_input_required(method: "prompts/get", params: params, cancellation: cancellation) response.fetch("result", {}) end |
#instructions ⇒ Object
The server's instructions text, present in both eras when provided.
179 180 181 |
# File 'lib/mcp/client.rb', line 179 def instructions server_info&.dig("instructions") end |
#list_prompts(cursor: nil, meta: nil, cancellation: nil) ⇒ MCP::Client::ListPromptsResult
Returns a single page of prompts from the server.
416 417 418 419 420 421 422 423 424 425 426 427 428 |
# File 'lib/mcp/client.rb', line 416 def list_prompts(cursor: nil, meta: nil, cancellation: nil) params = cursor ? { cursor: cursor } : nil response = request(method: "prompts/list", params: params, meta: , cancellation: cancellation) result = response["result"] || {} ListPromptsResult.new( prompts: result["prompts"] || [], next_cursor: result["nextCursor"], meta: result["_meta"], ttl_ms: result["ttlMs"], cache_scope: result["cacheScope"], ) end |
#list_resource_templates(cursor: nil, meta: nil, cancellation: nil) ⇒ MCP::Client::ListResourceTemplatesResult
Returns a single page of resource templates from the server.
380 381 382 383 384 385 386 387 388 389 390 391 392 |
# File 'lib/mcp/client.rb', line 380 def list_resource_templates(cursor: nil, meta: nil, cancellation: nil) params = cursor ? { cursor: cursor } : nil response = request(method: "resources/templates/list", params: params, meta: , cancellation: cancellation) result = response["result"] || {} ListResourceTemplatesResult.new( resource_templates: result["resourceTemplates"] || [], next_cursor: result["nextCursor"], meta: result["_meta"], ttl_ms: result["ttlMs"], cache_scope: result["cacheScope"], ) end |
#list_resources(cursor: nil, meta: nil, cancellation: nil) ⇒ MCP::Client::ListResourcesResult
Returns a single page of resources from the server.
344 345 346 347 348 349 350 351 352 353 354 355 356 |
# File 'lib/mcp/client.rb', line 344 def list_resources(cursor: nil, meta: nil, cancellation: nil) params = cursor ? { cursor: cursor } : nil response = request(method: "resources/list", params: params, meta: , cancellation: cancellation) result = response["result"] || {} ListResourcesResult.new( resources: result["resources"] || [], next_cursor: result["nextCursor"], meta: result["_meta"], ttl_ms: result["ttlMs"], cache_scope: result["cacheScope"], ) end |
#list_tools(cursor: nil, meta: nil, cancellation: nil) ⇒ MCP::Client::ListToolsResult
Returns a single page of tools from the server.
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 |
# File 'lib/mcp/client.rb', line 288 def list_tools(cursor: nil, meta: nil, cancellation: nil) params = cursor ? { cursor: cursor } : nil response = request(method: "tools/list", params: params, meta: , cancellation: cancellation) result = response["result"] || {} tools = (result["tools"] || []).filter_map do |tool| next if exclude_invalid_x_mcp_header?(tool) Tool.new( name: tool["name"], description: tool["description"], input_schema: tool["inputSchema"], output_schema: tool["outputSchema"], annotations: tool["annotations"], ) end ListToolsResult.new( tools: tools, next_cursor: result["nextCursor"], meta: result["_meta"], ttl_ms: result["ttlMs"], cache_scope: result["cacheScope"], ) end |
#on_elicitation(&handler) ⇒ Object
Registers a handler for elicitation/create requests the server sends while one of
this client's requests is in flight. The handler receives the request params
(message and requestedSchema, string keys) and must return an ElicitResult-shaped Hash:
{ action: "accept" | "decline" | "cancel", content: { ... } }.
The same handler answers both ways a server can ask: a real request mid-call, which needs a transport that
carries server-to-client requests (e.g. MCP::Client::HTTP); and an elicitation/create embedded in a SEP-2322
input_required result, which needs no server-to-client route, and is how the modern lifecycle asks now that it
forbids server-to-client requests. Both routes need capabilities: { elicitation: {} } passed to connect: per SEP-2322,
a server MUST NOT embed input requests of a kind the client has not declared.
https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation
573 574 575 |
# File 'lib/mcp/client.rb', line 573 def on_elicitation(&handler) register_input_handler(Methods::ELICITATION_CREATE, &handler) end |
#on_roots(&handler) ⇒ Object
MCP Roots (roots/list) is deprecated as of MCP protocol version 2026-07-28 (SEP-2577).
Register this handler only to interoperate with servers that still ask for roots.
Registers a handler for roots/list, answering both a server-to-client request on transports that
support one and an embedded roots/list inside a SEP-2322 input_required result. The handler
receives the request params (nil for roots/list) and must return a ListRootsResult-shaped Hash:
{ roots: [{ uri: "file:///project", name: "Project" }] }.
https://modelcontextprotocol.io/specification/2025-11-25/client/roots
626 627 628 |
# File 'lib/mcp/client.rb', line 626 def on_roots(&handler) register_input_handler(Methods::ROOTS_LIST, &handler) end |
#on_sampling(&handler) ⇒ Object
MCP Sampling (sampling/createMessage) is deprecated as of MCP protocol version 2026-07-28 (SEP-2577).
Register this handler only to interoperate with servers that still send sampling requests during the deprecation window;
new servers should call LLM provider APIs directly.
Registers a handler for sampling/createMessage requests the server sends while one of this client's requests is in flight.
The handler receives the request params (messages, maxTokens, optionally systemPrompt, modelPreferences, tools,
toolChoice, ...; string keys) and must return a CreateMessageResult-shaped Hash:
{ role: "assistant", content: { type: "text", text: "..." }, model: "...", stopReason: "..." }.
For trust and safety, the spec recommends a human in the loop able to review, edit, or reject the request and the generated response
before it is returned to the server. To reject, raise ServerRequestError with the spec's user-rejection code -1.
The same handler answers both ways a server can ask: a real request mid-call, which needs a transport that
carries server-to-client requests (e.g. MCP::Client::HTTP); and a sampling/createMessage embedded in a SEP-2322
input_required result, which needs no server-to-client route. Both routes need capabilities: { sampling: {} } passed to
connect (or { sampling: { tools: {} } } for tool-enabled requests): per SEP-2322, a server MUST NOT embed input requests of
a kind the client has not declared.
https://modelcontextprotocol.io/specification/2025-11-25/client/sampling
610 611 612 |
# File 'lib/mcp/client.rb', line 610 def on_sampling(&handler) register_input_handler(Methods::SAMPLING_CREATE_MESSAGE, &handler) end |
#ping(meta: nil, cancellation: nil) ⇒ Hash
Sends a ping request to the server to verify the connection is alive.
Per the MCP spec, the server responds with an empty result.
644 645 646 647 648 649 |
# File 'lib/mcp/client.rb', line 644 def ping(meta: nil, cancellation: nil) result = request(method: Methods::PING, meta: , cancellation: cancellation)["result"] raise ValidationError, "Response validation failed: missing or invalid `result`" unless result.is_a?(Hash) result end |
#prompts(cancellation: nil) ⇒ Array<Hash>
Returns every prompt available on the server. Iterates through all pages automatically
when the server paginates, so the full collection is returned regardless of the server's page_size setting.
Use #list_prompts when you need fine-grained cursor control.
Each call will make a new request - the result is not cached.
439 440 441 442 |
# File 'lib/mcp/client.rb', line 439 def prompts(cancellation: nil) # TODO: consider renaming to `list_all_prompts`. fetch_all_pages { |cursor| list_prompts(cursor: cursor, cancellation: cancellation) }.flat_map(&:prompts) end |
#protocol_version ⇒ Object
The protocol version in use on this connection, independent of its era:
the version negotiated by initialize (legacy) or adopted via server/discover (modern).
Returns nil before connect.
167 168 169 170 171 |
# File 'lib/mcp/client.rb', line 167 def protocol_version return transport.protocol_version if transport.respond_to?(:protocol_version) server_info&.dig("protocolVersion") end |
#read_resource(uri:, meta: nil, cancellation: nil, input_responses: nil, request_state: nil) ⇒ Array<Hash>
Reads a resource from the server by URI and returns the contents.
506 507 508 509 510 511 512 513 514 515 |
# File 'lib/mcp/client.rb', line 506 def read_resource(uri:, meta: nil, cancellation: nil, input_responses: nil, request_state: nil) params = { uri: uri } params = params.merge(_meta: ) if && !.empty? params[:inputResponses] = input_responses if input_responses params[:requestState] = request_state if request_state response = drive_input_required(method: "resources/read", params: params, cancellation: cancellation) response.dig("result", "contents") || [] end |
#resource_templates(cancellation: nil) ⇒ Array<Hash>
Returns every resource template available on the server. Iterates through all pages automatically
when the server paginates, so the full collection is returned regardless of the server's page_size setting.
Use #list_resource_templates when you need fine-grained cursor control.
Each call will make a new request - the result is not cached.
403 404 405 406 |
# File 'lib/mcp/client.rb', line 403 def resource_templates(cancellation: nil) # TODO: consider renaming to `list_all_resource_templates`. fetch_all_pages { |cursor| list_resource_templates(cursor: cursor, cancellation: cancellation) }.flat_map(&:resource_templates) end |
#resources(cancellation: nil) ⇒ Array<Hash>
Returns every resource available on the server. Iterates through all pages automatically
when the server paginates, so the full collection is returned regardless of the server's page_size setting.
Use #list_resources when you need fine-grained cursor control.
Each call will make a new request - the result is not cached.
367 368 369 370 |
# File 'lib/mcp/client.rb', line 367 def resources(cancellation: nil) # TODO: consider renaming to `list_all_resources`. fetch_all_pages { |cursor| list_resources(cursor: cursor, cancellation: cancellation) }.flat_map(&:resources) end |
#server_capabilities ⇒ Object
The server's capabilities Hash, present in both eras. Returns nil before connect.
174 175 176 |
# File 'lib/mcp/client.rb', line 174 def server_capabilities server_info&.dig("capabilities") end |
#server_implementation ⇒ Object
The server's identity (name/version), independent of where the era puts it: top-level serverInfo
on legacy results, the optional _meta io.modelcontextprotocol/serverInfo stamp on modern results.
Returns nil when a modern server does not identify itself.
186 187 188 |
# File 'lib/mcp/client.rb', line 186 def server_implementation server_info&.dig("_meta", RequestEnvelope::SERVER_INFO_META_KEY) || server_info&.dig("serverInfo") end |
#server_info ⇒ Object
The raw handshake result exactly as the server returned it, so its shape depends on the connection's era (SEP-2575):
after the legacy handshake it is an InitializeResult (protocolVersion, top-level serverInfo), after modern adoption
it is a DiscoverResult (supportedVersions, ttlMs/cacheScope, serverInfo optionally under _meta).
Code that must work against both eras should prefer the era-independent readers #protocol_version, #server_capabilities,
#instructions, and #server_implementation; this raw form remains the window to everything they do not cover
(supportedVersions, cache hints, _meta, extension data). Returns nil before connect, after close,
or when the transport does not expose a cached handshake result.
160 161 162 |
# File 'lib/mcp/client.rb', line 160 def server_info transport.server_info if transport.respond_to?(:server_info) end |
#tools(cancellation: nil) ⇒ Array<MCP::Client::Tool>
Returns every tool available on the server. Iterates through all pages automatically
when the server paginates, so the full collection is returned regardless of the server's page_size setting.
Use #list_tools when you need fine-grained cursor control.
Each call will make a new request - the result is not cached.
331 332 333 334 |
# File 'lib/mcp/client.rb', line 331 def tools(cancellation: nil) # TODO: consider renaming to `list_all_tools`. fetch_all_pages { |cursor| list_tools(cursor: cursor, cancellation: cancellation) }.flat_map(&:tools) end |