-
Notifications
You must be signed in to change notification settings - Fork 54
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
Open
jcat4
wants to merge
22
commits into
modelcontextprotocol:main
Choose a base branch
from
jcat4:add-basic-http-client-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
e08c659
refactor cdoe in prep for client support
jcat4 bcd59c3
remove gem
jcat4 db08725
remove DS_Store
jcat4 53a1156
add original spec files code back
jcat4 5eacee6
move and fix some tests
jcat4 9ad0d99
Add basic HTTP client support
jcat4 309aba5
add some client docs
jcat4 d0f6b4c
add more robust error handling
jcat4 3aa961c
Add basic HTTP client support
jcat4 3a0b9b8
fix gemspec
jcat4 efac287
patch up old reference
jcat4 f43c340
fix tests
jcat4 72e07aa
make faraday optional
jcat4 d571830
patch up lingering ModelContextProtocol references
jcat4 35a17c2
ew, stop calling private method in tests
jcat4 ce5ad24
I need a spellcheck extension
jcat4 5168003
rename private method
jcat4 50ad8c8
return all responses, not just text property of first one
jcat4 9ed51ff
attempt to break up readme between server and client
jcat4 0f9cb08
patch up sizing
jcat4 15bcc6a
add install instructions back
jcat4 289e462
add comment ackknowledging empty test
jcat4 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,6 @@ | |
/spec/reports/ | ||
/tmp/ | ||
Gemfile.lock | ||
|
||
# Mac stuff | ||
.DS_Store | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,7 +22,9 @@ Or install it yourself as: | |
$ gem install mcp | ||
``` | ||
|
||
## MCP Server | ||
You may need to add additional dependencies depending on which features you wish to access. | ||
|
||
## Building an MCP Server | ||
|
||
The `MCP::Server` class is the core component that handles JSON-RPC requests and responses. | ||
It implements the Model Context Protocol specification, handling model context requests and responses. | ||
|
@@ -216,7 +218,7 @@ $ ruby examples/stdio_server.rb | |
{"jsonrpc":"2.0","id":"2","method":"tools/list"} | ||
``` | ||
|
||
## Configuration | ||
### Configuration | ||
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. At the moment, this is server-specific. If we have this patch a client config too (or stop having the config scoped to just the server), we can move this somewhere else in the README |
||
|
||
The gem can be configured using the `MCP.configure` block: | ||
|
||
|
@@ -362,7 +364,7 @@ When an exception occurs: | |
|
||
If no exception reporter is configured, a default no-op reporter is used that silently ignores exceptions. | ||
|
||
## Tools | ||
### Tools | ||
|
||
MCP spec includes [Tools](https://modelcontextprotocol.io/docs/concepts/tools) which provide functionality to LLM apps. | ||
|
||
|
@@ -425,7 +427,7 @@ Tools can include annotations that provide additional metadata about their behav | |
|
||
Annotations can be set either through the class definition using the `annotations` class method or when defining a tool using the `define` method. | ||
|
||
## Prompts | ||
### Prompts | ||
|
||
MCP spec includes [Prompts](https://modelcontextprotocol.io/docs/concepts/prompts), which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs. | ||
|
||
|
@@ -548,7 +550,7 @@ The data contains the following keys: | |
`tool_name`, `prompt_name` and `resource_uri` are only populated if a matching handler is registered. | ||
This is to avoid potential issues with metric cardinality | ||
|
||
## Resources | ||
### Resources | ||
|
||
MCP spec includes [Resources](https://modelcontextprotocol.io/docs/concepts/resources) | ||
|
||
|
@@ -583,6 +585,74 @@ end | |
|
||
otherwise `resources/read` requests will be a no-op. | ||
|
||
## Building an MCP Client | ||
|
||
The `MCP::Client` module provides client implementations for interacting with MCP servers. Currently, it supports HTTP transport for making JSON-RPC requests to MCP servers. | ||
|
||
**Note:** The client HTTP transport requires the `faraday` gem. Add `gem 'faraday', '>= 2.0'` to your Gemfile if you plan to use the client HTTP transport functionality. | ||
|
||
### HTTP Transport Layer | ||
|
||
You'll need to add `faraday` as a dependency to use the HTTP transport layer. | ||
|
||
```ruby | ||
gem 'mcp' | ||
gem 'faraday', '>= 2.0' | ||
``` | ||
|
||
The `MCP::Client::Http` class provides a simple HTTP client for interacting with MCP servers: | ||
|
||
```ruby | ||
client = MCP::Client::Http.new(url: "https://api.example.com/mcp") | ||
|
||
# List available tools | ||
tools = client.tools | ||
tools.each do |tool| | ||
puts "Tool: #{tool.name}" | ||
puts "Description: #{tool.description}" | ||
puts "Input Schema: #{tool.input_schema}" | ||
end | ||
|
||
# Call a specific tool | ||
response = client.call_tool( | ||
tool: tools.first, | ||
input: { message: "Hello, world!" } | ||
) | ||
``` | ||
|
||
The HTTP client supports: | ||
- Tool listing via the `tools/list` method | ||
- Tool invocation via the `tools/call` method | ||
- Automatic JSON-RPC 2.0 message formatting | ||
- UUID v7 request ID generation | ||
- Setting headers for things like authorization | ||
|
||
#### HTTP Authorization | ||
|
||
By default, the HTTP client has no authentication, but it supports custom headers for authentication. For example, to use Bearer token authentication: | ||
|
||
```ruby | ||
client = MCP::Client::Http.new( | ||
url: "https://api.example.com/mcp", | ||
headers: { | ||
"Authorization" => "Bearer my_token" | ||
} | ||
) | ||
|
||
client.tools # will make the call using Bearer auth | ||
``` | ||
|
||
You can add any custom headers needed for your authentication scheme. The client will include these headers in all requests. | ||
|
||
### Tool Objects | ||
|
||
The client provides wrapper objects for tools returned by the server: | ||
|
||
- `MCP::Client::Tool` - Represents a single tool with its metadata | ||
- `MCP::Client::Tools` - Collection of tools with enumerable functionality | ||
|
||
These objects provide easy access to tool properties like name, description, and input schema. | ||
|
||
## Releases | ||
|
||
This gem is published to [RubyGems.org](https://rubygems.org/gems/mcp) | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
|
||
attr_reader :url, :version | ||
|
||
def initialize(url:, version: DEFAULT_VERSION, headers: {}) | ||
@url = url | ||
@version = version | ||
@headers = headers | ||
end | ||
|
||
def tools | ||
response = send_request(method: "tools/list").body | ||
|
||
::MCP::Client::Tools.new(response) | ||
end | ||
|
||
def call_tool(tool:, input:) | ||
response = send_request( | ||
method: "tools/call", | ||
params: { name: tool.name, arguments: input }, | ||
).body | ||
|
||
response.dig("result", "content") | ||
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 send_request(method:, params: nil) | ||
client.post( | ||
"", | ||
{ | ||
jsonrpc: "2.0", | ||
id: request_id, | ||
method:, | ||
params:, | ||
mcp: { jsonrpc: "2.0", id: request_id, method:, params: }.compact, | ||
}.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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } || [] | ||
end | ||
end | ||
end | ||
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
❤️