Argot — an agent-first command interface framework for Rust
Overview
Argot models command-line interfaces as structured command languages, not just argument parsers. The command model is the single source of truth: it drives CLI help output, machine-readable schemas, Markdown documentation, and optional MCP tool exposure — all from the same data.
Argot prioritizes agent usability and discoverability. AI agents can query commands programmatically and receive structured JSON rather than scraping help text. Humans get a familiar, ergonomic CLI with prefix resolution, typo correction, and contextual help.
The design philosophy is: the CLI is one interface to the command model, not the source of truth.
Quick Start
Add argot-cmd to your Cargo.toml:
[]
= "0.1"
Define a command, build a Cli, and call run_env_args_and_exit():
use Arc;
use ;
Core Concepts
Canonical Identity
Every command has a stable canonical name. All resolution, help output, and serialization use this name. Aliases and spellings both resolve to the canonical name, but are semantically distinct.
Aliases
Aliases are advertised alternatives — they appear in --help output and participate in prefix matching:
builder
.alias
.alias
// ...
Spellings
Spellings are silent corrections — typo variants or alternate capitalizations that resolve to the canonical command without being advertised:
builder
.spelling // silent typo correction
.spelling // another silent correction
// ...
Spellings participate in exact matching but not prefix matching. They are never shown in help output.
Semantic Aliases (Intent Discovery)
Semantic aliases are natural-language phrases that describe what a command does. They are used for intent-based discovery via Registry::match_intent() but are not shown in help output and do not participate in normal resolution:
builder
.semantic_alias
.semantic_alias
// ...
Use .semantic_aliases(["...", "..."]) to set multiple at once.
Building Commands
All builder methods consume and return self for chaining. Call .build() at the end; it returns Result<Command, BuildError>.
use Arc;
use ;
let cmd = builder
.alias // shown in help, participates in prefix matching
.alias
.spelling // silent typo correction
.summary // one-line description
.description // prose description
.argument
.flag
.example
.best_practice
.anti_pattern
.subcommand
.handler
.build
.unwrap;
Arbitrary Metadata
Attach structured application-specific metadata with .meta(). Values are serde_json::Value and are included in JSON serialization:
use json;
let cmd = builder
.meta
.meta
.build
.unwrap;
Arguments and Flags
Arguments
Positional arguments are bound in declaration order. Use Argument::builder(name):
use Argument;
// Required positional argument
let env = builder
.description
.required
.build
.unwrap;
// Optional with a default
let format = builder
.description
.default_value
.build
.unwrap;
// Variadic: consumes all remaining tokens (must be last)
let files = builder
.description
.variadic
.build
.unwrap;
Flags
Named flags can be boolean or value-taking. Use Flag::builder(name):
use Flag;
// Boolean flag
let verbose = builder
.short
.description
.build
.unwrap;
// Value-taking flag with a default
let output = builder
.short
.description
.takes_value
.default_value
.build
.unwrap;
// Value-taking flag with an allowed choices list
let format = builder
.takes_value
.choices
.description
.build
.unwrap;
// Required value-taking flag
let token = builder
.takes_value
.required
.description
.build
.unwrap;
// Repeatable boolean flag: -v -v -v stores "3"
let debug = builder
.short
.repeatable
.build
.unwrap;
// Repeatable value-taking flag: --tag a --tag b stores ["a","b"]
let tag = builder
.takes_value
.repeatable
.build
.unwrap;
// Environment variable fallback
let api_key = builder
.takes_value
.env
.build
.unwrap;
Lookup order for a flag: CLI argv → environment variable (if .env() is set) → default value → required error.
Mutually Exclusive Flags
Declare groups of flags where at most one may be provided per invocation:
use ;
let cmd = builder
.flag
.flag
.flag
.exclusive
.build
.unwrap;
Providing more than one flag from the group returns ParseError::MutuallyExclusive { flags }.
The Cli Layer
Cli wires together Registry, Parser, and the render layer. It handles built-in behaviors so application code only needs to build commands and register handlers.
use ;
use Arc;
Built-in Behaviors
| Input | Behavior |
|---|---|
--help / -h |
Print help for the most-specific resolved command; return Ok(()). |
--version / -V |
Print "<app_name> <version>"; return Ok(()). |
| Empty argument list | Print the top-level command listing; return Ok(()). |
| Unrecognized command | Print error and best-effort help to stderr; return Err(CliError::Parse(...)). |
Run Methods
// Read from std::env::args().skip(1)
cli.run_env_args?;
// Read from an explicit iterator (useful in tests)
cli.run?;
// Read from env args and exit the process (recommended for main())
cli.run_env_args_and_exit;
// Explicit args with process exit
cli.run_and_exit;
Agent Discovery
Enable a built-in query command that gives agents structured access to command metadata:
new
.with_query_support
.run_env_args_and_exit;
Agents can then call:
# List all commands as JSON
mytool query commands
# Get structured metadata for a single command
mytool query deploy
# List examples for a command
mytool query examples deploy
# Prefix matching and aliases work too
mytool query dep
When a query is ambiguous (e.g. mytool query dep matches both deploy and describe), agents receive structured JSON rather than an error:
The --json flag is accepted for compatibility but has no effect; all query output is already JSON.
Registry and Search
Registry owns the command tree and provides all lookup operations. Pass registry.commands() to Parser::new to wire it into the parse pipeline.
use ;
let registry = new;
// Exact canonical lookup
let cmd = registry.get_command.unwrap;
// Walk into subcommands by canonical path
let sub = registry.get_subcommand.unwrap;
// Examples for a command
let examples = registry.get_examples.unwrap;
// Case-insensitive substring search across name, summary, description
let results = registry.search;
// Depth-first iteration over all commands (including subcommands)
for entry in registry.iter_all_recursive
// Serialize the entire tree to pretty-printed JSON (handlers excluded)
let json = registry.to_json.unwrap;
// Intent matching: score commands by how many phrase words appear in their
// combined text (canonical, aliases, semantic_aliases, summary, description)
let hits = registry.match_intent;
// Returns Vec<(&Command, u32)> sorted by score descending
CommandEntry carries the full path from the registry root:
entry.name // last segment: "add"
entry.path_str // dotted path: "remote.add"
entry.path // Vec<String>: ["remote", "add"]
Rendering
All render functions return String. None of them print directly; callers write the output wherever appropriate.
Plain-text Help
use render_help;
let help = render_help;
// Sections: NAME, SUMMARY, DESCRIPTION, USAGE, ARGUMENTS, FLAGS,
// SUBCOMMANDS, EXAMPLES, BEST PRACTICES, ANTI-PATTERNS
// Empty sections are omitted.
Compact Command Listing
use render_subcommand_list;
let listing = render_subcommand_list;
// Two-column output: " canonical summary"
Markdown Documentation
use render_markdown;
let md = render_markdown;
// GitHub-flavored Markdown with ## headings, tables for arguments/flags,
// and fenced code blocks for examples.
Full Registry Docs
use render_docs;
let docs = render_docs;
// "# Commands" heading, table of contents with depth-based indentation,
// and per-command Markdown sections separated by "---".
Disambiguation
use render_ambiguity;
let msg = render_ambiguity;
JSON Schema
Generate a JSON Schema (draft-07) suitable for agent tool definitions (OpenAI function calling, Anthropic tool use, MCP):
use render_json_schema;
let schema = render_json_schema.unwrap;
// Arguments become string properties; boolean flags become boolean properties;
// flags with choices get an "enum" constraint.
Shell Completions
Generate tab-completion scripts for bash, zsh, or fish:
use ;
let bash_script = render_completion;
let zsh_script = render_completion;
let fish_script = render_completion;
Source the script in your shell profile to enable tab-completion.
Custom Renderer
Implement the Renderer trait and inject it with Cli::with_renderer:
use ;
;
let cli = new
.with_renderer;
Middleware
Implement Middleware to hook into the parse-and-dispatch lifecycle. Register it with Cli::with_middleware. Multiple middlewares are invoked in registration order.
All methods have default no-op implementations; override only what you need.
use ;
;
let cli = new
.with_middleware;
Async Support
Enable the async feature to register async handlers and use run_async:
[]
= { = "0.1", = ["async"] }
= { = "1", = ["rt-multi-thread", "macros"] }
Dispatch priority when both handlers are registered: async handler takes precedence over the sync handler.
Async convenience methods mirror their sync equivalents:
| Method | Description |
|---|---|
run_async(args) |
Parse and dispatch asynchronously. |
run_env_args_async() |
Same, reading from std::env::args().skip(1). |
run_async_and_exit(args) |
Dispatch and exit the process. |
run_env_args_async_and_exit() |
Same, reading from env args. |
MCP Transport
Enable the mcp feature to expose commands as MCP tools over a stdio JSON-RPC 2.0 transport:
[]
= { = "0.1", = ["mcp"] }
Tool Naming Convention
Commands are exposed as MCP tools using a dash-joined path:
- Top-level
deploy→ tool namedeploy - Subcommand
service rollback→ tool nameservice-rollback - Three levels
service deployment blue-green→ tool nameservice-deployment-blue-green
Supported MCP Methods
| Method | Description |
|---|---|
initialize |
Returns server name, version, and capabilities. |
tools/list |
Lists all commands as MCP tool definitions with JSON Schema input schemas. |
tools/call |
Dispatches a command by tool name, building a ParsedCommand from the JSON arguments. |
Notifications (requests without an id) receive no response per the JSON-RPC 2.0 specification.
Derive Macro
Enable the derive feature to auto-generate Command definitions from struct attributes:
[]
= { = "0.1", = ["derive"] }
Struct-Level Attributes
| Key | Type | Description |
|---|---|---|
canonical = "name" |
string | Override the command name. Default: struct name in kebab-case (DeployApp → deploy-app). |
summary = "text" |
string | One-line summary. |
description = "text" |
string | Long prose description. |
alias = "a" |
string | Add an alias. Repeat the attribute to add more. |
best_practice = "text" |
string | Add a best-practice tip. Repeatable. |
anti_pattern = "text" |
string | Add an anti-pattern warning. Repeatable. |
Field-Level Attributes
Fields without #[argot(...)] are skipped. Annotated fields must include either positional or flag.
| Key | Description |
|---|---|
positional |
Treat as a positional Argument. |
flag |
Treat as a named Flag. |
required |
Mark as required. |
short = 'c' |
Short character for a flag. |
takes_value |
Flag consumes the next token as its value. |
description = "text" |
Human-readable description. |
default = "value" |
Default value string. |
Name conventions: struct names use CamelCase → kebab-case; field names use snake_case → kebab-case (e.g., dry_run → dry-run).
Fuzzy Search
Enable the fuzzy feature to search commands with the skim fuzzy-matching algorithm:
[]
= { = "0.1", = ["fuzzy"] }
Fuzzy search covers the canonical name, summary, and description fields. Commands with no match are excluded from the results.
Feature Flags
| Feature | Description | Default |
|---|---|---|
async |
AsyncHandlerFn, Cli::run_async(), and async-family entry points |
no |
derive |
#[derive(ArgotCommand)] proc-macro from argot-cmd-derive |
no |
fuzzy |
Registry::fuzzy_search() via fuzzy-matcher (skim algorithm) |
no |
mcp |
McpServer stdio transport (Model Context Protocol) |
no |
Error Handling
Argot uses structured error types via thiserror. All fallible operations return Result<T, E>.
BuildError
Returned by CommandBuilder::build(), ArgumentBuilder::build(), and FlagBuilder::build().
Common variants: EmptyCanonical, AliasEqualsCanonical, DuplicateAlias, DuplicateFlagName, DuplicateShortFlag, DuplicateArgumentName, DuplicateSubcommandName, VariadicNotLast, EmptyChoices, ExclusiveGroupTooSmall, ExclusiveGroupUnknownFlag.
ResolveError
Returned by Resolver::resolve().
use ResolveError;
match resolver.resolve
ParseError
Returned by Parser::parse().
Common variants: NoCommand, Resolve(ResolveError), MissingArgument, UnexpectedArgument, MissingFlag, FlagMissingValue, UnknownFlag, UnknownSubcommand, InvalidChoice, MutuallyExclusive.
CliError
Returned by Cli::run().
| Variant | Description |
|---|---|
Parse(ParseError) |
Parse failed; error and best-effort help have already been printed to stderr. |
NoHandler(String) |
The resolved command has no handler registered. |
Handler(Box<dyn Error>) |
The registered handler returned an error. |
QueryError
Returned by Registry::to_json().
| Variant | Description |
|---|---|
Serialization(serde_json::Error) |
JSON serialization failed. |
Design Principles
Single Source of Truth — the command model drives all outputs: help text, Markdown docs, JSON schemas, and MCP tool definitions. Manual help strings are not needed.
Deterministic Behavior — parsing and resolution are predictable. Prefix matching is unambiguous; ambiguity is surfaced as an explicit error or structured JSON rather than a guess.
Explicit Over Magical — no hidden behavior. Aliases are declared, spellings are declared, exclusivity groups are declared. Nothing is inferred from field types or naming conventions unless you opt in via #[derive(ArgotCommand)].
Discoverability — commands are easy to explore programmatically. Agents never need to scrape help text; the query command and Registry API provide structured access to all metadata.
Idiomatic Rust — ownership and lifetimes are explicit; all fallible operations return Result; optional integrations are gated behind feature flags.
MSRV
Minimum Supported Rust Version: 1.94.0
License
MIT