Files
mcp-to-cli/README.md
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

2.3 KiB

mcp-to-cli

MCP servers advertise their tools as (name, description, JSON Schema) triples. mcp-to-cli turns each CLI-safe tool into a clap subcommand whose flags come from the schema's top-level properties, and turns parsed argv back into the {property: value} object the server expects.

It is pure mapping: no I/O, no processes, no MCP protocol. clap, serde_json, and thiserror are the whole dependency list.

Use

use mcp_to_cli::{Payload, ToolInfo, args, command};

fn main() {
    let catalog = [ToolInfo {
        name: "create_pull_request".into(),
        description: "Create a new pull request".into(),
        input_schema: serde_json::json!({
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "draft": {"type": "boolean", "default": false}
            },
            "required": ["title"]
        }),
    }];

    let cmd = command("github", &catalog);
    let matches = cmd
        .try_get_matches_from(["github", "create_pull_request", "--title", "Ship it"])
        .expect("argv parses");

    match args(&cmd, &matches).expect("argv maps") {
        Payload::Mapped(value) => println!("{value}"),  // {"title":"Ship it"}
        Payload::Overridden(raw) => println!("{raw}"),  // the --json string, verbatim
        Payload::Schema { tool } => println!("print the schema of {tool}"),
        _ => unreachable!(),
    }
}

The mapping is conservative, with an escape hatch at every level of schema complexity:

  • enums become clap possible_values;
  • scalar arrays become repeatable flags;
  • every shape the flag form cannot express collapses to a --<property>_json flag;
  • --json carries the whole argument object verbatim, for scripts;
  • schema defaults are never pre-filled: an omitted flag omits the key, and the server stays the validator.

Discovery rides along: a bare invocation answers with the tool list (list_json), and --schema asks for one tool's input_schema instead of calling it (Payload::Schema).

Examples

cargo run --example github      # the real GitHub create_pull_request schema
cargo run --example blender     # a Blender MCP server's tools
cargo run --example catalog     # every tool of a JSON catalog

cargo test drives the mapping matrix against vendored real schemas.