From 6b08edd332cb68f6fc40f2a18a29414e25bb6883 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Thu, 15 Oct 2026 16:28:44 +0900 Subject: [PATCH] Ignore empty members of the Accept header ## Motivation and Context An `Accept` header with an empty list member, such as `application/json,,text/event-stream` or one with a leading comma, makes the Streamable HTTP transport answer a POST with 500 and report a `NoMethodError` to the exception reporter, and raise out of a GET, although the required media types are listed (issue #609): `parse_accept_header` splits each member on `;` and an empty member yields nothing to strip. RFC 9110 section 5.6.1.2 has a recipient parse and ignore a reasonable number of empty list elements, and the TypeScript and Python servers accept such a header, the one by substring checks and the other by stripping each member. `parse_accept_header` now skips empty members, so the POST and GET checks see the media types that are there. Fixes #609. ## How Has This Been Tested? Two new tests in `test/mcp/server/transports/streamable_http_transport_test.rb`, a POST with a doubled and with a leading comma and a GET with a leading comma. Against the previous library the POST test fails with a 500 and the GET test raises. ## Breaking Changes None. --- .../transports/streamable_http_transport.rb | 6 +-- .../streamable_http_transport_test.rb | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/mcp/server/transports/streamable_http_transport.rb b/lib/mcp/server/transports/streamable_http_transport.rb index ef85ef2f..f2ef9313 100644 --- a/lib/mcp/server/transports/streamable_http_transport.rb +++ b/lib/mcp/server/transports/streamable_http_transport.rb @@ -1883,10 +1883,10 @@ def validate_accept_header(request, required_types) nil end + # Empty list members (`application/json,,text/event-stream`), which RFC 9110 section 5.6.1.2 has + # a recipient parse and ignore, are skipped. def parse_accept_header(header) - header.split(",").map do |part| - part.split(";").first.strip.downcase - end + header.split(",").map { |part| part.split(";").first.to_s.strip.downcase }.reject(&:empty?) end def validate_content_type(request) diff --git a/test/mcp/server/transports/streamable_http_transport_test.rb b/test/mcp/server/transports/streamable_http_transport_test.rb index 1e1ebc7a..710e7e35 100644 --- a/test/mcp/server/transports/streamable_http_transport_test.rb +++ b/test/mcp/server/transports/streamable_http_transport_test.rb @@ -1756,6 +1756,23 @@ def string assert_equal 200, response[0] end + test "POST request with empty members in the Accept header succeeds" do + ["application/json,,text/event-stream", ", application/json, text/event-stream"].each do |accept| + request = create_rack_request_without_accept( + "POST", + "/", + { + "CONTENT_TYPE" => "application/json", + "HTTP_ACCEPT" => accept, + }, + { jsonrpc: "2.0", method: "initialize", id: "123", params: initialize_params }.to_json, + ) + + response = @transport.handle_request(request) + assert_equal 200, response[0], accept.inspect + end + end + test "POST request with Accept: */* succeeds" do request = create_rack_request_without_accept( "POST", @@ -1886,6 +1903,30 @@ def string assert_equal "text/event-stream", response[1]["content-type"] end + test "GET request with an empty member in the Accept header succeeds" do + init_request = create_rack_request( + "POST", + "/", + { "CONTENT_TYPE" => "application/json" }, + { jsonrpc: "2.0", method: "initialize", id: "123", params: initialize_params }.to_json, + ) + init_response = @transport.handle_request(init_request) + session_id = init_response[1]["mcp-session-id"] + + request = create_rack_request( + "GET", + "/", + { + "HTTP_MCP_SESSION_ID" => session_id, + "HTTP_ACCEPT" => ",text/event-stream", + }, + ) + + response = @transport.handle_request(request) + assert_equal 200, response[0] + assert_equal "text/event-stream", response[1]["content-type"] + end + test "GET request with Accept: */* succeeds" do init_request = create_rack_request( "POST",