Skip to content

Commit 9caba30

Browse files
authored
Merge pull request #354 from koic/reject_non_hash_post_body_in_streamable_http_transport
Reject non-Hash JSON-RPC bodies in `StreamableHTTPTransport`
2 parents 993dbf5 + f62d813 commit 9caba30

2 files changed

Lines changed: 73 additions & 5 deletions

File tree

lib/mcp/server/transports/streamable_http_transport.rb

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ module MCP
1616
class Server
1717
module Transports
1818
class StreamableHTTPTransport < Transport
19+
class InvalidJsonError < StandardError; end
20+
1921
SSE_HEADERS = {
2022
"Content-Type" => "text/event-stream",
2123
"Cache-Control" => "no-cache",
@@ -339,8 +341,11 @@ def handle_post(request)
339341
body_string = request.body.read
340342
session_id = extract_session_id(request)
341343

342-
body = parse_request_body(body_string)
343-
return body if parse_error_tuple?(body)
344+
begin
345+
body = parse_request_body(body_string)
346+
rescue InvalidJsonError
347+
return invalid_json_response
348+
end
344349

345350
# The `MCP-Protocol-Version` header is only meaningful after negotiation, so on `initialize`
346351
# the JSON-RPC body `params.protocolVersion` is authoritative and the header (if any) is ignored.
@@ -352,7 +357,8 @@ def handle_post(request)
352357
return protocol_version_error if protocol_version_error
353358
end
354359

355-
return body unless body.is_a?(Hash) # Non-Hash JSON-RPC bodies are not supported in 2025-11-25.
360+
# MCP 2025-11-25 does not support JSON-RPC batch, so the body must be a single message object.
361+
return non_hash_body_response unless body.is_a?(Hash)
356362

357363
if initialize_request?(body)
358364
handle_initialization(body_string, body)
@@ -510,11 +516,19 @@ def not_acceptable_response(required_types)
510516
def parse_request_body(body_string)
511517
JSON.parse(body_string, symbolize_names: true)
512518
rescue JSON::ParserError, TypeError
519+
raise InvalidJsonError
520+
end
521+
522+
def invalid_json_response
513523
[400, { "Content-Type" => "application/json" }, [{ error: "Invalid JSON" }.to_json]]
514524
end
515525

516-
def parse_error_tuple?(body)
517-
body.is_a?(Array) && body.size == 3 && body[0] == 400
526+
def non_hash_body_response
527+
[
528+
400,
529+
{ "Content-Type" => "application/json" },
530+
[{ error: "Bad Request: request body must be a single JSON-RPC message object" }.to_json],
531+
]
518532
end
519533

520534
def initialize_request?(body)

test/mcp/server/transports/streamable_http_transport_test.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,60 @@ def string
9696
assert_equal "Invalid JSON", body["error"]
9797
end
9898

99+
test "POST request with JSON array body returns 400" do
100+
init_request = create_rack_request(
101+
"POST",
102+
"/",
103+
{ "CONTENT_TYPE" => "application/json" },
104+
{ jsonrpc: "2.0", method: "initialize", id: "init" }.to_json,
105+
)
106+
init_response = @transport.handle_request(init_request)
107+
session_id = init_response[1]["Mcp-Session-Id"]
108+
109+
request = create_rack_request(
110+
"POST",
111+
"/",
112+
{
113+
"CONTENT_TYPE" => "application/json",
114+
"HTTP_MCP_SESSION_ID" => session_id,
115+
},
116+
[{ jsonrpc: "2.0", method: "tools/list", id: "list" }].to_json,
117+
)
118+
119+
response = @transport.handle_request(request)
120+
assert_equal 400, response[0]
121+
122+
body = JSON.parse(response[2][0])
123+
assert_includes body["error"], "single JSON-RPC message object"
124+
end
125+
126+
test "POST request with non-object JSON body returns 400" do
127+
init_request = create_rack_request(
128+
"POST",
129+
"/",
130+
{ "CONTENT_TYPE" => "application/json" },
131+
{ jsonrpc: "2.0", method: "initialize", id: "init" }.to_json,
132+
)
133+
init_response = @transport.handle_request(init_request)
134+
session_id = init_response[1]["Mcp-Session-Id"]
135+
136+
request = create_rack_request(
137+
"POST",
138+
"/",
139+
{
140+
"CONTENT_TYPE" => "application/json",
141+
"HTTP_MCP_SESSION_ID" => session_id,
142+
},
143+
"\"foo\"",
144+
)
145+
146+
response = @transport.handle_request(request)
147+
assert_equal 400, response[0]
148+
149+
body = JSON.parse(response[2][0])
150+
assert_includes body["error"], "single JSON-RPC message object"
151+
end
152+
99153
test "handles POST request with initialize method" do
100154
request = create_rack_request(
101155
"POST",

0 commit comments

Comments
 (0)