Files
mcp-to-cli/examples/catalog.rs
T
vhaudiquet a7e1de76ac
ci / checks (push) Failing after 14s
Import mcp-to-cli from the rhema workspace
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.
2026-11-12 12:20:25 +01:00

94 lines
3.3 KiB
Rust

//! A stress harness for the mapping: build one command tree from a
//! directory of real MCP tool definitions (wire-format JSON objects
//! with `name`, `description`, `inputSchema` — e.g. github-mcp-server's
//! `__toolsnaps__`), then behave like the other examples. Prints a
//! render summary to stderr. Build once, drive it like a CLI:
//!
//! ```sh
//! cargo build -p mcp-to-cli --example catalog
//! target/debug/examples/catalog github /path/to/__toolsnaps__
//! target/debug/examples/catalog github /path/to/__toolsnaps__ list
//! target/debug/examples/catalog github /path/to/__toolsnaps__ create_issue --help
//! target/debug/examples/catalog github /path/to/__toolsnaps__ create_issue --owner acme --repo api --title hi
//! ```
use mcp_to_cli::{Payload, ToolInfo, args, command, list_json};
fn main() {
let mut argv = std::env::args().skip(1);
let name = argv.next().unwrap_or_else(|| {
eprintln!("usage: catalog <server-name> <dir-of-tool-json> [argv]");
std::process::exit(2);
});
let dir = argv.next().unwrap_or_else(|| {
eprintln!("usage: catalog <server-name> <dir-of-tool-json> [argv]");
std::process::exit(2);
});
let argv: Vec<String> = argv.collect();
let mut paths: Vec<_> = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("{dir}: {e}"))
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|e| e == "json" || e == "snap"))
.collect();
paths.sort();
let catalog: Vec<ToolInfo> = paths
.iter()
.map(|p| {
let raw: serde_json::Value = std::fs::read_to_string(p)
.expect("fixture reads")
.parse()
.expect("tool JSON parses");
let field = |k: &str| {
raw.get(k)
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_owned()
};
ToolInfo {
name: field("name"),
description: field("description"),
input_schema: raw.get("inputSchema").cloned().unwrap_or_default(),
}
})
.collect();
let rendered = command(&name, &catalog)
.get_subcommands()
.filter(|c| c.get_name() != "help")
.count();
eprintln!(
"{} tools loaded, {} rendered as subcommands",
catalog.len(),
rendered,
);
let mut cmd = command(&name, &catalog);
if argv.as_slice() == ["list"] {
println!("{}", list_json(&catalog));
return;
}
if argv.is_empty() {
cmd.print_help().expect("help renders to stdout");
return;
}
let mut full = vec![name.as_str()];
full.extend(argv.iter().map(String::as_str));
let matches = cmd
.clone()
.try_get_matches_from(full)
.unwrap_or_else(|e| e.exit());
match args(&cmd, &matches) {
Ok(Payload::Mapped(v)) => {
println!(
"{}",
serde_json::to_string_pretty(&v).expect("values render")
)
}
Ok(Payload::Overridden(raw)) => println!("--json override, verbatim: {raw}"),
Ok(_) => unreachable!("payload variants are the two above"),
Err(e) => {
eprintln!("{e}");
std::process::exit(2);
}
}
}