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.
94 lines
3.3 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|