Merge pull request #492 from koic/require_mcp_method_header_on_the_modern_path

Require the `Mcp-Method` header on the modern path
This commit is contained in:
Koichi ITO 2026-08-08 15:29:32 +09:00 • committed by GitHub
commit 2c3c3075b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 16 deletions

View file

@ -634,23 +634,43 @@ module MCP
) )
end end
# `Mcp-Method` is required on every modern POST and `Mcp-Name` on the name-bearing methods:
# the spec lists a missing required standard header among the validation failures that MUST be rejected,
# and the TypeScript SDK enforces presence the same way. Absence cannot be treated as "nothing to compare":
# the headers exist so intermediaries can route without parsing bodies, and a client that omits them
# defeats that contract silently.
method_header = request.env["HTTP_MCP_METHOD"] method_header = request.env["HTTP_MCP_METHOD"]
if method_header && method_header != body[:method] if method_header.to_s.empty?
return header_mismatch_response(
"Mcp-Method header is required on the modern path",
body[:id],
)
end
if method_header != body[:method]
return header_mismatch_response( return header_mismatch_response(
"Mcp-Method header value '#{method_header}' does not match body value '#{body[:method]}'", "Mcp-Method header value '#{method_header}' does not match body value '#{body[:method]}'",
body[:id], body[:id],
) )
end end
name_header = request.env["HTTP_MCP_NAME"] if NAME_BEARING_METHODS.include?(body[:method])
if name_header && NAME_BEARING_METHODS.include?(body[:method])
body_name = params.is_a?(Hash) ? params[:name] || params[:uri] : nil body_name = params.is_a?(Hash) ? params[:name] || params[:uri] : nil
decoded_name = decode_header_value(name_header) name_header = request.env["HTTP_MCP_NAME"]
if body_name && decoded_name != body_name if body_name
return header_mismatch_response( if name_header.to_s.empty?
"Mcp-Name header value '#{decoded_name}' does not match body value '#{body_name}'", return header_mismatch_response(
body[:id], "Mcp-Name header is required for `#{body[:method]}`",
) body[:id],
)
end
decoded_name = decode_header_value(name_header)
if decoded_name != body_name
return header_mismatch_response(
"Mcp-Name header value '#{decoded_name}' does not match body value '#{body_name}'",
body[:id],
)
end
end end
end end

View file

@ -5340,6 +5340,31 @@ module MCP
assert_equal(-32020, JSON.parse(response[2][0]).dig("error", "code")) assert_equal(-32020, JSON.parse(response[2][0]).dig("error", "code"))
end end
test "modern POST without the Mcp-Method header returns 400 with -32020" do
# The header is required on every modern POST; a missing required standard header is
# a validation failure, matching the TypeScript SDK's enforcement.
response = @transport.handle_request(modern_rack_request(
modern_body("ping", {}),
method_header: nil,
))
assert_equal 400, response[0]
body = JSON.parse(response[2][0])
assert_equal(-32020, body.dig("error", "code"))
assert_includes body.dig("error", "message"), "Mcp-Method"
end
test "modern POST without the Mcp-Name header on a name-bearing method returns 400 with -32020" do
response = @transport.handle_request(modern_rack_request(
modern_body("tools/call", { name: "some_tool", arguments: {} }),
))
assert_equal 400, response[0]
body = JSON.parse(response[2][0])
assert_equal(-32020, body.dig("error", "code"))
assert_includes body.dig("error", "message"), "Mcp-Name"
end
test "modern POST decodes the base64 Mcp-Name sentinel and enforces the match" do test "modern POST decodes the base64 Mcp-Name sentinel and enforces the match" do
# `Mcp-Name` mirrors `params.uri` for `resources/read`; a non-ASCII value arrives # `Mcp-Name` mirrors `params.uri` for `resources/read`; a non-ASCII value arrives
# wrapped in the `=?base64?...?=` sentinel produced by `MCP::Client::HTTP`. # wrapped in the `=?base64?...?=` sentinel produced by `MCP::Client::HTTP`.
@ -5410,6 +5435,7 @@ module MCP
response = @transport.handle_request(modern_rack_request( response = @transport.handle_request(modern_rack_request(
modern_body("tools/call", { name: "guarded_tool", arguments: {} }), modern_body("tools/call", { name: "guarded_tool", arguments: {} }),
headers: { "HTTP_MCP_NAME" => "guarded_tool" },
)) ))
assert_equal 400, response[0] assert_equal 400, response[0]
@ -5762,13 +5788,24 @@ module MCP
end end
# Builds a POST request routed to the modern path via the `MCP-Protocol-Version` header. # Builds a POST request routed to the modern path via the `MCP-Protocol-Version` header.
def modern_rack_request(body, version: "2026-07-28", headers: {}) # Mirrors what a conforming modern client sends: `Mcp-Method` is required on every modern POST,
create_rack_request( # so it defaults to the body's method. Pass `method_header: nil` to omit it
"POST", # (for tests of the requirement itself) or a String to send a specific value.
"/", def modern_rack_request(body, version: "2026-07-28", headers: {}, method_header: :from_body)
{ "CONTENT_TYPE" => "application/json", "HTTP_MCP_PROTOCOL_VERSION" => version }.merge(headers), derived_method = if method_header == :from_body
body, begin
) parsed = JSON.parse(body)
parsed["method"] if parsed.is_a?(Hash)
rescue JSON::ParserError
nil
end
else
method_header
end
base_headers = { "CONTENT_TYPE" => "application/json", "HTTP_MCP_PROTOCOL_VERSION" => version }
base_headers["HTTP_MCP_METHOD"] = derived_method if derived_method
create_rack_request("POST", "/", base_headers.merge(headers), body)
end end
# Builds a JSON-RPC body carrying the SEP-2575 modern `_meta` envelope. # Builds a JSON-RPC body carrying the SEP-2575 modern `_meta` envelope.