# Your CLI is an agent tool

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

Source: https://burgee.interlace.tools/docs/agent-surfaces

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

```bash
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.

The document shows each option's default and env var, but not which of them a run used. So
just before `commands` it carries one line naming the flag that answers that:
`"explain":"--explain <option> on a command says where its value came from: default, env, config or flag"`.
Agents that read `--schema` without it spent three to eight turns probing the env var through
shell calls (D-20261009-b1-totals-and-explain). One command's schema (`mytool deploy --schema`)
does not carry it, and neither does `--schema` on `burgee/commander` or `burgee/yargs`, which
do not parse `--explain`.

`mytool schema` is the same request spelled as a subcommand — `schema deploy` and
`schema deploy --field options.region` print exactly what the flag forms print — because
that is where [clispec.dev](https://clispec.dev) and the agents that follow it look. A
program whose own `schema` command exists, or whose root command takes arguments, keeps the
word: burgee answers only the flag there. The root `--help` names `--schema`.

### It is also a clispec v0.3 document

The same document is a [The CLI Spec v0.3](https://clispec.dev) document
([its schema](https://clispec.dev/schema/v0.3.json)): the keys clispec defines sit beside
burgee's own, and `schemaVersion` stays 1, because none of burgee's keys changed meaning. Each
is read from what the program already declares.

| Key | What it says | Read from |
| :-- | :-- | :-- |
| `clispec` | `"0.3"` | — |
| `version` | the program's version, always | `defineProgram({ version })`, else the owning `package.json` |
| `errors` | every kind a run can fail with: `{ kind, exit_code, retryable, description }` | `ExitCode`, and each `defineError` class |
| `global_args` | the flags every command takes | the rows root help prints under `Global options:` |
| `output` | `{ "tty": "text", "piped": "text", "formats": [...] }`: text unless asked for, even under a pipe | — |
| a command's `example` | its first example as `{ "args": [...] }`, the program's name dropped | `examples` |
| a command's `cardinality`, `output_kind`, `media_type` | how much comes back, and what it is | `output: { cardinality, kind, mediaType }` |
| a command's `stdout_schema` | what `--json` prints: the envelope, with your result as its `data` | `output.schema`, else `fields` |

```js
defineCommand({
  name: 'list',
  effects: 'read_only',
  examples: [{ command: 'mytool list --json' }],
  output: { cardinality: 'bounded', schema: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } } },
  run: () => [{ id: 'a' }], // TypeScript holds this to output.schema
});
```

A runnable command with no `output.cardinality` makes `burgee dev` print a warning, never an
error: clispec has no safe default for how much comes back. clispec's checker scores the
reference demo 24 / 24 ([Benchmarks](/docs/benchmarks)). Not carried: the `burgee/commander` and
`burgee/yargs` documents, whose `--schema` is built where their weight budgets have no room, and
the summary printed over the budget, which lists commands without what clispec requires of one.

## `--mcp` — the program as MCP tools

```bash
mytool --mcp
```

Serves the same manifest as [MCP](https://modelcontextprotocol.io) tools over stdio —
JSON-RPC 2.0, four methods (`initialize`, `ping`, `tools/list`, `tools/call`), no dependency outside the burgee family. 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.

### What becomes a tool

Every command that runs and is not hidden, named by its path — `config get` is
`config_get`, and a program whose root command runs (a single `run(defineCommand(…))`, or
a commander program with a root `.action()`) is named after the program. What keeps a
command out is a word its author wrote, never silence:

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

- **On burgee's own API, `effects` is required.** `defineCommand` refuses a runnable
  command without it, so every tool carries MCP's `readOnlyHint`, `idempotentHint` and
  `destructiveHint`, generated from the declaration. `'withheld'` serves the command to
  people and keeps it out of `tools/list`.
- **On `burgee/commander` and `burgee/yargs`, a command that declared nothing is still a
  tool.** Neither incumbent has a notion of effects, so it is listed with
  `annotations: { effects: 'undeclared' }` and no hints — and MCP's defaults for absent
  hints (not read-only, destructive) are the reading a client applies. Declare it with
  `.effects('read_only')`, or keep it out with `.effects('withheld')`.

### Arguments

A tool's `inputSchema` has one property per argument and option. An option's property is
its declared key and names the flag it becomes — `"dryRun": { "type": "boolean", "flag":
"--dry-run" }` — and a tool call is turned back into exactly that flag. A boolean sent as
`false` is passed as `--no-<name>` when the option would otherwise be on (a `true` default
or an environment variable) and left out otherwise, so an agent that spells out every
`false` trips no relation. Under `burgee/commander`, a `--no-color` declared without a
`--color` is the property `noColor`.

### Results

The `--json` envelope, byte for byte. Under `burgee/commander`, `data` is what the action
**returned**: an action that prints instead gives `data: null`, because commander actions
return nothing. Return the value — `.action(() => ({ lines }))` — and it is `data`.
Printed output becomes the tool result: whatever a handler writes to stdout during a call
(`console.log`, `process.stdout.write`) is captured and follows the envelope as a second
text item, never a stray line on the JSON-RPC stream, and what it writes to stderr stays on
stderr.

The data comes back as data too. The result carries the envelope's `data` as
`structuredContent` beside the text, or its `error` on a failure, and wraps anything that is not
an object as `{ "result": … }`, because the protocol requires an object. A command that declares
`output.schema` lists it as the tool's `outputSchema`, wrapped the same way, so a client can
check the result against it; a command that declares none lists none, rather than a schema
guessed from one run. Text a handler printed instead of an envelope carries no
`structuredContent`, and the text is unchanged either way.

A handler that throws, rejects or throws something that is not an `Error` is an error
result, not a crashed server: `isError: true`, and the text is the `{ "ok": false, "error":
… }` envelope a `--json` caller gets on stdout. An `AuthError` carries `code` 5 (`"auth"` on
the commander and yargs front ends) with its `hint` and `fix`, and the server answers the
next call.

### Cancellation and progress

Calls are served concurrently, so a client can stop one while it runs. `notifications/cancelled`
aborts that call's `ctx.signal` with the reason `cancelled`, the same signal `--timeout` aborts:
hand it to `fetch`, a child process or anything else that takes a signal, and the work stops.
Its `ctx.onExit` handlers run once, and no response is sent for it. The server goes on serving.

`ctx.progress(done, total?, message?)` reports how far a command has got. When the client sent a
`progressToken` with the call, each report is a `notifications/progress` carrying it; a value that
does not rise above the last one is dropped, because the protocol requires progress to increase.
Anywhere else `ctx.progress` does nothing, so call it unconditionally:

```ts
run: async ({ signal, progress }) => {
  for (const [at, file] of files.entries()) {
    await upload(file, { signal });
    progress(at + 1, files.length, file);
  }
}
```

A bar from `flagstaff/progress` does the same on its own: inside a call that asked for progress, it
sends its count, with its static line (`12/30 files · 40%`) as the message, and draws nothing into
the result. Outside one it draws as it always has. The root `flagstaff` entry's `progress` always
draws, so the bridge costs nothing to a program that never serves MCP.

### Client configuration

For Claude Desktop, Claude Code, Cursor or any stdio client, register the program as a
server — nothing else to write. `--mcp --print-config` prints the block for the program you run
it on: its name, and the command that starts it. A program started as one of its package's bins
is started by that name, which is how an installed CLI is found on `PATH`; one started as
`node <file>` is started that way, with the file's full path. On Windows, where npm installs a
bin as a `.cmd` shim that a client cannot start directly, the bin is started through
`"command": "cmd", "args": ["/c", "demo-burgee", "--mcp"]`. For the reference demo, run as its
bin `demo-burgee` on Linux or macOS:

{/* mcp-config:start — `demo-burgee --mcp --print-config`, written by scripts/mcp-config-block.ts */}

```json
{
  "mcpServers": {
    "demo": {
      "command": "demo-burgee",
      "args": [
        "--mcp"
      ]
    }
  }
}
```

{/* mcp-config:end */}

Paste it into the client's configuration as it is. The block above is that command's output,
and a test starts the server from it and completes a tool call.

On `burgee/commander` and `burgee/yargs`, a program that declares its own `--schema` or
`--mcp` option keeps it, and burgee leaves it alone: the surfaces are additive, never a change
to a program that already exists. On burgee's own API the names are reserved, and
`defineCommand` refuses an option that redefines one.

## `--skill` — the program as an Agent Skill

```bash
mytool --skill                        # SKILL.md on stdout
npx burgee skill dist/index.js        # SKILL.md and AGENTS.md, beside the bin
npx burgee skill dist/index.js --check  # in CI: exit 1 when the committed files are stale
```

An [Agent Skill](https://agentskills.io/specification), projected from the same declaration:
front matter with a `name` and a `description` within the specification's limits, then what the
program is for, the contract an agent relies on (`--json` and its envelope, `--explain`, every
exit code with its error kind), and one line and the first example per command. It is small on
purpose: a command's options are behind `mytool --schema <command>`, which the skill names and
never inlines, so an agent pays for one command's contract when it uses that command.

`burgee skill` writes `SKILL.md` and `AGENTS.md` (the same body, no front matter) into the
directory of the `bin` your package.json names, unless `--out` says where. When an `AGENTS.md`
sits beside the binary or in the directory above it, the root help's line for agents names it;
without one, the line is unchanged. A root that declares its own `--skill` option keeps it.

## `completion <shell>` — static unless you ask

```bash
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 does not execute the
program, with one exception you opt into: an option or a positional that declares `complete`
has its values asked for at TAB time, through `mytool __complete`. 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.

