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:
commit
a7e1de76ac
24 files changed
+3817
No files matched your search
@@ -0,0 +1,93 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user