Merge pull request #546 from koic/parse_json_response_bodies_in_the_client

Parse HTTP client response bodies without Faraday's JSON middleware
This commit is contained in:
Koichi ITO 2026-09-08 18:13:24 +09:00 • committed by GitHub
commit d0b22ef722
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 27 additions and 3 deletions

View file

@ -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"

View file

@ -723,7 +723,8 @@ module MCP
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
@ -1074,8 +1075,8 @@ module MCP
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(

View file

@ -940,6 +940,26 @@ module MCP
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