Skip to content

Commit 5eb4b8a

Browse files
authored
fix(mcp): return typed error JSON for unsupported actions (info/describe/list-filter) (ultraworkers#2989)
`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 <filter>`: list accepts no filter argument - `info <name>` / `describe <name>`: 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 ultraworkers#504
1 parent 65aa559 commit 5eb4b8a

1 file changed

Lines changed: 78 additions & 0 deletions

File tree

  • rust/crates/commands/src

rust/crates/commands/src/lib.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2674,10 +2674,44 @@ fn render_mcp_report_for(
26742674
)),
26752675
}
26762676
}
2677+
Some(args) if args.split_whitespace().next() == Some("list") && args.contains(' ') => {
2678+
// `mcp list <filter>` — list does not accept arguments; treat as unsupported action.
2679+
Ok(render_mcp_unsupported_action_text(
2680+
args,
2681+
"list accepts no filter argument; use `claw mcp list`",
2682+
))
2683+
}
2684+
Some(args) if matches!(args.split_whitespace().next(), Some("info" | "describe")) => {
2685+
Ok(render_mcp_unsupported_action_text(
2686+
args,
2687+
"use `claw mcp show <server>` to inspect a server",
2688+
))
2689+
}
26772690
Some(args) => Ok(render_mcp_usage(Some(args))),
26782691
}
26792692
}
26802693

2694+
fn render_mcp_unsupported_action_text(action: &str, hint: &str) -> String {
2695+
format!(
2696+
"MCP\n Error unsupported action '{action}'\n Hint {hint}\n Usage /mcp [list|show <server>|help]"
2697+
)
2698+
}
2699+
2700+
fn render_mcp_unsupported_action_json(action: &str, hint: &str) -> Value {
2701+
json!({
2702+
"kind": "mcp",
2703+
"action": "error",
2704+
"ok": false,
2705+
"error_kind": "unsupported_action",
2706+
"requested_action": action,
2707+
"hint": hint,
2708+
"usage": {
2709+
"slash_command": "/mcp [list|show <server>|help]",
2710+
"direct_cli": "claw mcp [list|show <server>|help]",
2711+
},
2712+
})
2713+
}
2714+
26812715
fn render_mcp_report_json_for(
26822716
loader: &ConfigLoader,
26832717
cwd: &Path,
@@ -2758,6 +2792,18 @@ fn render_mcp_report_json_for(
27582792
})),
27592793
}
27602794
}
2795+
Some(args) if args.split_whitespace().next() == Some("list") && args.contains(' ') => {
2796+
Ok(render_mcp_unsupported_action_json(
2797+
args,
2798+
"list accepts no filter argument; use `claw mcp list`",
2799+
))
2800+
}
2801+
Some(args) if matches!(args.split_whitespace().next(), Some("info" | "describe")) => {
2802+
Ok(render_mcp_unsupported_action_json(
2803+
args,
2804+
"use `claw mcp show <server>` to inspect a server",
2805+
))
2806+
}
27612807
Some(args) => Ok(render_mcp_usage_json(Some(args))),
27622808
}
27632809
}
@@ -4745,6 +4791,38 @@ mod tests {
47454791
);
47464792
}
47474793

4794+
#[test]
4795+
fn mcp_unsupported_actions_return_typed_error_not_generic_help() {
4796+
// `mcp info <name>` and `mcp list <filter>` must return typed errors, not raw help.
4797+
// Regression for #504: these previously fell through to render_mcp_usage with
4798+
// unexpected=arg, giving no machine-readable error_kind.
4799+
use crate::handle_mcp_slash_command_json;
4800+
use std::path::PathBuf;
4801+
let cwd = PathBuf::from("/tmp");
4802+
4803+
let info_json = handle_mcp_slash_command_json(Some("info nonexistent"), &cwd)
4804+
.expect("info nonexistent should not error at IO level");
4805+
assert_eq!(info_json["kind"], "mcp");
4806+
assert_eq!(info_json["ok"], false);
4807+
assert_eq!(info_json["error_kind"], "unsupported_action");
4808+
assert!(info_json["hint"]
4809+
.as_str()
4810+
.unwrap_or_default()
4811+
.contains("show"));
4812+
4813+
let list_filter_json = handle_mcp_slash_command_json(Some("list nonexistent"), &cwd)
4814+
.expect("list nonexistent should not error at IO level");
4815+
assert_eq!(list_filter_json["kind"], "mcp");
4816+
assert_eq!(list_filter_json["ok"], false);
4817+
assert_eq!(list_filter_json["error_kind"], "unsupported_action");
4818+
4819+
let describe_json = handle_mcp_slash_command_json(Some("describe myserver"), &cwd)
4820+
.expect("describe myserver should not error at IO level");
4821+
assert_eq!(describe_json["kind"], "mcp");
4822+
assert_eq!(describe_json["ok"], false);
4823+
assert_eq!(describe_json["error_kind"], "unsupported_action");
4824+
}
4825+
47484826
#[test]
47494827
fn rejects_invalid_mcp_arguments() {
47504828
let show_error = parse_error_message("/mcp show alpha beta");

0 commit comments

Comments
 (0)