diff --git a/lib/mcp/server/transports/streamable_http_transport.rb b/lib/mcp/server/transports/streamable_http_transport.rb index fd45b332..bd395696 100644 --- a/lib/mcp/server/transports/streamable_http_transport.rb +++ b/lib/mcp/server/transports/streamable_http_transport.rb @@ -517,6 +517,11 @@ def handle_post(request) end if notification?(body) + # Reject a notification carrying an unknown or expired session ID instead of + # dispatching it against the shared `Server`. Mirrors the response and regular-request + # branches; without it a custom notification handler could run without a live session. + return session_not_found_response if !@stateless && !session_active?(session_id) + dispatch_notification(body_string, session_id) handle_accepted elsif response?(body) diff --git a/test/mcp/server/transports/streamable_http_transport_test.rb b/test/mcp/server/transports/streamable_http_transport_test.rb index d36e1b07..3b79ce85 100644 --- a/test/mcp/server/transports/streamable_http_transport_test.rb +++ b/test/mcp/server/transports/streamable_http_transport_test.rb @@ -525,6 +525,40 @@ def string assert_equal "Missing session ID", body["error"]["message"] end + test "rejects notification with an unknown session ID in stateful mode" do + request = create_rack_request( + "POST", + "/", + { "CONTENT_TYPE" => "application/json", "HTTP_MCP_SESSION_ID" => "does-not-exist" }, + { jsonrpc: "2.0", method: "notifications/initialized" }.to_json, + ) + + response = @transport.handle_request(request) + assert_equal 404, response[0] + body = JSON.parse(response[2][0]) + assert_equal "Session not found", body["error"]["message"] + end + + test "accepts a notification carrying a live session ID in stateful mode" do + init_request = create_rack_request( + "POST", + "/", + { "CONTENT_TYPE" => "application/json" }, + { jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json, + ) + session_id = @transport.handle_request(init_request)[1]["Mcp-Session-Id"] + + notification = create_rack_request( + "POST", + "/", + { "CONTENT_TYPE" => "application/json", "HTTP_MCP_SESSION_ID" => session_id }, + { jsonrpc: "2.0", method: "notifications/initialized" }.to_json, + ) + + response = @transport.handle_request(notification) + assert_equal 202, response[0] + end + test "rejects response without session ID in stateful mode" do request = create_rack_request( "POST",