From 5eb4b8a944d75cd759f08fa7b60d7bf9694a73c4 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 5 May 2026 05:13:07 +0900 Subject: [PATCH] fix(mcp): return typed error JSON for unsupported actions (info/describe/list-filter) (#2989) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `claw mcp info nonexistent --output-format json` and `claw mcp list nonexistent --output-format json` fell through to the generic help renderer, returning an opaque envelope with only `unexpected` set — no machine-readable error_kind. Fix: - Add typed guards in render_mcp_report_for/_json_for for: - `list `: list accepts no filter argument - `info ` / `describe `: suggest `mcp show` - New render_mcp_unsupported_action_text/json helpers emit `ok:false`, `error_kind:"unsupported_action"`, `hint`, `requested_action` - `mcp show`, `mcp list`, `mcp help` existing paths unchanged Test: mcp_unsupported_actions_return_typed_error_not_generic_help asserts kind=="mcp", ok==false, error_kind=="unsupported_action" for info/list-filter/describe paths. Pinpoint: ROADMAP #504 --- rust/crates/commands/src/lib.rs | 78 +++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index 5b153272..454888d7 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -2674,10 +2674,44 @@ fn render_mcp_report_for( )), } } + Some(args) if args.split_whitespace().next() == Some("list") && args.contains(' ') => { + // `mcp list ` — list does not accept arguments; treat as unsupported action. + Ok(render_mcp_unsupported_action_text( + args, + "list accepts no filter argument; use `claw mcp list`", + )) + } + Some(args) if matches!(args.split_whitespace().next(), Some("info" | "describe")) => { + Ok(render_mcp_unsupported_action_text( + args, + "use `claw mcp show ` to inspect a server", + )) + } Some(args) => Ok(render_mcp_usage(Some(args))), } } +fn render_mcp_unsupported_action_text(action: &str, hint: &str) -> String { + format!( + "MCP\n Error unsupported action '{action}'\n Hint {hint}\n Usage /mcp [list|show |help]" + ) +} + +fn render_mcp_unsupported_action_json(action: &str, hint: &str) -> Value { + json!({ + "kind": "mcp", + "action": "error", + "ok": false, + "error_kind": "unsupported_action", + "requested_action": action, + "hint": hint, + "usage": { + "slash_command": "/mcp [list|show |help]", + "direct_cli": "claw mcp [list|show |help]", + }, + }) +} + fn render_mcp_report_json_for( loader: &ConfigLoader, cwd: &Path, @@ -2758,6 +2792,18 @@ fn render_mcp_report_json_for( })), } } + Some(args) if args.split_whitespace().next() == Some("list") && args.contains(' ') => { + Ok(render_mcp_unsupported_action_json( + args, + "list accepts no filter argument; use `claw mcp list`", + )) + } + Some(args) if matches!(args.split_whitespace().next(), Some("info" | "describe")) => { + Ok(render_mcp_unsupported_action_json( + args, + "use `claw mcp show ` to inspect a server", + )) + } Some(args) => Ok(render_mcp_usage_json(Some(args))), } } @@ -4745,6 +4791,38 @@ mod tests { ); } + #[test] + fn mcp_unsupported_actions_return_typed_error_not_generic_help() { + // `mcp info ` and `mcp list ` must return typed errors, not raw help. + // Regression for #504: these previously fell through to render_mcp_usage with + // unexpected=arg, giving no machine-readable error_kind. + use crate::handle_mcp_slash_command_json; + use std::path::PathBuf; + let cwd = PathBuf::from("/tmp"); + + let info_json = handle_mcp_slash_command_json(Some("info nonexistent"), &cwd) + .expect("info nonexistent should not error at IO level"); + assert_eq!(info_json["kind"], "mcp"); + assert_eq!(info_json["ok"], false); + assert_eq!(info_json["error_kind"], "unsupported_action"); + assert!(info_json["hint"] + .as_str() + .unwrap_or_default() + .contains("show")); + + let list_filter_json = handle_mcp_slash_command_json(Some("list nonexistent"), &cwd) + .expect("list nonexistent should not error at IO level"); + assert_eq!(list_filter_json["kind"], "mcp"); + assert_eq!(list_filter_json["ok"], false); + assert_eq!(list_filter_json["error_kind"], "unsupported_action"); + + let describe_json = handle_mcp_slash_command_json(Some("describe myserver"), &cwd) + .expect("describe myserver should not error at IO level"); + assert_eq!(describe_json["kind"], "mcp"); + assert_eq!(describe_json["ok"], false); + assert_eq!(describe_json["error_kind"], "unsupported_action"); + } + #[test] fn rejects_invalid_mcp_arguments() { let show_error = parse_error_message("/mcp show alpha beta");