incurs 0.7.2

A declarative CLI framework for Rust with typed commands, agent discovery, HTTP, and MCP
Documentation

incurs

The CLI framework for humans and agents.

Define a command once. incurs derives the argument parsing, help, validation, output formatting, JSON Schemas, and every transport from that single definition — so a command you wrote for a terminal is already an MCP tool, an HTTP route, an OpenAPI operation, a skill file an agent can read, a shell completion, and a window.

[dependencies]
incurs = "0.6"
schemars = "1"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

Requires Rust 1.88 or newer.

Quick start

use incurs::cli::Cli;
use incurs::command::{CommandDef, TypedContext, TypedResult};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, incurs::Args)]
struct GreetArgs {
    /// Name to greet.
    name: String,
}

#[derive(Deserialize, incurs::Options)]
struct GreetOptions {
    /// Add an exclamation mark.
    excited: bool,
}

#[derive(JsonSchema, Serialize)]
struct GreetOutput {
    message: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let greet = CommandDef::typed::<GreetArgs, GreetOptions, (), GreetOutput, _, _>(
        "greet",
        |ctx: TypedContext<GreetArgs, GreetOptions, ()>| async move {
            TypedResult::ok(GreetOutput {
                message: format!(
                    "Hello, {}{}",
                    ctx.args.name,
                    if ctx.options.excited { "!" } else { "." },
                ),
            })
        },
    )
    .description("Greet someone")
    .done();

    Cli::create("greet")
        .version("1.0.0")
        .command("greet", greet)
        .serve()
        .await
}
$ greet greet Ada --excited --json
{
  "message": "Hello, Ada!"
}

Doc comments become descriptions everywhere. CommandDef::typed derives the input schemas from GreetArgs and GreetOptions, and the output schema from GreetOutput. The handler receives validated values whether the call arrived from a terminal, HTTP, or an agent.

What that one definition already gives you

Nothing below needs extra code.

Run this You get
greet --help Help for the CLI and every command
greet greet --schema The command's JSON Schema
greet --llms / --llms-full A manifest written for an agent to read
greet --format json|yaml|toon|jsonl|md The same result in any output format
greet --mcp An MCP server over stdio
greet mcp add Registration with detected MCP clients
greet skills add Agent Skills installed for detected agents
greet completions bash|zsh|fish A shell completion script
greet plugin build --output ./dist A portable Agent Plugins 1.0 package
greet --config-schema A schema for the config file format

Output

A command returns TypedResult::ok(value), and the serialized value is the output. Pass --full-output for the {ok, data, meta} envelope when a caller wants timing and command metadata. TypedResult::error(code, message) produces the structured error envelope and a non-zero exit; ok_with_exit_code passes a wrapped subprocess's status through without turning success into a failure; ok_with_cta attaches follow-up commands.

The default format is TOON. Table and CSV are deliberately opt-in, through a separate crate:

incurs-extras = "0.6"
use incurs_extras::{CliExtras, ExtraFormat};

let cli = cli.default_extra_format(ExtraFormat::Table);

Feature flags

incurs = { version = "0.6", features = ["http", "mcp", "openapi"] }
Feature Adds
cli (default) Process adapters, signals, and terminal output
toon, tokens (default) TOON output and --token-count / --token-limit
http Axum routes for root and nested commands, plus fetch gateways
mcp MCP server, all five published standards at once
openapi Import an OpenAPI 3.x document as commands, and emit one
yaml YAML output
agent-plugins, agent-plugins-mcp Load Agent Plugin packages, with or without MCP transports

MCP clients that support discovery negotiate with server/discover; older clients initialize with their exact standard. Fallback happens only on protocol evidence, never on an authentication, transport, or server failure.

The incurs command-line tool

cargo install incurs-cli     # installs the `incurs` binary
incurs explain               # the Rust authoring reference, by topic
incurs explain typed-commands

Code generation

Point it at a project whose binary exposes --llms-full, and it writes typed helpers for calling that CLI from Rust:

