Skip to content

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
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@
/spec/reports/
/tmp/
Gemfile.lock

# Mac stuff
.DS_Store
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ gem "activesupport"
gem "debug"
gem "rake", "~> 13.0"
gem "sorbet-static-and-runtime"

group :test do
gem "webmock"
gem "faraday", ">= 2.0"
end
80 changes: 75 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -216,7 +218,7 @@ $ ruby examples/stdio_server.rb
{"jsonrpc":"2.0","id":"2","method":"tools/list"}
```

## Configuration
### Configuration
Copy link
Contributor Author

@jcat4 jcat4 Jul 31, 2025

Choose a reason for hiding this comment

The 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:

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions lib/mcp/client.rb
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
115 changes: 115 additions & 0 deletions lib/mcp/client/http.rb
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
26 changes: 26 additions & 0 deletions lib/mcp/client/tool.rb
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
30 changes: 30 additions & 0 deletions lib/mcp/client/tools.rb
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
2 changes: 2 additions & 0 deletions mcp.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ Gem::Specification.new do |spec|

spec.add_dependency("json_rpc_handler", "~> 0.1")
spec.add_dependency("json-schema", ">= 4.1")

# Faraday is required for the client HTTP transport layer
end
Loading