Merge pull request #28 from jcat4/add-basic-http-client-support
Add basic http client support
This commit is contained in:
commit
97640ed626
4 changed files with 609 additions and 8 deletions
|
|
@ -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
88
lib/mcp/client.rb
Normal 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
88
lib/mcp/client/http.rb
Normal 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
15
lib/mcp/client/tool.rb
Normal 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
|
||||
Loading…
Reference in a new issue