diff --git a/src/mcp-server.rs b/src/mcp-server.rs index 91a67b0..43d7b33 100644 --- a/src/mcp-server.rs +++ b/src/mcp-server.rs @@ -23,7 +23,11 @@ struct Request { method: String, #[serde(default)] params: Value, - id: Value, + /// Absent for JSON-RPC notifications (fire-and-forget); present for + /// requests that expect a response. Note that `Some(Value::Null)` is + /// an explicit null id (still a request), distinct from `None`. + #[serde(default)] + id: Option, } #[derive(Serialize)] @@ -50,7 +54,10 @@ struct DaemonResponse { id: Value, } -fn respond(id: Value, result: Value) { +/// Write a successful response for a request. Silently no-ops for +/// notifications (id = None) — they are fire-and-forget. +fn respond(id: Option, result: Value) { + let Some(id) = id else { return; }; let resp = Response { jsonrpc: "2.0".into(), result, id }; let json = serde_json::to_string(&resp).unwrap(); let mut stdout = io::stdout().lock(); @@ -58,7 +65,11 @@ fn respond(id: Value, result: Value) { let _ = stdout.flush(); } -fn respond_error(id: Value, code: i64, message: &str) { +/// Write an error response for a request. Silently no-ops for +/// notifications (id = None) per JSON-RPC 2.0 — a server MUST NOT reply +/// to a notification, even with an error. +fn respond_error(id: Option, code: i64, message: &str) { + let Some(id) = id else { return; }; let resp = ErrorResponse { jsonrpc: "2.0".into(), error: json!({ "code": code, "message": message }), @@ -232,7 +243,8 @@ fn main() { Err(e) => { eprintln!("consciousness-mcp: bad json-rpc: {e}"); eprintln!("consciousness-mcp: input was: {line}"); - respond_error(Value::Null, -32700, &format!("parse error: {e}")); + // Per spec, parse errors get a response with id: null. + respond_error(Some(Value::Null), -32700, &format!("parse error: {e}")); continue; } };