Skip to content

Commit d0b22ef

Browse files
authored
Merge pull request #546 from koic/parse_json_response_bodies_in_the_client
Parse HTTP client response bodies without Faraday's JSON middleware
2 parents 44af58c + d40d901 commit d0b22ef

3 files changed

Lines changed: 27 additions & 3 deletions

File tree

‎Gemfile‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ gem "activesupport"
1919
gem "debug" if RUBY_VERSION >= "3.1"
2020
# Avoid i18n 1.15.0, which breaks on Ruby 3.1 (ruby-i18n/i18n#735).
2121
gem "i18n", "!= 1.15.0"
22+
# FIXME: Drop this once a json release includes https://github.com/ruby/json/pull/1072. json 3.0.0 and 3.0.1
23+
# forward arguments after a leading parameter, syntax Ruby 2.7.3 was the first to parse, while allowing Ruby 2.7.0.
24+
gem "json", "< 3" if RUBY_VERSION < "2.7.3"
2225
gem "rake", "~> 13.0"
2326
gem "sorbet-static-and-runtime" if RUBY_VERSION >= "3.0"
2427
gem "yard", "~> 0.9"

‎lib/mcp/client/http.rb‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,8 @@ def client
723723
require_faraday!
724724
@client ||= Faraday.new(url) do |faraday|
725725
faraday.request(:json)
726-
faraday.response(:json)
726+
# The client parses response bodies itself (`resolve_response_body`), so streamed and buffered responses
727+
# take the same path; Faraday's json response middleware is deliberately left out.
727728
faraday.response(:raise_error)
728729

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

1077-
# Adapters without `on_data` support deliver the body via `response.body`,
1078-
# already parsed by the json response middleware.
1078+
# Adapters without `on_data` support deliver the body via `response.body`; a JSON middleware
1079+
# added by the Faraday customizer may have parsed it already.
10791080
response.body.is_a?(String) ? parse_json_buffer(response.body, method, params) : response.body
10801081
else
10811082
raise RequestHandlerError.new(

‎test/mcp/client/http_test.rb‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,26 @@ def test_send_request_parses_json_response_when_adapter_does_not_stream
940940
assert_equal({ "result" => { "tools" => [] } }, response)
941941
end
942942

943+
def test_send_request_parses_a_json_body_with_a_parser_taking_keyword_options_only
944+
# json 3.0 accepts parser options as keywords only, and Faraday's json response middleware
945+
# passes them as a positional Hash, which Ruby 3 no longer converts. The stand-in parser has
946+
# the json 3.0 signature, so the body must reach `JSON.parse` through the client's own call.
947+
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
948+
stub.post("/") do
949+
[200, { "Content-Type" => "application/json" }, { result: { tools: [] } }.to_json]
950+
end
951+
end
952+
client = HTTP.new(url: url) { |faraday| faraday.adapter(:test, stubs) }
953+
parse = JSON.method(:parse)
954+
keyword_options_only_parse = ->(source, **options) { parse.call(source, **options) }
955+
956+
response = JSON.stub(:parse, keyword_options_only_parse) do
957+
client.send_request(request: { jsonrpc: "2.0", id: "test_id", method: "tools/list" })
958+
end
959+
960+
assert_equal({ "result" => { "tools" => [] } }, response)
961+
end
962+
943963
def test_send_request_mirrors_x_mcp_header_params_into_mcp_param_headers
944964
# SEP-2243: on a modern connection, `tools/list` teaches the transport the `x-mcp-header`
945965
# declarations, and the following `tools/call` mirrors the annotated arguments into

0 commit comments

Comments
 (0)