diff --git a/crates/mcp/src/lib.rs b/crates/mcp/src/lib.rs index e82b65c..66a8d26 100644 --- a/crates/mcp/src/lib.rs +++ b/crates/mcp/src/lib.rs @@ -191,7 +191,13 @@ async fn list_transcripts_tool(pool: &SqlitePool, args: Value) -> Result, } - let args: Args = serde_json::from_value(args).unwrap_or_default(); + // Every other tool handler returns -32602 on malformed args; + // previously this one silently fell back to defaults, hiding + // client bugs (2026-04-22 review MAJOR). An empty Value + // deserialises cleanly into Args::default, so the "no args" + // path still works — only genuinely malformed payloads error. + let args: Args = serde_json::from_value(args) + .map_err(|e| error(-32602, format!("Invalid arguments: {e}")))?; let limit = args.limit.unwrap_or(20).clamp(1, 200); let rows = kon_storage::list_transcripts(pool, limit) @@ -420,6 +426,32 @@ mod tests { assert!(err.message.contains("expected value")); } + #[tokio::test] + async fn list_transcripts_rejects_malformed_params_with_invalid_arguments() { + // Regression for the 2026-04-22 review MAJOR: previously the + // handler did `from_value(args).unwrap_or_default()`, so + // `{"limit": "not-a-number"}` silently became `limit = 20`. + // Every other handler returns -32602 on shape mismatch; this + // one must now do the same. + let request = json!({ + "jsonrpc": "2.0", + "id": 99, + "method": "tools/call", + "params": { + "name": "list_transcripts", + "arguments": { "limit": "twenty" }, + }, + }); + + let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap(); + let response = handle_message(&pool, request).await.expect("has response"); + + assert!(response.result.is_none()); + let err = response.error.expect("expected error"); + assert_eq!(err.code, -32602, "invalid arguments must surface as -32602"); + assert!(err.message.contains("Invalid arguments")); + } + #[tokio::test] async fn unknown_method_returns_method_not_found_error() { let request = json!({