fix(cr-2026-04-22): MCP stdio replies with parse-error on malformed JSON
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.
This commit is contained in:
@@ -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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[tokio::test]
|
||||||
async fn unknown_method_returns_method_not_found_error() {
|
async fn unknown_method_returns_method_not_found_error() {
|
||||||
let request = json!({
|
let request = json!({
|
||||||
|
|||||||
@@ -23,18 +23,22 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let raw: serde_json::Value = match serde_json::from_str(trimmed) {
|
let response = match serde_json::from_str::<serde_json::Value>(trimmed) {
|
||||||
Ok(value) => value,
|
Ok(raw) => match kon_mcp::handle_message(&pool, raw).await {
|
||||||
|
Some(response) => response,
|
||||||
|
None => continue, // notification — no reply
|
||||||
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("[kon-mcp] ignoring malformed line: {err}");
|
// Per JSON-RPC 2.0 §5.1: a Parse Error responds with
|
||||||
continue;
|
// 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)?;
|
let payload = serde_json::to_string(&response)?;
|
||||||
stdout.write_all(payload.as_bytes()).await?;
|
stdout.write_all(payload.as_bytes()).await?;
|
||||||
stdout.write_all(b"\n").await?;
|
stdout.write_all(b"\n").await?;
|
||||||
|
|||||||
Reference in New Issue
Block a user