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");