A completer is called as `complete(partial, context)`, and `context` is the rest of the line
before the cursor. It carries the command `path`, the `options` already typed (coerced the way a
run coerces them), the `positionals`, and `raw` for any value that failed coercion. So one value
can depend on another (yargs #793):

```ts
defineCommand({
  name: 'deploy',
  options: {
    remote: { type: 'string' },
    branch: { type: 'string', complete: (partial, { options }) => branchesOf(String(options.remote ?? 'origin')) },
  },
  arguments: [{ name: 'service', complete: () => services() }],
  effects: 'non_idempotent',
  run: ({ options, args }) => deploy(args.service, options),
});
```

The line is unfinished, so nothing is required, no relation is checked, and no env, config or
default is read on TAB. A completer that takes one parameter is called as it always was, and a
completer that throws offers nothing.

## `--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:

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

Every command that runs lists `--explain <option>` in its help, and the root help's `For
agents:` line names it beside `--schema` and `--json`. So do the `--schema` document and the
hint on an unknown command that matches nothing. 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

`ctx.interactive` is true only with a terminal on stdin, no `CI`, and no detected agent
(`AI_AGENT`, `CLAUDECODE`, `CURSOR_AGENT`, `CODEX_THREAD_ID`, `GEMINI_CLI`), terminal or not;
`FORCE_TTY=1` overrides. It is roundel's `interactive()`, the same rule caique's prompts ask. A
handler reads `ctx.interactive` and `ctx.agent`, and where it would have prompted it stops with
what the caller must do:

```ts
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.