incurs gen --dir ./my-cli --entry my-cli --config-schema
  • src/incurs_generated.rs — typed command modules, argument and option types, CTA renderers, and the embedded manifest
  • incurs.manifest.json — the canonical command manifest
  • config.schema.json — the config file schema, with --config-schema

--output and --json-output override the first two paths. --entry takes a Cargo binary name or a path to an executable.

Agent Plugin packages

Any incurs CLI can package itself:

my-cli plugin build --bundle-cli --output ./dist/my-cli-plugin

The package keeps its layers separate: skills/<name>/SKILL.md for agents to read, mcp.json declaring the tool surface, and bin/my-cli as the executable, all declared by a root plugin.json. Use --plugin-no-mcp for a skills-only package.

The incurs binary is also a client for those packages:

incurs plugin install ./dist/my-cli-plugin
incurs plugin validate ./dist/my-cli-plugin --data-dir ~/.local/share/my-cli-plugin
incurs plugin tools    ./dist/my-cli-plugin --data-dir ~/.local/share/my-cli-plugin
incurs plugin call     ./dist/my-cli-plugin my-server_my-tool \
  --arguments '{"name":"Ada"}' --data-dir ~/.local/share/my-cli-plugin
incurs plugin uninstall my-cli [--purge]

Installation validates the package, checks its operating system and architecture, and installs the command into your executable directory, telling you when that directory is not on PATH. Each MCP server connects independently, so one failure does not hide the tools from the others. The data directory you name is created before launch and persists across runs.

See Agent Plugins compatibility for the full behavior and failure-boundary matrix.

Calling commands without a CLI

Cli::tool_catalog() exposes every MCP-visible command as a transport-neutral Rust API. Calls made through it use the same schemas, middleware, environment fields, config defaults, streaming results, and structured errors as the CLI — it is the boundary MCP, Code Mode, and the desktop application all go through. try_tool_catalog() reports name collisions instead of silently replacing a command.

Code Mode

incurs-codemode lets an agent write a small JavaScript program that calls your tools, instead of making one tool call per step. It provides connector discovery, approval policy, immutable capability snapshots, deterministic replay, cancellation, ordered events, artifact-backed large values, rollback hooks, and bounded durable history. It can wrap an incurs catalog, a remote MCP client, or an authenticated OpenAPI client.

incurs-codemode-local runs those programs in a resource-limited QuickJS runtime, and incurs-codemode-mcp exposes the lifecycle to MCP clients:

Tool Purpose
codemode_search Search tools and snippets, with the declarations needed to call each match
codemode_execute Start a JavaScript execution
codemode_execution Read execution state or an owned artifact
codemode_decide Approve or reject one pending action
codemode_cancel Cancel a running or paused execution

Terminal applications

incurs-app-ratatui runs a command graph full-screen in a terminal: commands on the left, the selected command's inputs collected from its schema, results below.

use incurs_app_ratatui::TerminalApp;

TerminalApp::from_cli(&cli)?.title("Todo").run()

The installed incurs tool does this for its own commands:

incurs --tui

See its README for the key map and the controls each schema type gets.

Native desktop applications

incurs-app-gpui ships the same command graph as a double-clickable application, for people who will never open a terminal. Commands are listed in a window, each command's inputs are collected from its schema, and every call goes through the tool catalog — so validation, middleware, config defaults, streaming, and cancellation behave exactly as they do on the CLI.

use incurs_app_gpui::DesktopApp;

DesktopApp::from_cli(&cli)?.title("Todo").run()

MacBundle wraps the built executable in a macOS .app that installs by dragging. See its README for the full guide and current limits.

How it fits together

              typed command definitions
                         |
                         v
            shared command graph + schemas
      ______________|____________________________
     |        |        |            |            |
    CLI     HTTP      MCP     tool catalog   generated artifacts
                                   |          |-- OpenAPI
                                   |          |-- Agent Skills
                                   |          |-- shell completions
                                   |          `-- Rust and JSON codegen
                                   |
                                   |-- Code Mode  (local QuickJS, remote executors)
                                   |
                                   `-- native desktop window

Learn more

License

MIT. incurs began as a Rust port of wevm/incur and keeps its command model; the two are now independent.