diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index a8fd88d5..d148f241 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -3027,7 +3027,19 @@ fn render_mcp_report_json_for( } } Some(args) if is_help_arg(args) => Ok(render_mcp_usage_json(None)), - Some("show") => Ok(render_mcp_usage_json(Some("show"))), + // #830: `claw mcp show` with no server name is a missing required + // argument, not an unknown action. Emit a dedicated error_kind so + // machine consumers can distinguish "I know show, but need a name" + // from "I don't know this action". + Some("show") => Ok(serde_json::json!({ + "kind": "mcp", + "action": "show", + "status": "error", + "ok": false, + "error_kind": "missing_argument", + "hint": "Usage: claw mcp show \nRun `claw mcp list` to see available servers.", + "message": "missing required argument: mcp show requires a server name.", + })), Some(args) if args.split_whitespace().next() == Some("show") => { let mut parts = args.split_whitespace(); let _ = parts.next(); diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 5febf841..874408de 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -271,7 +271,9 @@ Run `claw --help` for usage." /// matching against the error messages produced throughout the CLI surface. fn classify_error_kind(message: &str) -> &'static str { // Check specific patterns first (more specific before generic) - if message.starts_with("unknown_slash_command:") { + if message.starts_with("missing_argument:") || message.starts_with("missing required argument:") { + "missing_argument" + } else if message.starts_with("unknown_slash_command:") { "unknown_slash_command" } else if message.starts_with("command_not_found:") { "command_not_found" diff --git a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs index 240884bf..a1f2e411 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -4054,3 +4054,33 @@ fn resume_safe_interactive_only_hint_includes_resume_suggestion() { "/diff hint must suggest --resume (it is resume-safe) (#829): hint={hint:?}" ); } + +// #830: claw mcp show (missing server name) must emit missing_argument, not unknown_mcp_action +#[test] +fn mcp_show_missing_server_name_emits_missing_argument() { + let root = unique_temp_dir("mcp-show-missing-830"); + std::fs::create_dir_all(&root).expect("create temp dir"); + let output = run_claw(&root, &["--output-format", "json", "mcp", "show"], &[]); + assert_eq!(output.status.code(), Some(1), "mcp show (no name) should exit 1"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let j: serde_json::Value = + serde_json::from_str(stdout.trim()).expect("mcp show (no name) must emit JSON (#830)"); + assert_eq!( + j["error_kind"], "missing_argument", + "mcp show (no name) must emit missing_argument, not unknown_mcp_action (#830): {j}" + ); + assert_ne!( + j["error_kind"], "unknown_mcp_action", + "mcp show (no name) must not emit unknown_mcp_action (#830): {j}" + ); + let hint = j["hint"].as_str().unwrap_or(""); + assert!( + hint.contains("claw mcp show") || hint.contains("mcp list"), + "mcp show (no name) hint should mention usage (#830): {hint:?}" + ); + assert!( + stderr.is_empty(), + "mcp show (no name) JSON must have empty stderr (#830): {stderr:?}" + ); +}