## Motivation and Context
`Client#call_tool` previously required an `MCP::Client::Tool` object to call a tool.
This made the API harder to use compared to other MCP SDK implementations,
which accept a tool name string directly:
- Python SDK: `session.call_tool(name, arguments)` accepts a name string.
https://github.com/modelcontextprotocol/python-sdk/blob/v1.26.0/src/mcp/client/session.py#L370
- TypeScript SDK: `client.callTool({ name, arguments })` accepts a name string.
ccb78f2/packages/client/src/client/client.ts (L834)
## How Has This Been Tested?
Updated existing tests and added new tests.
## Breaking Change
No. This change adds a `name:` parameter so callers can pass a tool name string
directly without looking up a `Tool` object first. The existing `tool:` parameter
continues to work for backward compatibility.
177 lines
5.7 KiB
Ruby
177 lines
5.7 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
require_relative "client/stdio"
|
||
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 name [String] The name of the tool to call.
|
||
# @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 Call by name
|
||
# response = client.call_tool(name: "my_tool", arguments: { foo: "bar" })
|
||
# content = response.dig("result", "content")
|
||
#
|
||
# @example Call with a tool object
|
||
# 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(name: nil, tool: nil, arguments: nil, progress_token: nil)
|
||
tool_name = name || tool&.name
|
||
raise ArgumentError, "Either `name:` or `tool:` must be provided." unless tool_name
|
||
|
||
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
|