-
Notifications
You must be signed in to change notification settings - Fork 56
Add basic http client support #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 16 commits
e08c659
bcd59c3
db08725
53a1156
5eacee6
9ad0d99
309aba5
d0f6b4c
3aa961c
3a0b9b8
efac287
f43c340
72e07aa
d571830
35a17c2
ce5ad24
5168003
50ad8c8
9ed51ff
0f9cb08
15bcc6a
289e462
d097729
687c2ae
dccb29e
281cb4c
e92e4ec
e05e502
f3f13e4
f83e561
f3bc852
644cf1c
3a87856
c2bf904
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,6 @@ | |
/spec/reports/ | ||
/tmp/ | ||
Gemfile.lock | ||
|
||
# Mac stuff | ||
.DS_Store | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# frozen_string_literal: true | ||
|
||
# require "json_rpc_handler" | ||
# require_relative "shared/instrumentation" | ||
# require_relative "shared/methods" | ||
|
||
module MCP | ||
module Client | ||
# Can be made an abstract class if we need shared behavior | ||
|
||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
# frozen_string_literal: true | ||
|
||
module MCP | ||
module Client | ||
class Http | ||
DEFAULT_VERSION = "0.1.0" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know what purpose this serves. I think I copied some other code in the app. If there are no strong feelings, I'm going to drop this and the version attr There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dropped, pls let me know if this should be added back and why |
||
|
||
attr_reader :url, :version | ||
|
||
def initialize(url:, version: DEFAULT_VERSION, headers: {}) | ||
@url = url | ||
@version = version | ||
@headers = headers | ||
end | ||
|
||
def tools | ||
jcat4 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
response = make_request(method: "tools/list").body | ||
|
||
::MCP::Client::Tools.new(response) | ||
end | ||
|
||
def call_tool(tool:, input:) | ||
response = make_request( | ||
method: "tools/call", | ||
params: { name: tool.name, arguments: input }, | ||
).body | ||
|
||
response.dig("result", "content", 0, "text") | ||
jcat4 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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'" | ||
end | ||
|
||
def make_request(method:, params: nil) | ||
jcat4 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
client.post( | ||
"", | ||
{ | ||
jsonrpc: "2.0", | ||
id: request_id, | ||
method:, | ||
params:, | ||
mcp: { jsonrpc: "2.0", id: request_id, method:, params: }.compact, | ||
jcat4 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}.compact, | ||
) | ||
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 | ||
|
||
def request_id | ||
SecureRandom.uuid_v7 | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# typed: false | ||
# frozen_string_literal: true | ||
|
||
module MCP | ||
module Client | ||
class Tool | ||
attr_reader :payload | ||
|
||
def initialize(payload) | ||
@payload = payload | ||
end | ||
|
||
def name | ||
payload["name"] | ||
end | ||
|
||
def description | ||
payload["description"] | ||
end | ||
|
||
def input_schema | ||
payload["inputSchema"] | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# typed: false | ||
# frozen_string_literal: true | ||
|
||
module MCP | ||
module Client | ||
class Tools | ||
include Enumerable | ||
|
||
attr_reader :response | ||
|
||
def initialize(response) | ||
@response = response | ||
end | ||
|
||
def each(&block) | ||
tools.each(&block) | ||
end | ||
|
||
def all | ||
tools | ||
end | ||
|
||
private | ||
|
||
def tools | ||
@tools ||= @response.dig("result", "tools")&.map { |tool| Tool.new(tool) } || [] | ||
jcat4 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️