Import mcp-to-cli from the rhema workspace
ci / checks (push) Failing after 14s

The crate maps MCP tool catalogs to clap commands and back, and has no
rhema dependencies — it was in-tree only because the workspace glob
reached it. Its own repository gives it its own release cadence and
its own crates.io token, scoped to this crate alone.
This commit is contained in:
vhaudiquet committed 2026-11-12 12:20:25 +01:00
commit a7e1de76ac
24 files changed
+3817

No files matched your search

+289
View File
@@ -0,0 +1,289 @@
//! Tool selection: which tools become subcommands, the bare
//! invocation list, and the errors `args` reports.
mod common;
use common::{mapped, overridden, parse, run, tool};
use mcp_to_cli::{MappingError, args, cli_safe, command, list_json};
use serde_json::json;
#[test]
fn cli_safe_accepts_argv_clean_names() {
for name in [
"create_issue",
"a",
"A9",
"with-dash",
"with_underscore",
"9",
"help",
] {
assert!(cli_safe(name), "{name} should be safe");
}
}
#[test]
fn cli_safe_rejects_everything_else() {
for name in [
"",
"has space",
"dot.name",
"sla/sh",
"uni\u{e7}ode",
"star*",
"tab\tname",
"query?",
] {
assert!(!cli_safe(name), "{name} should not be safe");
}
}
#[test]
fn unsafe_named_tools_are_omitted() {
let cmd = command(
"srv",
&[
tool(
"safe",
"safe tool",
json!({"type": "object", "properties": {}}),
),
tool(
"bad name",
"unreachable",
json!({"type": "object", "properties": {}}),
),
],
);
assert!(cmd.find_subcommand("safe").is_some());
assert!(cmd.find_subcommand("bad name").is_none());
}
#[test]
fn help_named_tools_are_skipped() {
let cmd = command(
"srv",
&[
tool(
"help",
"clap owns this name",
json!({"type": "object", "properties": {}}),
),
tool("other", "fine", json!({"type": "object", "properties": {}})),
],
);
// cli_safe("help") is true — the skip is the command's, not the
// charset's.
assert!(cli_safe("help"));
assert!(cmd.find_subcommand("help").is_none());
assert!(cmd.find_subcommand("other").is_some());
}
#[test]
fn non_object_roots_are_omitted() {
let cmd = command(
"srv",
&[
tool(
"arr",
"array root",
json!({"type": "array", "items": {"type": "string"}}),
),
tool("str", "string root", json!({"type": "string"})),
tool("null_root", "null root", serde_json::Value::Null),
tool(
"object",
"object root",
json!({"type": "object", "properties": {}}),
),
],
);
assert!(cmd.find_subcommand("arr").is_none());
assert!(cmd.find_subcommand("str").is_none());
assert!(cmd.find_subcommand("null_root").is_none());
assert!(cmd.find_subcommand("object").is_some());
}
#[test]
fn duplicate_tool_names_keep_the_first() {
let cmd = command(
"srv",
&[
tool(
"dup",
"first wins",
json!({"type": "object", "properties": {}}),
),
tool(
"dup",
"second ignored",
json!({"type": "object", "properties": {}}),
),
],
);
let sc = cmd.find_subcommand("dup").expect("one subcommand");
assert_eq!(
sc.get_about().map(|a| a.to_string()),
Some("first wins".to_owned())
);
}
#[test]
fn subcommands_follow_catalog_order() {
let empty = json!({"type": "object", "properties": {}});
let cmd = command(
"srv",
&[
tool("bravo", "b", empty.clone()),
tool("alpha", "a", empty.clone()),
tool("charlie", "c", empty),
],
);
let names: Vec<&str> = cmd.get_subcommands().map(|sc| sc.get_name()).collect();
assert_eq!(names, ["bravo", "alpha", "charlie"]);
}
#[test]
fn descriptions_flow_into_about_and_flag_help_verbatim() {
let about = "List things, possibly at length, always helpfully";
let flag_help = "Repository owner (username or organization)";
let cmd = command(
"srv",
&[tool(
"t",
about,
json!({"type": "object", "properties": {"owner": {"type": "string", "description": flag_help}}}),
)],
);
let sc = cmd.find_subcommand("t").expect("tool subcommand");
assert_eq!(
sc.get_about().map(|a| a.to_string()),
Some(about.to_owned())
);
let arg = sc
.get_arguments()
.find(|a| a.get_long() == Some("owner"))
.expect("owner flag");
assert_eq!(
arg.get_help().map(|h| h.to_string()),
Some(flag_help.to_owned())
);
}
#[test]
fn bare_invocation_matches_and_args_reports_it() {
let cmd = command(
"srv",
&[tool("t", "d", json!({"type": "object", "properties": {}}))],
);
let matches = parse(&cmd, &[]).expect("bare invocation parses");
assert!(matches.subcommand().is_none());
assert_eq!(
args(&cmd, &matches).unwrap_err(),
MappingError::NoSubcommand
);
}
#[test]
fn unknown_tool_subcommand_is_claps_error() {
let cmd = command(
"srv",
&[tool("t", "d", json!({"type": "object", "properties": {}}))],
);
assert!(parse(&cmd, &["nope"]).is_none());
}
#[test]
fn matches_outside_the_command_report_unknown_subcommand() {
let with_a = command(
"srv",
&[tool("a", "d", json!({"type": "object", "properties": {}}))],
);
let empty = command("srv", &[]);
let matches = parse(&with_a, &["a"]).expect("parses");
assert_eq!(
args(&empty, &matches).unwrap_err(),
MappingError::UnknownSubcommand {
name: "a".to_owned(),
}
);
}
#[test]
fn list_json_is_name_and_description_pairs() {
let value = list_json(&[
tool("create_issue", "Create an issue", json!({"type": "object"})),
tool(
"get_me",
"Get the authenticated user",
json!({"type": "object"}),
),
]);
assert_eq!(
value,
json!([
{"name": "create_issue", "description": "Create an issue"},
{"name": "get_me", "description": "Get the authenticated user"},
])
);
}
#[test]
fn list_json_of_an_empty_catalog_is_an_empty_array() {
assert_eq!(list_json(&[]), json!([]));
}
#[test]
fn list_json_mirrors_the_rendered_tree() {
let catalog = [
tool("safe", "kept", json!({"type": "object", "properties": {}})),
tool(
"bad name",
"dropped",
json!({"type": "object", "properties": {}}),
),
tool(
"help",
"dropped",
json!({"type": "object", "properties": {}}),
),
tool("str_root", "dropped", json!({"type": "string"})),
tool(
"safe",
"duplicate dropped",
json!({"type": "object", "properties": {}}),
),
];
let cmd = command("srv", &catalog);
let tree: Vec<serde_json::Value> = cmd
.get_subcommands()
.map(|sc| {
json!({
"name": sc.get_name(),
"description": sc.get_about().map(|a| a.to_string()).unwrap_or_default(),
})
})
.collect();
assert_eq!(list_json(&catalog), serde_json::Value::Array(tree));
assert_eq!(
list_json(&catalog),
json!([{"name": "safe", "description": "kept"}])
);
}
#[test]
fn mapped_and_overridden_cover_the_whole_surface() {
let cmd = command(
"srv",
&[tool(
"t",
"d",
json!({"type": "object", "properties": {"owner": {"type": "string"}}}),
)],
);
assert_eq!(overridden(run(&cmd, &["t", "--json", "{}"]).unwrap()), "{}");
assert_eq!(
mapped(run(&cmd, &["t", "--owner", "o"]).unwrap()),
json!({"owner": "o"})
);
}