Merge pull request #354 from koic/reject_non_hash_post_body_in_streamable_http_transport
Reject non-Hash JSON-RPC bodies in `StreamableHTTPTransport`
This commit is contained in:
commit
9caba3085d
2 changed files with 73 additions and 5 deletions
|
|
@ -16,6 +16,8 @@ module MCP
|
|||
class Server
|
||||
module Transports
|
||||
class StreamableHTTPTransport < Transport
|
||||
class InvalidJsonError < StandardError; end
|
||||
|
||||
SSE_HEADERS = {
|
||||
"Content-Type" => "text/event-stream",
|
||||
"Cache-Control" => "no-cache",
|
||||
|
|
@ -339,8 +341,11 @@ module MCP
|
|||
body_string = request.body.read
|
||||
session_id = extract_session_id(request)
|
||||
|
||||
body = parse_request_body(body_string)
|
||||
return body if parse_error_tuple?(body)
|
||||
begin
|
||||
body = parse_request_body(body_string)
|
||||
rescue InvalidJsonError
|
||||
return invalid_json_response
|
||||
end
|
||||
|
||||
# The `MCP-Protocol-Version` header is only meaningful after negotiation, so on `initialize`
|
||||
# the JSON-RPC body `params.protocolVersion` is authoritative and the header (if any) is ignored.
|
||||
|
|
@ -352,7 +357,8 @@ module MCP
|
|||
return protocol_version_error if protocol_version_error
|
||||
end
|
||||
|
||||
return body unless body.is_a?(Hash) # Non-Hash JSON-RPC bodies are not supported in 2025-11-25.
|
||||
# MCP 2025-11-25 does not support JSON-RPC batch, so the body must be a single message object.
|
||||
return non_hash_body_response unless body.is_a?(Hash)
|
||||
|
||||
if initialize_request?(body)
|
||||
handle_initialization(body_string, body)
|
||||
|
|
@ -510,11 +516,19 @@ module MCP
|
|||
def parse_request_body(body_string)
|
||||
JSON.parse(body_string, symbolize_names: true)
|
||||
rescue JSON::ParserError, TypeError
|
||||
raise InvalidJsonError
|
||||
end
|
||||
|
||||
def invalid_json_response
|
||||
[400, { "Content-Type" => "application/json" }, [{ error: "Invalid JSON" }.to_json]]
|
||||
end
|
||||
|
||||
def parse_error_tuple?(body)
|
||||
body.is_a?(Array) && body.size == 3 && body[0] == 400
|
||||
def non_hash_body_response
|
||||
[
|
||||
400,
|
||||
{ "Content-Type" => "application/json" },
|
||||
[{ error: "Bad Request: request body must be a single JSON-RPC message object" }.to_json],
|
||||
]
|
||||
end
|
||||
|
||||
def initialize_request?(body)
|
||||
|
|
|
|||
|
|
@ -96,6 +96,60 @@ module MCP
|
|||
assert_equal "Invalid JSON", body["error"]
|
||||
end
|
||||
|
||||
test "POST request with JSON array body returns 400" do
|
||||
init_request = create_rack_request(
|
||||
"POST",
|
||||
"/",
|
||||
{ "CONTENT_TYPE" => "application/json" },
|
||||
{ jsonrpc: "2.0", method: "initialize", id: "init" }.to_json,
|
||||
)
|
||||
init_response = @transport.handle_request(init_request)
|
||||
session_id = init_response[1]["Mcp-Session-Id"]
|
||||
|
||||
request = create_rack_request(
|
||||
"POST",
|
||||
"/",
|
||||
{
|
||||
"CONTENT_TYPE" => "application/json",
|
||||
"HTTP_MCP_SESSION_ID" => session_id,
|
||||
},
|
||||
[{ jsonrpc: "2.0", method: "tools/list", id: "list" }].to_json,
|
||||
)
|
||||
|
||||
response = @transport.handle_request(request)
|
||||
assert_equal 400, response[0]
|
||||
|
||||
body = JSON.parse(response[2][0])
|
||||
assert_includes body["error"], "single JSON-RPC message object"
|
||||
end
|
||||
|
||||
test "POST request with non-object JSON body returns 400" do
|
||||
init_request = create_rack_request(
|
||||
"POST",
|
||||
"/",
|
||||
{ "CONTENT_TYPE" => "application/json" },
|
||||
{ jsonrpc: "2.0", method: "initialize", id: "init" }.to_json,
|
||||
)
|
||||
init_response = @transport.handle_request(init_request)
|
||||
session_id = init_response[1]["Mcp-Session-Id"]
|
||||
|
||||
request = create_rack_request(
|
||||
"POST",
|
||||
"/",
|
||||
{
|
||||
"CONTENT_TYPE" => "application/json",
|
||||
"HTTP_MCP_SESSION_ID" => session_id,
|
||||
},
|
||||
"\"foo\"",
|
||||
)
|
||||
|
||||
response = @transport.handle_request(request)
|
||||
assert_equal 400, response[0]
|
||||
|
||||
body = JSON.parse(response[2][0])
|
||||
assert_includes body["error"], "single JSON-RPC message object"
|
||||
end
|
||||
|
||||
test "handles POST request with initialize method" do
|
||||
request = create_rack_request(
|
||||
"POST",
|
||||
|
|
|
|||
Loading…
Reference in a new issue