Skip to content
Merged
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 Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ gem "activesupport"
gem "debug" if RUBY_VERSION >= "3.1"
# Avoid i18n 1.15.0, which breaks on Ruby 3.1 (ruby-i18n/i18n#735).
gem "i18n", "!= 1.15.0"
# FIXME: Drop this once a json release includes https://github.com/ruby/json/pull/1072. json 3.0.0 and 3.0.1
# forward arguments after a leading parameter, syntax Ruby 2.7.3 was the first to parse, while allowing Ruby 2.7.0.
gem "json", "< 3" if RUBY_VERSION < "2.7.3"
gem "rake", "~> 13.0"
gem "sorbet-static-and-runtime" if RUBY_VERSION >= "3.0"
gem "yard", "~> 0.9"
Expand Down
7 changes: 4 additions & 3 deletions lib/mcp/client/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,8 @@ def client
require_faraday!
@client ||= Faraday.new(url) do |faraday|
faraday.request(:json)
faraday.response(:json)
# The client parses response bodies itself (`resolve_response_body`), so streamed and buffered responses
# take the same path; Faraday's json response middleware is deliberately left out.
faraday.response(:raise_error)

faraday.headers["Accept"] = ACCEPT_HEADER
Expand Down Expand Up @@ -1074,8 +1075,8 @@ def resolve_response_body(stream, response, method, params)
elsif content_type&.include?("application/json")
return parse_json_buffer(stream.buffer, method, params) unless stream.buffer.empty?

# Adapters without `on_data` support deliver the body via `response.body`,
# already parsed by the json response middleware.
# Adapters without `on_data` support deliver the body via `response.body`; a JSON middleware
# added by the Faraday customizer may have parsed it already.
response.body.is_a?(String) ? parse_json_buffer(response.body, method, params) : response.body
else
raise RequestHandlerError.new(
Expand Down
20 changes: 20 additions & 0 deletions test/mcp/client/http_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,26 @@ def test_send_request_parses_json_response_when_adapter_does_not_stream
assert_equal({ "result" => { "tools" => [] } }, response)
end

def test_send_request_parses_a_json_body_with_a_parser_taking_keyword_options_only
# json 3.0 accepts parser options as keywords only, and Faraday's json response middleware
# passes them as a positional Hash, which Ruby 3 no longer converts. The stand-in parser has
# the json 3.0 signature, so the body must reach `JSON.parse` through the client's own call.
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
stub.post("/") do
[200, { "Content-Type" => "application/json" }, { result: { tools: [] } }.to_json]
end
end
client = HTTP.new(url: url) { |faraday| faraday.adapter(:test, stubs) }
parse = JSON.method(:parse)
keyword_options_only_parse = ->(source, **options) { parse.call(source, **options) }

response = JSON.stub(:parse, keyword_options_only_parse) do
client.send_request(request: { jsonrpc: "2.0", id: "test_id", method: "tools/list" })
end

assert_equal({ "result" => { "tools" => [] } }, response)
end

def test_send_request_mirrors_x_mcp_header_params_into_mcp_param_headers
# SEP-2243: on a modern connection, `tools/list` teaches the transport the `x-mcp-header`
# declarations, and the following `tools/call` mirrors the annotated arguments into
Expand Down