Merge pull request #28 from jcat4/add-basic-http-client-support

Add basic http client support
This commit is contained in:
Koichi ITO 2025-09-09 18:51:07 +09:00 • committed by GitHub
commit 97640ed626
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 609 additions and 8 deletions

View file

@ -22,6 +22,9 @@ require_relative "mcp/tool/response"
require_relative "mcp/tool/annotations"
require_relative "mcp/transport"
require_relative "mcp/version"
require_relative "mcp/client"
require_relative "mcp/client/http"
require_relative "mcp/client/tool"
module MCP
class << self

88
lib/mcp/client.rb Normal file
View file

@ -0,0 +1,88 @@
# frozen_string_literal: true
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
# Calls a tool via the transport layer.
#
# @param tool [MCP::Client::Tool] The tool to be called.
# @param arguments [Object, nil] The arguments to pass to the tool.
# @return [Object] The result of the tool call, as returned by the transport.
#
# @example
# tool = client.tools.first
# result = client.call_tool(tool: tool, arguments: { foo: "bar" })
#
# @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)
response = transport.send_request(request: {
jsonrpc: JsonRpcHandler::Version::V2_0,
id: request_id,
method: "tools/call",
params: { name: tool.name, arguments: arguments },
})
response.dig("result", "content")
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

88
lib/mcp/client/http.rb Normal file
View file

@ -0,0 +1,88 @@
# frozen_string_literal: true
module MCP
class Client
class HTTP
attr_reader :url
def initialize(url:, headers: {})
@url = url
@headers = headers
end
def send_request(request:)
method = request[:method] || request["method"]
params = request[:params] || request["params"]
client.post("", request).body
rescue Faraday::BadRequestError => e
raise RequestHandlerError.new(
"The #{method} request is invalid",
{ method:, params: },
error_type: :bad_request,
original_error: e,
)
rescue Faraday::UnauthorizedError => e
raise RequestHandlerError.new(
"You are unauthorized to make #{method} requests",
{ method:, params: },
error_type: :unauthorized,
original_error: e,
)
rescue Faraday::ForbiddenError => e
raise RequestHandlerError.new(
"You are forbidden to make #{method} requests",
{ method:, params: },
error_type: :forbidden,
original_error: e,
)
rescue Faraday::ResourceNotFound => e
raise RequestHandlerError.new(
"The #{method} request is not found",
{ method:, params: },
error_type: :not_found,
original_error: e,
)
rescue Faraday::UnprocessableEntityError => e
raise RequestHandlerError.new(
"The #{method} request is unprocessable",
{ method:, params: },
error_type: :unprocessable_entity,
original_error: e,
)
rescue Faraday::Error => e # Catch-all
raise RequestHandlerError.new(
"Internal error handling #{method} request",
{ method:, params: },
error_type: :internal_error,
original_error: e,
)
end
private
attr_reader :headers
def client
require_faraday!
@client ||= Faraday.new(url) do |faraday|
faraday.request(:json)
faraday.response(:json)
faraday.response(:raise_error)
headers.each do |key, value|
faraday.headers[key] = value
end
end
end
def require_faraday!
require "faraday"
rescue LoadError
raise LoadError, "The 'faraday' gem is required to use the MCP client HTTP transport. " \
"Add it to your Gemfile: gem 'faraday', '>= 2.0'" \
"See https://rubygems.org/gems/faraday for more details."
end
end
end
end

15
lib/mcp/client/tool.rb Normal file
View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
module MCP
class Client
class Tool
attr_reader :name, :description, :input_schema
def initialize(name:, description:, input_schema:)
@name = name
@description = description
@input_schema = input_schema
end
end
end
end