From d25b095788868becdd100eec001de104969c9220 Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 22 Apr 2026 09:13:33 +0100 Subject: [PATCH] fix(cr-2026-04-22): MCP stdio replies with parse-error on malformed JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MAJOR from the 2026-04-22 review (crates/mcp/src/main.rs:26-30): the stdio transport logged malformed JSON lines to stderr and continued without sending any JSON-RPC response. Clients saw silence instead of the -32700 Parse Error they could key off. handle_message has a parse-error branch for shape mismatch, but it never ran for bytes that failed to parse as JSON at all. Exposes a new public helper kon_mcp::parse_error_response(detail) that mirrors the existing internal error_response pattern, filling id with null per JSON-RPC 2.0 §5.1 (parse error, no id recoverable). main.rs now writes that response out before continuing the read loop. Regression test on the helper asserts: jsonrpc "2.0", id null, code -32700, message starts with "Parse error" and includes the underlying serde detail. --- crates/mcp/src/lib.rs | 22 ++++++++++++++++++++++ crates/mcp/src/main.rs | 20 ++++++++++++-------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/crates/mcp/src/lib.rs b/crates/mcp/src/lib.rs index 3a7b7a1..e82b65c 100644 --- a/crates/mcp/src/lib.rs +++ b/crates/mcp/src/lib.rs @@ -335,6 +335,16 @@ fn error_response(id: Value, code: i32, message: String) -> JsonRpcResponse { } } +/// Build a JSON-RPC 2.0 Parse Error response (code -32700, id null), +/// for use by the stdio transport when a raw line fails to parse as +/// JSON at all. `handle_message` covers the shape-mismatch case; this +/// helper covers the `serde_json::from_str` failure in `main.rs` so +/// clients receive a well-formed JSON-RPC reply instead of silence +/// (2026-04-22 review MAJOR). +pub fn parse_error_response(detail: &str) -> JsonRpcResponse { + error_response(Value::Null, -32700, format!("Parse error: {detail}")) +} + #[cfg(test)] mod tests { use super::*; @@ -398,6 +408,18 @@ mod tests { ); } + #[test] + fn parse_error_response_has_jsonrpc_2_0_shape() { + let resp = parse_error_response("expected value at line 1 column 1"); + assert_eq!(resp.jsonrpc, "2.0"); + assert_eq!(resp.id, Value::Null); + assert!(resp.result.is_none()); + let err = resp.error.expect("parse_error_response must carry an error"); + assert_eq!(err.code, -32700); + assert!(err.message.contains("Parse error")); + assert!(err.message.contains("expected value")); + } + #[tokio::test] async fn unknown_method_returns_method_not_found_error() { let request = json!({ diff --git a/crates/mcp/src/main.rs b/crates/mcp/src/main.rs index e1b68dd..31e75d9 100644 --- a/crates/mcp/src/main.rs +++ b/crates/mcp/src/main.rs @@ -23,18 +23,22 @@ async fn main() -> anyhow::Result<()> { continue; } - let raw: serde_json::Value = match serde_json::from_str(trimmed) { - Ok(value) => value, + let response = match serde_json::from_str::(trimmed) { + Ok(raw) => match kon_mcp::handle_message(&pool, raw).await { + Some(response) => response, + None => continue, // notification — no reply + }, Err(err) => { - eprintln!("[kon-mcp] ignoring malformed line: {err}"); - continue; + // Per JSON-RPC 2.0 §5.1: a Parse Error responds with + // code -32700 and id null. Previously this branch + // logged and continued, dropping the response — + // clients saw silence instead of a structured error + // (2026-04-22 review MAJOR). + eprintln!("[kon-mcp] parse error: {err}"); + kon_mcp::parse_error_response(&err.to_string()) } }; - let Some(response) = kon_mcp::handle_message(&pool, raw).await else { - continue; // notification — no reply - }; - let payload = serde_json::to_string(&response)?; stdout.write_all(payload.as_bytes()).await?; stdout.write_all(b"\n").await?;