Search
Search documentation
Introduction
Updated Oct 23, 2026
Define each command with typed input and output structs. Get composability, structured JSON I/O, and LLM-ready manifests baked in — for humans, agents, and scripts alike. Built on serde, schemars and clap; Rust 1.88+, edition 2024.
Installation
cargo add fireargs
cargo add serde --features derive
cargo add schemarsA first command
f::command(config) opens a builder: .input::<I>() takes the input struct, .output::<O>() the output type and .handler(...) the async function between them. Fields become arguments and options; a #[schemars(extend("fireargs" = {...}))] attribute carries the CLI config serde and schemars do not model. Import the module as f (use fireargs as f;), as here, or the names you use.
use std::process::ExitCode;
use fireargs::{self as f, CommandConfig, Error};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
// Typed input: doc comments, defaults and bounds become arguments and options
#[derive(Deserialize, JsonSchema)]
struct GreetInput {
/// The user's name.
#[schemars(extend("fireargs" = {"kind": "argument"}))]
name: String,
/// Repeat it n times.
#[serde(default = "one")]
#[schemars(range(min = 1))]
times: u32,
/// Use capitals.
#[serde(default)]
#[schemars(extend("fireargs" = {"kind": "option", "short": "s"}))]
shout: bool,
}
fn one() -> u32 {
1
}
// Typed output, validated on every run
#[derive(Serialize, JsonSchema)]
struct Greeting {
greeting: String,
}
fn main() -> ExitCode {
let config = CommandConfig {
name: Some("greet".into()),
description: Some("Greet someone politely.".into()),
..Default::default()
};
// A clap command, typed from the input and output structs
let greet = f::command(config)
.input::<GreetInput>()
.output::<Greeting>()
// Output for humans; --json prints the data instead
.render(|output, _| Some(output.greeting.clone()))
.handler(|input, _| async move {
let words = vec![format!("hello {}", input.name); input.times as usize];
let mut greeting = words.join(" ");
if input.shout {
greeting = greeting.to_uppercase();
}
Ok::<_, Error>(Greeting { greeting })
});
// Parse argv, run the handler and return the exit code
greet.run()
}That gets you:
$ greet world --times 2 -s # CLI input, rendered output
HELLO WORLD HELLO WORLD
$ greet world --json # CLI input, JSON output
{
"greeting": "hello world"
}
$ greet --json '{"name":"world","times":2}' # JSON input, JSON output
{
"greeting": "hello world hello world"
}
$ greet world --times 0
error: option '--times <value>' argument '0' is invalid. 0 is less than the minimum of 1--help comes from clap:
Greet someone politely.
Usage: greet [OPTIONS] [name]
Arguments:
[name]
The user's name.
Options:
--times <value>
Repeat it n times. (default: 1)
-s, --shout
Use capitals. (default: false)
--json [<value>]
write output as JSON; pass a JSON string to also drive input
--llms
print this command's schema (name, input, output) as JSON for LLM tool-use
-h, --help
Print help (see a summary with '-h')A first program
Group leaves with f::program(config).router([...]):
// Composable commands: a router of leaves and nested programs
let cli = f::program(ProgramConfig {
name: Some("myapp".into()),
description: Some("My CLI".into()),
version: Some("1.0.0".into()),
..Default::default()
})
.router([("greet", greet.into()), ("deploy", deploy.into()), ("db", db.into())]);
// Parse argv, run the selected leaf and return the exit code
cli.run()That gives:
$ myapp --help
My CLI
Usage: myapp [OPTIONS] [COMMAND]
Commands:
greet Greet someone politely.
deploy Deploy the current build.
db Database tasks.
help Print this message or the help of the given subcommand(s)
Options:
--llms
print the program manifest (subcommand schemas) as JSON for LLM tool-use
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version
$ myapp db migrate --json '{}'
{
"applied": 0
}Programs nest — router accepts other programs the same way it accepts
leaves. myapp db migrate routes through both levels.
JSON mode
Every command takes --json. Bare, it switches the output to JSON; given a value, that value is
the input, checked against the input schema, so scripts never assemble argv:
$ greet world --times 2 --json # CLI input, JSON output
{
"greeting": "hello world hello world"
}
$ greet --json '{"name":"world","times":3,"shout":true}' # JSON input, JSON output
{
"greeting": "HELLO WORLD HELLO WORLD HELLO WORLD"
}
$ greet world --json | jq -r .greeting # pipe it into other tools
hello world
$ greet --json '{"times":2}' # input errors go to stderr, exit code 1
error: invalid --json input at 'name': "name" is a required property
$ greet --json '{"name":"world","times":"2"}'
error: invalid --json input at 'times': "2" is not of type "integer"stdout only ever carries the data, so a script can parse it without checking for noise.
LLMs mode
Every command and program ships with --llms, which prints an
MCP-tools/list-shaped manifest. For greet.rs above:
{
"tools": [
{
"name": "help",
"description": "Documentation tool, not callable. Each `tools[].name` is the space-separated path beneath this binary. Invoke a tool with `<binary> <name> --json '<value>'` matching that tool's `inputSchema`; output is JSON on stdout matching `outputSchema`.",
"inputSchema": { "type": "object", "properties": {} },
"outputSchema": { "type": "object", "properties": {} }
},
{
"name": "greet",
"description": "Greet someone politely.",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The user's name."
},
"times": {
"type": "integer",
"format": "uint32",
"minimum": 1,
"description": "Repeat it n times.",
"default": 1
},
"shout": {
"type": "boolean",
"description": "Use capitals.",
"default": false
}
},
"required": ["name"],
"$schema": "https://json-schema.org/draft/2020-12/schema"
},
"outputSchema": {
"type": "object",
"properties": { "greeting": { "type": "string" } },
"required": ["greeting"],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
}
]
}An LLM agent reads the manifest, picks a tool, and invokes it with
<binary> <name> --json '<value>'. Output comes back as JSON on stdout.
Learn more
- Commands — the
f::command(...)builder and the leaf it builds. - Inputs — the input struct and per-field CLI config in
#[schemars(extend(...))]. - Outputs — the output type, serialization, and JSON Schema in
--llms. - Programs — subcommand trees, nesting, and program-level config.
- JSON mode —
--jsonfor programmatic and pipeline use. - LLMs mode —
--llmsand the MCP-shaped manifest. - Composability — share common params across commands by composing structs.
- Agent Skills — teach your coding agent to write and drive Fireargs CLIs.
- Contributing — local setup, conventions, release flow.
License
MIT.
Created with ❤ and Livemark