Skip to content
Merged
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
24 changes: 19 additions & 5 deletions lib/mcp/server/transports/streamable_http_transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -339,8 +341,11 @@ def handle_post(request)
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

unless initialize_request?(body)
return missing_session_id_response if !@stateless && !session_id
Expand All @@ -349,7 +354,8 @@ def handle_post(request)
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)
Expand Down Expand Up @@ -507,11 +513,19 @@ def not_acceptable_response(required_types)
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)
Expand Down
54 changes: 54 additions & 0 deletions test/mcp/server/transports/streamable_http_transport_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,60 @@ def string
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",
Expand Down