diff --git a/lib/json_rpc_handler.rb b/lib/json_rpc_handler.rb index 6cbce1cb..8df32fb4 100644 --- a/lib/json_rpc_handler.rb +++ b/lib/json_rpc_handler.rb @@ -202,6 +202,6 @@ def error_response(id:, id_validation_pattern:, error:) jsonrpc: Version::V2_0, id: valid_id?(id, id_validation_pattern) ? id : nil, error: error.compact, - } unless id.nil? + }.compact unless id.nil? end end diff --git a/lib/mcp/server/transports/streamable_http_transport.rb b/lib/mcp/server/transports/streamable_http_transport.rb index ef85ef2f..e676f3fa 100644 --- a/lib/mcp/server/transports/streamable_http_transport.rb +++ b/lib/mcp/server/transports/streamable_http_transport.rb @@ -2033,7 +2033,7 @@ def validate_protocol_version_header(request) def json_rpc_error_response(status:, code:, message:, data: nil, id: nil) error = { code: code, message: message } error[:data] = data if data - body = { jsonrpc: "2.0", id: id, error: error } + body = { jsonrpc: "2.0", id: id, error: error }.compact [status, { "content-type" => "application/json" }, [body.to_json]] end @@ -2486,7 +2486,7 @@ def invalid_request_response(message, request_id: nil) code: JsonRpcHandler::ErrorCode::INVALID_REQUEST, message: message, }, - } + }.compact [400, { "content-type" => "application/json" }, [body.to_json]] end diff --git a/test/json_rpc_handler_test.rb b/test/json_rpc_handler_test.rb index 07db0ecf..7556df8c 100644 --- a/test/json_rpc_handler_test.rb +++ b/test/json_rpc_handler_test.rb @@ -290,6 +290,8 @@ # If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request), it MUST be # Null. # + # MCP leaves the id out instead: an error response has an optional string or integer id, never null. + # # Either the result member or error member MUST be included, but both members MUST NOT be included. it "returns a result object and no error object on success" do @@ -315,7 +317,7 @@ assert_nil @response[:result] end - it "returns nil for id when there is an error and and error detecting the id" do + it "omits the id when there is an error and an error detecting the id" do register("ping") { "pong" } handle jsonrpc: "2.0", id: {}, method: "ping" @@ -325,7 +327,7 @@ message: "Invalid Request", data: "Request ID must match validation pattern, or be an integer or null", } - assert_nil @response[:id] + refute @response.key?(:id) end it "returns the same request id on an Invalid Request error when the id is detectable" do @@ -339,7 +341,7 @@ assert_equal 3, @response[:id] end - it "returns nil for id on an Invalid Request error when the request has no id" do + it "omits the id on an Invalid Request error when the request has no id" do handle jsonrpc: "1.0", method: "ping", params: {} assert_rpc_error expected_error: { @@ -347,10 +349,10 @@ message: "Invalid Request", data: "JSON-RPC version must be 2.0", } - assert_nil @response[:id] + refute @response.key?(:id) end - it "returns nil for id on an Invalid Request error when the request id is explicitly null" do + it "omits the id on an Invalid Request error when the request id is explicitly null" do handle jsonrpc: "1.0", id: nil, method: "ping", params: {} assert_rpc_error expected_error: { @@ -358,10 +360,10 @@ message: "Invalid Request", data: "JSON-RPC version must be 2.0", } - assert_nil @response[:id] + refute @response.key?(:id) end - it "returns nil for id on an Invalid Request error when the id fails validation" do + it "omits the id on an Invalid Request error when the id fails validation" do handle jsonrpc: "1.0", id: "", method: "ping", params: {} assert_rpc_error expected_error: { @@ -369,7 +371,7 @@ message: "Invalid Request", data: "JSON-RPC version must be 2.0", } - assert_nil @response[:id] + refute @response.key?(:id) end # 5.1 Error object @@ -405,6 +407,7 @@ message: "Parse error", data: "Invalid JSON", } + refute @response.key?(:id) end it "returns an error with code set to -32600 when the request is not an array or a hash" do diff --git a/test/mcp/server/transports/streamable_http_transport_test.rb b/test/mcp/server/transports/streamable_http_transport_test.rb index 1e1ebc7a..3f431296 100644 --- a/test/mcp/server/transports/streamable_http_transport_test.rb +++ b/test/mcp/server/transports/streamable_http_transport_test.rb @@ -171,7 +171,7 @@ def string body = JSON.parse(response[2][0]) assert_equal "2.0", body["jsonrpc"] - assert_nil body["id"] + refute body.key?("id") assert_equal JsonRpcHandler::ErrorCode::PARSE_ERROR, body["error"]["code"] assert_equal "Parse error: Invalid JSON", body["error"]["message"] end @@ -300,7 +300,7 @@ def string assert_equal 404, response[0] body = JSON.parse(response[2][0]) assert_equal "2.0", body["jsonrpc"] - assert_nil body["id"] + refute body.key?("id") assert_equal JsonRpcHandler::ErrorCode::INVALID_REQUEST, body["error"]["code"] assert_equal "Session not found", body["error"]["message"] end @@ -388,7 +388,7 @@ def string assert_equal 400, response[0] body = JSON.parse(response[2][0]) assert_equal "2.0", body["jsonrpc"] - assert_nil body["id"] + refute body.key?("id") assert_equal JsonRpcHandler::ErrorCode::INVALID_REQUEST, body["error"]["code"] assert_match(/single request object/i, body["error"]["message"]) end @@ -553,7 +553,7 @@ def string body = JSON.parse(response[2][0]) assert_equal "2.0", body["jsonrpc"] - assert_nil body["id"] + refute body.key?("id") assert_equal JsonRpcHandler::ErrorCode::INVALID_REQUEST, body["error"]["code"] assert_equal "Missing session ID", body["error"]["message"] end @@ -711,7 +711,7 @@ def string body = JSON.parse(response[2][0]) assert_equal "2.0", body["jsonrpc"] - assert_nil body["id"] + refute body.key?("id") assert_equal JsonRpcHandler::ErrorCode::INVALID_REQUEST, body["error"]["code"] assert_equal "Conflict: Only one SSE stream is allowed per session", body["error"]["message"] end @@ -1618,7 +1618,7 @@ def string body = JSON.parse(response[2][0]) assert_equal "2.0", body["jsonrpc"] - assert_nil body["id"] + refute body.key?("id") assert_equal JsonRpcHandler::ErrorCode::INVALID_REQUEST, body["error"]["code"] assert_equal "Method not allowed", body["error"]["message"] end @@ -1636,7 +1636,7 @@ def string body = JSON.parse(response[2][0]) assert_equal "2.0", body["jsonrpc"] - assert_nil body["id"] + refute body.key?("id") assert_equal JsonRpcHandler::ErrorCode::INVALID_REQUEST, body["error"]["code"] assert_equal "Unsupported Media Type: Content-Type must be application/json", body["error"]["message"] end @@ -1831,7 +1831,7 @@ def string body = JSON.parse(response[2][0]) assert_equal "2.0", body["jsonrpc"] - assert_nil body["id"] + refute body.key?("id") assert_equal JsonRpcHandler::ErrorCode::INVALID_REQUEST, body["error"]["code"] assert_equal "Not Acceptable: Accept header must include text/event-stream", body["error"]["message"] end @@ -2996,7 +2996,7 @@ def string body = JSON.parse(response[2][0]) assert_equal "2.0", body["jsonrpc"] - assert_nil body["id"] + refute body.key?("id") assert_equal JsonRpcHandler::ErrorCode::INTERNAL_ERROR, body["error"]["code"] assert_equal "Internal server error", body["error"]["message"] end @@ -6758,9 +6758,8 @@ def string parsed = JSON.parse(response[2][0]) - # The member is present and null, as JSON-RPC 2.0 requires when the id cannot be determined. - assert parsed.key?("id") - assert_nil parsed["id"] + # The member is left out: MCP allows no null id, where JSON-RPC 2.0 would set it to null. + refute parsed.key?("id") assert_equal JsonRpcHandler::ErrorCode::INVALID_REQUEST, parsed.dig("error", "code") assert_empty @transport.instance_variable_get(:@listen_subscriptions) end