brontes
brontes (Greek: thunder). In myth, the Cyclops smith who forged Zeus's thunderbolts. This crate will forge clap CLIs into MCP servers.
A Rust library for transforming clap CLIs into MCP servers, inspired by njayp/ophis.
Written by Claude (Opus 4.6 / 4.7); maintained by us.
Status: Beta — used in production by anodizer + cfgd; APIs stabilizing toward 1.0.
Why brontes
- Ship existing CLIs to AI agents in two lines. Mount
brontes::commandand dispatch withbrontes::handle; every clap subcommand becomes an MCP tool, instantly usable from Claude Desktop, Cursor, VSCode, and Zed. - Token-efficient by design. Per-command description overrides,
Short/Longmode toggle, deprecation filter, andafter_help"Examples:" promotion let you trim the description surface the LLM has to read. - Production-ready security defaults. Streamable HTTP transport is loopback-only by default via rmcp's DNS-rebind allow-list; widen it explicitly with
--allow-host. Auth is not built in — wire it throughMiddleware. - Async middleware boundary. Wrap tool execution with auth, audit logging, rate limiting, or distributed tracing without forking the runtime.
How it works
clap::Command tree
│
▼
┌───────────────────┐ ┌──────────────────────┐
│ brontes walker │ ──────▶ │ Vec<rmcp::Tool> │
│ (selectors, │ │ (one tool per │
│ annotations, │ │ reachable command) │
│ descriptions) │ └──────────────────────┘
└───────────────────┘ │
▼
┌──────────────────────────┐
│ MCP server runtime │
│ • stdio (mcp start) │
│ • HTTP (mcp stream) │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ Editor configs │
│ • Claude Desktop │
│ • Cursor (user + ws) │
│ • VSCode (user + ws) │
│ • Zed (user + ws) │
└──────────────────────────┘
The walker recursively visits every clap::Command, applies safety filters (deprecated commands, selector predicates), and turns each leaf into an rmcp::model::Tool with a JSON-Schema-typed input map derived from the command's flags. Layered on at this stage:
- Annotations
- Per-command description overrides
- Per-flag schema overrides
The runtime wraps that tool list in an rmcp server and serves it over either stdin/stdout (for editor-launched processes) or streamable HTTP (for sidecar deployments). Tool invocations re-enter your binary as ordinary clap argv, so the same code path serves humans and agents.
Quick start
Two lines mount and dispatch the mcp subtree on any existing clap CLI:
use Command;
async
For tiny CLIs whose only purpose is the MCP server, collapse the ceremony
into one line with brontes::run:
use Command;
async
Editor integration
brontes ships built-in commands to register the resulting MCP server in the three major AI-aware editors. Each manager writes a JSON config file in the editor's standard location, snapshots the existing file to <base>.backup.json before any in-place mutation, and exposes enable / disable / list leaves for symmetric lifecycle control.
# Register the server in Claude Desktop
()
# Register in Cursor (user mode, ~/.cursor/mcp.json)
# Register in Cursor (per-workspace, lives in $CWD/.cursor/mcp.json)
# Register in VSCode (user mode)
# Register in Zed (user mode, ~/.config/zed/settings.json on macOS/Linux,
# %APPDATA%\Zed\settings.json on Windows; preserves theme/font/keymap and
# any other unrelated top-level keys in settings.json on round-trip).
# Per-workspace Zed config (lives in $CWD/.zed/settings.json).
# List the configured servers for a given editor
# Remove the brontes-managed entry
Shared flags on every enable:
| Flag | Purpose |
|---|---|
--config-path <PATH> |
Override the per-editor default config location |
--server-name <NAME> |
Override the MCP server key written into the config (defaults to the binary name) |
--env KEY=VAL (-e, repeatable) |
Append environment variables the editor will inject when launching the server |
--log-level <LEVEL> |
Set the server's tracing level (trace/debug/info/warn/error) |
--workspace is additionally accepted on cursor, vscode, AND zed's
enable, disable, and list leaves — pass it whenever you want the
workspace-mode config ($CWD/.cursor/mcp.json, $CWD/.vscode/mcp.json,
or $CWD/.zed/settings.json) instead of the per-OS user config.
Zed differs structurally from the other three editors: its settings.json
also carries the user's theme, font, keymap, and other editor settings.
brontes parses the file as JSONC (line comments and trailing commas are
tolerated on load), writes back strict JSON, and preserves every
non-context_servers top-level key verbatim. The first write strips
JSONC comments — same trade-off the upstream Zed CLI accepts when it
rewrites the file.
Backups are only written when an existing file is mutated — first writes don't litter .backup.json files. See SECURITY.md for the editor-config threat surface.
Features
- Stdio MCP server —
mcp startruns an stdio server fronting your clap CLI; the launch transport every editor manager wires up. - Streamable HTTP MCP server —
mcp streamexposes the same tool list over HTTP via rmcp 1.6; loopback-only by default, widen with--allow-host. - Editor managers for Claude Desktop, Cursor, VSCode, and Zed — each with
enable/disable/listleaves,--workspaceper-project mode where applicable, and snapshot-before-write backups. Zed preserves unrelatedsettings.jsonkeys (theme, font, keymap) and tolerates JSONC on load. - Async middleware —
Middlewarewraps tool execution for auth, audit logging, rate limiting, or distributed tracing without forking the runtime. - Default env injection —
Config::default_env(key, val)ships env vars with every tool launch; per-callenvfrom the MCP client wins on key conflict. - Tool-name prefix —
Config::tool_name_prefixreplaces the root command name in every generated tool name, avoiding cross-CLI collisions on the same MCP client. - Configurable MCP group name —
Config::command_namerenames themcpsubtree if your CLI already has anmcpsubcommand. - Per-flag JSON Schema override —
Config::flag_schema(cmd_path, flag, schema)swaps the auto-derived schema for one flag wholesale. - Per-flag type override —
Config::flag_type_overridegives a coarse type hint when a flag'svalue_parseris opaque to type-ID introspection. - Per-command annotations — read-only / destructive / idempotent / open-world hints via
Config::annotation(path, ToolAnnotations). Examples:fromafter_help— clap'safter_helpblock is appended to the MCP tool description, so--helpexamples reach LLM clients unchanged.- Per-command description override —
Config::description(path, text)replaces the entire description with LLM-targeted prompt text, bypassing thelong_about/about/after_helpcascade. - Per-command description-mode toggle —
Config::description_mode_for(path, mode)overrides the globalShort/Longdefault on a single command. - Deprecation filter —
Config::deprecate(path)hides commands from agents while keeping them visible to humans on the CLI. - Default description fallback — every tool gets a sensible
"Execute the {name} command"description when clap'sabout/long_aboutare both absent, so the MCP tool list is never silently empty-descriptioned. - MCP
Implementationidentity —Config::implementationsets the server name/version surfaced to MCP clients and the MCP registry; falls through toCARGO_PKG_*if unset.
Advanced
Middleware — auth, audit, tracing
A Middleware is an Arc'd async closure attached to a Selector that wraps tool execution. Use it to enforce auth, emit audit records, rate-limit, or attach distributed-tracing spans around every dispatched tool call.
use Arc;
use ;
use Command;
use tracing;
async
MiddlewareCtx carries the cancellation token, tool name, and deserialized ToolInput. BoxedNext is a one-shot FnOnce; call next(ctx).await exactly once to delegate to the wrapped exec step.
Per-command description configuration
Three knobs control what text becomes the MCP tool description. Default is DescriptionMode::Long (prefer clap's long_about, fall back to about).
use ;
let cfg = default
// 1) Flip the global default to the short field.
.description_mode
// 2) Restore long-form for one command that needs the verbose blurb.
.description_mode_for
// 3) Replace the entire description with LLM-targeted prompt text.
// Bypasses the long_about / about / after_help cascade entirely.
.description;
The literal description override is not appended to by the after_help "Examples:" block — you control the exact bytes sent to the MCP client. Empty / whitespace-only override text is rejected at generate_tools time as Error::Config. Closes the njayp/ophis#6 gap.
Default env injection
Inject environment variables into every tool invocation. Per-call env from the MCP client wins on key conflict.
use Config;
let cfg = default
.default_env
.default_env;
When both maps are empty the env key is omitted from the MCP wire payload entirely.
Per-flag schema and type overrides
flag_schema replaces the auto-derived JSON Schema for one flag wholesale (auto default/required/enum extraction is skipped). flag_type_override provides a coarse type hint for flags whose value_parser is opaque to brontes's type-ID introspection.
use ;
let cfg = default
// Wholesale schema replacement.
.flag_schema
// Coarse type hint when value_parser is a custom function.
.flag_type_override;
Server identity (registry-ready)
Set the MCP Implementation (server name and version) surfaced to MCP clients. Required when your binary name differs from the desired MCP server identity, or when publishing to the MCP registry — see the Releasing section below.
use Config;
use Implementation;
let cfg = default
.implementation;
If unset, brontes falls through to Implementation::default(), which derives name/version from CARGO_PKG_NAME / CARGO_PKG_VERSION.
Tool-name prefix and group name
tool_name_prefix replaces the root command name when constructing each MCP tool's name — useful when multiple brontes-mounted CLIs attach to the same MCP client and you want to avoid collisions. command_name renames the mcp subcommand group on the user's CLI — useful when your CLI already has an mcp subcommand.
use Config;
let cfg = default
.tool_name_prefix // tools become "agent_list", "agent_delete", etc.
.command_name; // the brontes subtree mounts as `my-cli agent ...`
Deprecation
Mark a command path as deprecated to filter it out of the generated tool list — the command still exists for humans on the CLI, but agents won't see it.
use Config;
let cfg = default.deprecate;
This is brontes-only — ophis has no equivalent.
Streamable HTTP — DNS-rebind allow-list
mcp stream exposes the MCP server over HTTP. rmcp's DNS-rebind guard defaults to allowing only localhost, 127.0.0.1, and ::1 in the Host: header; requests from any other hostname get a silent 403. To widen the allow-list for LAN or public exposure, pass --allow-host once per reachable hostname:
mcp stream flags:
| Flag | Default | Notes |
|---|---|---|
--host <HOST> |
0.0.0.0 (bind-all) |
Bind address |
--port <PORT> |
8080 |
TCP port |
--log-level <LEVEL> |
info |
trace / debug / info / warn / error |
--allow-host <HOST> |
(none) | Append to rmcp's DNS-rebind allow-list (repeatable) |
See SECURITY.md for the full HTTP-transport threat model.
API reference
brontes::command(cfg)/brontes::handle(matches, cli, cfg)/brontes::run(cli, cfg)— mount, dispatch, and one-shot runners for themcpsubtree (mcp startfor stdio,mcp streamfor streamable HTTP,mcp toolsto export the tool list,mcp claude {enable,disable,list},mcp cursor {enable,disable,list},mcp vscode {enable,disable,list}).generate_tools(root, cfg) -> Result<Vec<rmcp::model::Tool>>— offline tool-list builder for consumers that wire their own server.brontes::Config— fluent builder for tool-name prefix, selectors, default env, annotations, deprecated commands, per-flag schema/type overrides, log level, MCPImplementationidentity, and per-command description configuration.brontes::DescriptionMode—Short(preferabout) orLong(preferlong_about); default isLong.brontes::Selector+brontes::selectors::{allow_cmds, exclude_cmds, allow_cmds_containing, exclude_cmds_containing, allow_flags, exclude_flags, no_flags}— built-in matcher factories.brontes::Middleware/brontes::MiddlewareCtx/brontes::MiddlewareResult/brontes::BoxedNext— async wrap around tool execution.brontes::ToolAnnotations— typed mirror of rmcp's annotation surface.brontes::ToolInput/brontes::ToolOutput— the MCP tool-call payload shapes.brontes::SchemaType— coarse type classifier for per-flag overrides.brontes::Error/brontes::Result— error surface.
Releasing an MCP server built with brontes
anodizer is the
recommended release tool for brontes-powered CLIs. It is a single
Rust binary that ships your MCP server end-to-end in one
anodizer release invocation:
- Multi-platform binaries — cross-compiled archives for every Tier-1 target (Linux / macOS / Windows × x86_64 / aarch64).
- Crates.io publish —
cargo publishwith deterministic builds and immutable-version safety rails. - GitHub Releases — auto-generated changelog (Conventional Commits), uploaded archives, checksums, and signatures.
- MCP registry — direct publish to
registry.modelcontextprotocol.io
via the
mcp:block (server name, package shape, transport, auth). - Cosign signatures — keyless OIDC-signed artifacts.
- Auto-tagging — Conventional Commits drive the semver bump;
CI mints the tag, no manual
git tagstep.
brontes' own .anodizer.yaml is the worked
reference: it carries an annotated mcp: block showing every
registry-publish field, commented out because brontes itself is a
library. Copy the block into your own consumer's .anodizer.yaml,
uncomment, and fill in your values — that is the full release
pipeline.
Contributing
See CONTRIBUTING.md for development setup, the local CI workflow, MSRV policy, and pull-request expectations.
License
MIT. See LICENSE.