Skip to content
Closed
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
28 changes: 25 additions & 3 deletions lib/mcp/server/transports/streamable_http_transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -905,8 +905,8 @@ def handle_modern(request, header_version, body_string: nil)
message: "Unsupported protocol version",
data: {
supported: MCP::Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS,
requested: header_version,
},
requested: utf8_header_value(header_version),
}.compact,
id: body[:id],
)
end
Expand Down Expand Up @@ -1000,6 +1000,13 @@ def validate_modern_headers(request, body, header_version)
body[:id],
)
end
method_header = utf8_header_value(method_header)
unless method_header
return header_mismatch_response(
"Mcp-Method header value is not valid UTF-8",
body[:id],
)
end
if method_header != body[:method]
return header_mismatch_response(
"Mcp-Method header value '#{method_header}' does not match body value '#{body[:method]}'",
Expand All @@ -1019,6 +1026,12 @@ def validate_modern_headers(request, body, header_version)
end

decoded_name = decode_header_value(name_header)
unless decoded_name
return header_mismatch_response(
"Mcp-Name header value is not valid UTF-8",
body[:id],
)
end
if decoded_name != body_name
return header_mismatch_response(
"Mcp-Name header value '#{decoded_name}' does not match body value '#{body_name}'",
Expand Down Expand Up @@ -1580,14 +1593,23 @@ def header_mismatch_response(message, id)
)
end

def utf8_header_value(value)
decoded = value.dup.force_encoding(Encoding::UTF_8)
decoded if decoded.valid_encoding?
end

# Mirrors `MCP::Client::HTTP#encode_header_value`: a value wrapped as `=?base64?<base64>?=` decodes to
# its original UTF-8 string; anything else is taken verbatim. Duplicated here because the client transport
# requires faraday, which servers do not depend on.
def decode_header_value(value)
value = utf8_header_value(value)
return unless value

match = value.match(/\A=\?base64\?(.*)\?=\z/m)
return value unless match

match[1].unpack1("m0").force_encoding(Encoding::UTF_8)
decoded = match[1].unpack1("m0").force_encoding(Encoding::UTF_8)
decoded if decoded.valid_encoding?
rescue ArgumentError
value
end
Expand Down
38 changes: 38 additions & 0 deletions test/mcp/server/transports/streamable_http_transport_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6327,6 +6327,44 @@ def string
assert_equal "2027-01-01", body.dig("error", "data", "requested")
end

test "modern POST rejects standard headers that are not valid UTF-8" do
reported = []
original_reporter = MCP.configuration.exception_reporter
MCP.configuration.exception_reporter = ->(error, _context) { reported << error }
@server.expects(:handle).never

cases = [
["HTTP_MCP_PROTOCOL_VERSION", "2026-07-28\xFF", -32022, "Unsupported protocol version"],
["HTTP_MCP_METHOD", "tools/call\xFF", -32020, "Header mismatch: Mcp-Method header value is not valid UTF-8"],
["HTTP_MCP_NAME", "some_tool\xFF", -32020, "Header mismatch: Mcp-Name header value is not valid UTF-8"],
["HTTP_MCP_NAME", "=?base64?/w==?=", -32020, "Header mismatch: Mcp-Name header value is not valid UTF-8"],
]
cases.each do |header_name, value, code, message|
[Encoding::BINARY, Encoding::UTF_8].each do |encoding|
header_value = value.dup.force_encoding(encoding).freeze
response = @transport.handle_request(modern_rack_request(
modern_body("tools/call", { name: "some_tool", arguments: {} }),
headers: { header_name => header_value },
))

expected_error = { "code" => code, "message" => message }
if code == -32022
expected_error["data"] = { "supported" => Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS }
end

assert_equal 400, response[0], "#{header_name} (#{encoding})"
assert_equal({ "content-type" => "application/json" }, response[1])
assert_equal(
{ "jsonrpc" => "2.0", "id" => 1, "error" => expected_error },
JSON.parse(response[2][0]),
)
assert_empty reported
end
end
ensure
MCP.configuration.exception_reporter = original_reporter
end

test "modern POST with a header/body version mismatch returns 400 with -32020" do
# Header names the supported modern version; the body envelope claims another.
response = @transport.handle_request(modern_rack_request(
Expand Down
Loading