burgee

Your CLI is an agent tool

Every burgee program answers --schema and --mcp from its one declaration — no tool definitions, no second interface.

A burgee program is data first. The same manifest that renders --help answers two questions an agent asks before anything else.

--schema — the program as data

mytool --schema

Prints the commands, their arguments and options (with choices, defaults and which are required), each command's effects, its examples, and a JSON Schema per command. It reads the manifest and nothing else: no config file, no network, no handler runs (N8), so it is safe to call first.

--mcp — the program as MCP tools

mytool --mcp

Serves the same manifest as MCP tools over stdio — JSON-RPC 2.0, three methods, zero dependencies. A tool call runs the command exactly as a --json caller would; the result is that envelope, byte for byte, so an agent reading your CLI over MCP and one shelling out with --json see one payload.

Only what you publish

A command is a tool only when it declares what it does:

defineCommand({
  name: 'deploy',
  effects: 'non_idempotent', // read_only | idempotent | non_idempotent
  // …
});

effects generates MCP's readOnlyHint, idempotentHint and destructiveHint. The protocol defaults destructiveHint to true, so a command that says nothing is not served at all rather than served with the dangerous reading. On a commander-syntax program the same declaration is .effects('read_only').

Client configuration

For Claude Desktop, Claude Code, Cursor or any stdio client, register the program as a server — nothing else to write:

{
  "mcpServers": {
    "mytool": {
      "command": "npx",
      "args": ["mytool", "--mcp"]
    }
  }
}

If the program declares its own --schema or --mcp option, burgee leaves it alone: the surfaces are additive, never a change to a program that already exists.

completion <shell> — static, never runs your CLI on TAB

mytool completion zsh > "${fpath[1]}/_mytool"   # also: bash, fish, pwsh, fig

Commands, subcommands, options and choice values, with descriptions where the shell shows them — generated from the same manifest, as plain shell. Pressing TAB never executes the program. Each script is pinned by a snapshot and exercised by its real shell in CI. The templates live in burgee/completions and load only on that command, so a program pays for them when it prints a script, never at startup.

--explain <option> — where a value came from

Every value a command sees comes from exactly one place, in one fixed order: flag > env > config file > package.json field > default. --explain says which, and what it beat:

$ mytool deploy --explain region
region = "eu-1"   from config file ./mytool.config.json
         candidates: flag --region (unset), env MYTOOL_REGION (unset), default "us-1"

The same information rides under --json as meta.provenance, per option — for an agent, the difference between one call and five. envPrefix and config: true on defineProgram opt a program into env names and config discovery; nothing is read that the running command did not declare.

Stopping instead of prompting

Under a detected agent (AI_AGENT, CLAUDECODE, CURSOR_AGENT, CODEX_THREAD_ID, GEMINI_CLI, …) a program is non-interactive by default, terminal or not; FORCE_TTY=1 overrides. A handler reads ctx.interactive and ctx.agent, and where it would have prompted it stops with what the caller must do:

if (!ctx.interactive) ctx.actionRequired({
  reason: 'confirm',
  message: 'deploying to prod needs confirmation',
  next: [{ command: 'deploy --target prod --yes', when: 'once you have reviewed the plan' }],
});

The envelope carries status: "action_required", the reason, and next[] as runnable commands with the program in front and the caller's own --json carried; the exit code is 4. An idempotent command reports changed: true | false, and --schema summarises above a character budget with <command> --schema to drill into one.

On this page