diff --git a/memoria/crates/memoria-api/src/routes/mcp.rs b/memoria/crates/memoria-api/src/routes/mcp.rs index 339b7b76..ec84b045 100644 --- a/memoria/crates/memoria-api/src/routes/mcp.rs +++ b/memoria/crates/memoria-api/src/routes/mcp.rs @@ -260,6 +260,16 @@ pub async fn mcp_handler( ); } + // Reject non-conforming IDs before ping, notification, or tool dispatch. + if req.get("id").is_some_and(|id| !(id.is_string() || id.is_number() || id.is_null())) { + validation_err!( + "/mcp/_invalid_request", + -32600, + json!({"jsonrpc": "2.0", "id": null, + "error": {"code": -32600, "message": "Invalid Request: id must be string, number or null"}}) + ); + } + // "jsonrpc" MUST equal exactly "2.0". if req.get("jsonrpc").and_then(|v| v.as_str()) != Some("2.0") { let id = req.get("id").cloned().unwrap_or(serde_json::Value::Null); diff --git a/memoria/crates/memoria-mcp/src/server.rs b/memoria/crates/memoria-mcp/src/server.rs index ec3f8eb1..9de1ee1b 100644 --- a/memoria/crates/memoria-mcp/src/server.rs +++ b/memoria/crates/memoria-mcp/src/server.rs @@ -264,7 +264,7 @@ fn sse_router( // payloads still need an Invalid Request response, even without id. // Preserve the legacy endpoint's jsonrpc-field compatibility here; // tightening envelope validation is separate from notification handling. - if !req.is_object() || method.is_empty() { + if !req.is_object() || method.is_empty() || req.get("id").is_some_and(|id| !(id.is_string() || id.is_number() || id.is_null())) { let response_id = req.get("id").filter(|id| id.is_string() || id.is_number()).cloned().unwrap_or(Value::Null); let resp = serde_json::json!({"jsonrpc":"2.0","id":response_id, "error":{"code":-32600,"message":"Invalid Request"}}); @@ -354,8 +354,9 @@ async fn run_io( .cloned() .unwrap_or(Value::Null); let is_object = value.is_object(); + let valid_id = value.get("id").is_none_or(|id| id.is_string() || id.is_number() || id.is_null()); let req = match serde_json::from_value::(value) { - Ok(req) if is_object && !req.method.is_empty() => req, + Ok(req) if is_object && valid_id && !req.method.is_empty() => req, _ => { let resp = json!({"jsonrpc":"2.0","id":response_id, "error":{"code":-32600,"message":"Invalid Request"}});