Skip to content
Closed
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
2 changes: 1 addition & 1 deletion lib/json_rpc_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions lib/mcp/server/transports/streamable_http_transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
19 changes: 11 additions & 8 deletions test/json_rpc_handler_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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
Expand All @@ -339,37 +341,37 @@
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: {
code: -32600,
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: {
code: -32600,
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: "<script>alert('xss')</script>", method: "ping", params: {}

assert_rpc_error expected_error: {
code: -32600,
message: "Invalid Request",
data: "JSON-RPC version must be 2.0",
}
assert_nil @response[:id]
refute @response.key?(:id)
end

# 5.1 Error object
Expand Down Expand Up @@ -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
Expand Down
23 changes: 11 additions & 12 deletions test/mcp/server/transports/streamable_http_transport_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down