ruby-sdk/lib/mcp/client.rb
Koichi ITO 0c729d0a2d Use autoload to defer loading of unused subsystems
## Motivation and Context

`lib/mcp.rb` currently declares all `require_relative` statements in one place.
As a result, Server and Client are both loaded, the stdio and Streamable HTTP
transports are both loaded, and Tool, Prompt, and Resource are all loaded,
regardless of which components are actually used.

These components are selected by the user depending on their use case,
so loading everything upfront is unnecessary.

Using `autoload` for selected constants defers loading until the constant
is actually referenced, so only the subsystems required by the user are loaded.

## How Has This Been Tested?

All existing tests pass unchanged.

## Breaking Change

None. This change only defers loading of unused components.
2026-03-12 14:08:27 +09:00

168 lines
5.3 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
require_relative "client/http"
require_relative "client/tool"
module MCP
class Client
# Initializes a new MCP::Client instance.
#
# @param transport [Object] The transport object to use for communication with the server.
# The transport should be a duck type that responds to `send_request`. See the README for more details.
#
# @example
# transport = MCP::Client::HTTP.new(url: "http://localhost:3000")
# client = MCP::Client.new(transport: transport)
def initialize(transport:)
@transport = transport
end
# The user may want to access additional transport-specific methods/attributes
# So keeping it public
attr_reader :transport
# Returns the list of tools available from the server.
# Each call will make a new request – the result is not cached.
#
# @return [Array<MCP::Client::Tool>] An array of available tools.
#
# @example
# tools = client.tools
# tools.each do |tool|
# puts tool.name
# end
def tools
response = transport.send_request(request: {
jsonrpc: JsonRpcHandler::Version::V2_0,
id: request_id,
method: "tools/list",
})
response.dig("result", "tools")&.map do |tool|
Tool.new(
name: tool["name"],
description: tool["description"],
input_schema: tool["inputSchema"],
)
end || []
end
# Returns the list of resources available from the server.
# Each call will make a new request – the result is not cached.
#
# @return [Array<Hash>] An array of available resources.
def resources
response = transport.send_request(request: {
jsonrpc: JsonRpcHandler::Version::V2_0,
id: request_id,
method: "resources/list",
})
response.dig("result", "resources") || []
end
# Returns the list of resource templates available from the server.
# Each call will make a new request – the result is not cached.
#
# @return [Array<Hash>] An array of available resource templates.
def resource_templates
response = transport.send_request(request: {
jsonrpc: JsonRpcHandler::Version::V2_0,
id: request_id,
method: "resources/templates/list",
})
response.dig("result", "resourceTemplates") || []
end
# Returns the list of prompts available from the server.
# Each call will make a new request – the result is not cached.
#
# @return [Array<Hash>] An array of available prompts.
def prompts
response = transport.send_request(request: {
jsonrpc: JsonRpcHandler::Version::V2_0,
id: request_id,
method: "prompts/list",
})
response.dig("result", "prompts") || []
end
# Calls a tool via the transport layer and returns the full response from the server.
#
# @param tool [MCP::Client::Tool] The tool to be called.
# @param arguments [Object, nil] The arguments to pass to the tool.
# @param progress_token [String, Integer, nil] A token to request progress notifications from the server during tool execution.
# @return [Hash] The full JSON-RPC response from the transport.
#
# @example
# tool = client.tools.first
# response = client.call_tool(tool: tool, arguments: { foo: "bar" })
# structured_content = response.dig("result", "structuredContent")
#
# @note
# 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.
def call_tool(tool:, arguments: nil, progress_token: nil)
params = { name: tool.name, arguments: arguments }
if progress_token
params[:_meta] = { progressToken: progress_token }
end
transport.send_request(request: {
jsonrpc: JsonRpcHandler::Version::V2_0,
id: request_id,
method: "tools/call",
params: params,
})
end
# Reads a resource from the server by URI and returns the contents.
#
# @param uri [String] The URI of the resource to read.
# @return [Array<Hash>] An array of resource contents (text or blob).
def read_resource(uri:)
response = transport.send_request(request: {
jsonrpc: JsonRpcHandler::Version::V2_0,
id: request_id,
method: "resources/read",
params: { uri: uri },
})
response.dig("result", "contents") || []
end
# Gets a prompt from the server by name and returns its details.
#
# @param name [String] The name of the prompt to get.
# @return [Hash] A hash containing the prompt details.
def get_prompt(name:)
response = transport.send_request(request: {
jsonrpc: JsonRpcHandler::Version::V2_0,
id: request_id,
method: "prompts/get",
params: { name: name },
})
response.fetch("result", {})
end
private
def request_id
SecureRandom.uuid
end
class RequestHandlerError < StandardError
attr_reader :error_type, :original_error, :request
def initialize(message, request, error_type: :internal_error, original_error: nil)
super(message)
@request = request
@error_type = error_type
@original_error = original_error
end
end
end
end