From 88d9bed69d3b6110e6a340388f30569abd192d5c Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Sat, 28 Mar 2026 02:45:58 +0900 Subject: [PATCH] Reject POST requests without session ID in stateful mode Per the MCP specification (Streamable HTTP > Session Management): > Servers that require a session ID SHOULD respond to requests without an `Mcp-Session-Id` header > (other than initialization) with HTTP 400 Bad Request. https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management Previously, non-initialize POST requests without `Mcp-Session-Id` in stateful mode were processed with HTTP 200 (for regular requests) or HTTP 202 (for notifications/responses). This change adds an explicit check in `handle_post` to return HTTP 400 Bad Request when the session ID is missing in stateful mode for all non-initialize requests, aligning with the specification. --- .../transports/streamable_http_transport.rb | 10 +++- .../streamable_http_transport_test.rb | 56 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/lib/mcp/server/transports/streamable_http_transport.rb b/lib/mcp/server/transports/streamable_http_transport.rb index f1a5d9d..2b658ec 100644 --- a/lib/mcp/server/transports/streamable_http_transport.rb +++ b/lib/mcp/server/transports/streamable_http_transport.rb @@ -120,10 +120,14 @@ def handle_post(request) if body["method"] == "initialize" handle_initialization(body_string, body) - elsif notification?(body) || response?(body) - handle_accepted else - handle_regular_request(body_string, session_id) + return missing_session_id_response if !@stateless && !session_id + + if notification?(body) || response?(body) + handle_accepted + else + handle_regular_request(body_string, session_id) + end end rescue StandardError => e MCP.configuration.exception_reporter.call(e, { request: body_string }) diff --git a/test/mcp/server/transports/streamable_http_transport_test.rb b/test/mcp/server/transports/streamable_http_transport_test.rb index 4b346c4..94b6226 100644 --- a/test/mcp/server/transports/streamable_http_transport_test.rb +++ b/test/mcp/server/transports/streamable_http_transport_test.rb @@ -272,6 +272,62 @@ class StreamableHTTPTransportTest < ActiveSupport::TestCase assert_equal "Missing session ID", body["error"] end + test "rejects POST request without session ID in stateful mode" do + request = create_rack_request( + "POST", + "/", + { "CONTENT_TYPE" => "application/json" }, + { jsonrpc: "2.0", method: "ping", id: "1" }.to_json, + ) + + response = @transport.handle_request(request) + assert_equal 400, response[0] + body = JSON.parse(response[2][0]) + assert_equal "Missing session ID", body["error"] + end + + test "rejects notification without session ID in stateful mode" do + request = create_rack_request( + "POST", + "/", + { "CONTENT_TYPE" => "application/json" }, + { jsonrpc: "2.0", method: "notifications/initialized" }.to_json, + ) + + response = @transport.handle_request(request) + assert_equal 400, response[0] + body = JSON.parse(response[2][0]) + assert_equal "Missing session ID", body["error"] + end + + test "rejects response without session ID in stateful mode" do + request = create_rack_request( + "POST", + "/", + { "CONTENT_TYPE" => "application/json" }, + { jsonrpc: "2.0", id: "1", result: {} }.to_json, + ) + + response = @transport.handle_request(request) + assert_equal 400, response[0] + body = JSON.parse(response[2][0]) + assert_equal "Missing session ID", body["error"] + end + + test "allows POST request without session ID in stateless mode" do + stateless_transport = StreamableHTTPTransport.new(@server, stateless: true) + + request = create_rack_request( + "POST", + "/", + { "CONTENT_TYPE" => "application/json" }, + { jsonrpc: "2.0", method: "ping", id: "1" }.to_json, + ) + + response = stateless_transport.handle_request(request) + assert_equal 200, response[0] + end + test "rejects duplicate SSE connection with 409" do # Create a session init_request = create_rack_request(