ruby-sdk/lib/mcp/client/http.rb
Koichi ITO f7952e8aaf Support customizing the Faraday client in MCP::Client::HTTP
## Motivation and Context

`MCP::Client::HTTP` builds its Faraday connection internally in a private `client` method
with no way to customize the middleware stack or adapter.
Users who need observability (request/response recording, failure logging) or
a different HTTP adapter must override a private method, coupling to an internal API.

This accepts an optional block in `MCP::Client::HTTP.new` that yields the Faraday builder
after default middleware is configured, allowing users to add custom middleware or swap
the HTTP adapter.

## How Has This Been Tested?

Added a test that verifies custom headers set via the block are included in requests.
All existing tests continue to pass.

## Breaking Changes

None. The block argument is optional, so existing code is unaffected.

Resolves #303
2026-04-09 05:08:49 +09:00

107 lines
3.3 KiB
Ruby

# frozen_string_literal: true
module MCP
class Client
class HTTP
ACCEPT_HEADER = "application/json, text/event-stream"
attr_reader :url
def initialize(url:, headers: {}, &block)
@url = url
@headers = headers
@faraday_customizer = block
end
def send_request(request:)
method = request[:method] || request["method"]
params = request[:params] || request["params"]
response = client.post("", request)
validate_response_content_type!(response, method, params)
response.body
rescue Faraday::BadRequestError => e
raise RequestHandlerError.new(
"The #{method} request is invalid",
{ method: method, params: params },
error_type: :bad_request,
original_error: e,
)
rescue Faraday::UnauthorizedError => e
raise RequestHandlerError.new(
"You are unauthorized to make #{method} requests",
{ method: method, params: params },
error_type: :unauthorized,
original_error: e,
)
rescue Faraday::ForbiddenError => e
raise RequestHandlerError.new(
"You are forbidden to make #{method} requests",
{ method: method, params: params },
error_type: :forbidden,
original_error: e,
)
rescue Faraday::ResourceNotFound => e
raise RequestHandlerError.new(
"The #{method} request is not found",
{ method: method, params: params },
error_type: :not_found,
original_error: e,
)
rescue Faraday::UnprocessableEntityError => e
raise RequestHandlerError.new(
"The #{method} request is unprocessable",
{ method: method, params: params },
error_type: :unprocessable_entity,
original_error: e,
)
rescue Faraday::Error => e # Catch-all
raise RequestHandlerError.new(
"Internal error handling #{method} request",
{ method: method, params: 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)
faraday.headers["Accept"] = ACCEPT_HEADER
headers.each do |key, value|
faraday.headers[key] = value
end
@faraday_customizer&.call(faraday)
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
def validate_response_content_type!(response, method, params)
content_type = response.headers["Content-Type"]
return if content_type&.include?("application/json")
raise RequestHandlerError.new(
"Unsupported Content-Type: #{content_type.inspect}. This client only supports JSON responses.",
{ method: method, params: params },
error_type: :unsupported_media_type,
)
end
end
end
end