> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flow.engineer/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a command line

> Build a product CLI over your MCP server, with custom commands, authentication and terminal conversations.

Expose your MCP tools as commands for people, scripts and coding agents. Tool commands need no
model key: they call the server directly. The optional `chat` command talks to a messaging agent
through a separate terminal endpoint.

## Try the starter

Start `npm run mcp` in one terminal. In another terminal in the same project:

```sh theme={null}
npm run cli -- --help
npm run cli -- menu --json
npm run cli -- order --help
```

**Check it worked:** `menu --json` prints the café's menu as JSON. `order --help` lists the
commands for placing and tracking orders.

## Define your commands

Install the optional peers when adding a CLI to an existing project:

```sh theme={null}
npm i @modelcontextprotocol/client@2.3.1 @stricli/core@1.3.0
```

`src/cli.ts` defines your product's command names, groups, arguments and output:

```ts theme={null}
import { fileURLToPath } from "node:url";
import { defineCli, runCli } from "@flow-engineer/flow/cli";

export const cli = defineCli({
  name: "cafe",
  server: process.env.CAFE_SERVER ?? "http://127.0.0.1:8790/mcp",
  description: "Menu and pickup orders",
  groups: { order: "Place and track orders" },
  sections: { Orders: ["order"], Catalog: ["menu"] },
  commands: {
    menu: { tool: "get_menu", columns: ["item", "milks", "allergens"] },
    "order status": { tool: "order_status", args: ["order_id"] },
  },
});

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  process.exit(await runCli(cli, process.argv.slice(2)));
}
```

Tools without an explicit command use their tool name with underscores changed to dashes.
Commands support `--json` and `--fields`; writes support `--dry-run`, and actions that can delete
or overwrite require `--yes` without an interactive terminal. Add your own Stricli commands
alongside the generated tool commands. Handwritten commands execute locally without discovery,
including their help and argument parsing; an explicit `this.cli.call(...)` calls MCP. A handwritten
command takes precedence over a derived command with the same name. `flow check` checks that your
CLI configuration still matches the tools.

`sections` groups top-level commands and groups under headings in the order declared; remaining
commands appear under Other. `chat`, when configured, comes first; built-in commands appear under
This CLI. `flow check` rejects unknown names and a name listed in more than one section.
Run `cafe help order` to find commands whose names or descriptions contain every supplied word.

## Format results for people

Add `output` and `format` to a [tool](https://docs.flow.engineer/build-agent.md#format-tool-results) to share readable
Markdown across MCP and the CLI. A command without `columns` or `view` prints that text by
default; `--json` and `--fields` keep using the structured result.

For terminal rendering, install these optional peers:

```sh theme={null}
npm i marked@12.0.2 marked-terminal@7.3.0
```

For a tool returning `{ order_id: "BL-1040", ready_by: "10:30" }`, the example formatter prints
**Order BL-1040** is ready by 10:30. Piped output, `NO_COLOR`, and installations without these
peers print the Markdown as written. A formatter error falls back to JSON and records
`formatError` on `tool.end`; it does not turn a successful operation into a failure.

For an account-protected server, run the CLI's `auth login` command. It signs the person in with
OAuth; automation can use the configured token environment variable instead. The account and
permissions are verified by the same [MCP server](https://docs.flow.engineer/framework/mcp.md).

Run `npx @flow-engineer/flow cli --server <url> --help` for a generic CLI over an existing MCP
server. Always use the full package name with `npx`; `npx flow` fetches a different package.

## Common commands

```sh Discover theme={null}
npm run cli -- --help
npm run cli -- order --help
```

```sh Read data theme={null}
npm run cli -- menu --json
npm run cli -- order status BL-1040 --json
```

```sh Preview a write theme={null}
npm run cli -- order place --dry-run \
  --name Sam --items '[{"item":"Latte","size":"large","milk":"oat"}]'
```

The `order status` result depends on whether that order exists in the running café server.
`--dry-run` previews the operation without invoking the write tool.

## List and read skills

Use `skill` as a catalog and reader, without loading every skill body:

```sh theme={null}
npm run cli -- skill list
npm run cli -- skill get allergens
npm run cli -- skill get cafe-cli allergens --json
```

**Expected result:** `list` shows names and descriptions, including the generated `cafe-cli` guide
and the café's `allergens` workflow. `get allergens` prints that skill's full instructions. The
last command returns two named records, each with `name`, `description` and `content`; no other
skill bodies are read. Replace `cafe-cli` with `<your-cli-name>-cli` in your own project.

Bare `skill` is the same as `skill list`. `skill list --json` returns only names and descriptions;
`skill get <name> [name...] --json` returns an array even for one selected skill. The generated
CLI guide includes handwritten commands and maps MCP tool names to configured CLI command paths.
Workflow instructions come from the same `agent/skills/` source used by MCP and messaging.

To install selected skills:

```sh theme={null}
npm run cli -- skill get cafe-cli allergens --write ~/.claude/skills
```

This writes `<dir>/<skill-name>/SKILL.md` for each selected skill. Additional source assets are
not copied; use native plugin packaging to distribute those. The existing
`skill --write <dir>` command continues to export only the generated CLI guide. A handwritten
workflow with the same name takes precedence in the catalog; the legacy export still produces
the generated guide.

Use the standalone MCP endpoint (`/mcp`) for workflow skills. Its existing `read_skill` descriptor
advertises names and descriptions in `_meta["flow.engineer/skills"]`; bodies are fetched only by
`get`. Older MCP servers without this metadata still expose names through the reader's schema,
with empty descriptions. Unknown names trigger one discovery refresh before the CLI reports an
error. All requested names are checked before a batch is read or exported.

## Cached discovery and server updates

MCP-backed commands execute on your deployed MCP server; ship that server alongside the CLI.
The CLI caches tool definitions and the server's name/version in `~/.config/<name>/discovery/`
(`XDG_CONFIG_HOME` or `CliConfig.directory` overrides the location). Entries are scoped to the
endpoint, client and credentials. Discovery files have mode 600 and contain no credentials or tool results.

Cached definitions load immediately, including older snapshots. Help, `tools`, `skill` and dry
runs do not wait for a server check. Once a snapshot is five minutes old, a background refresh
checks the server and fetches current definitions. Installed CLIs use a separate process that
survives the command exiting, with a 15-second limit. Concurrent commands share one refresh;
background failures keep the previous definitions and do not add output.

Tool calls still connect and authenticate. If the server's name or version differs, the CLI
fetches definitions and rebuilds the generated command's parser and approval flags before sending
the call. Bump the MCP server's version when changing tool contracts or safety annotations.
Periodic discovery also picks up changes when a server does not bump its version.

If an unknown command or invalid argument could be caused by an old cache, the CLI fetches current
definitions and reparses once before execution. If the server rejects arguments after a request
has been sent, the CLI refreshes definitions and reports the error with a help hint. It never
silently resubmits that request or reruns a handwritten command.

To force current discovery:

```sh theme={null}
npm run cli -- tools --refresh-tools --json
```

**Expected result:** the CLI fetches and prints the server's current tool definitions and updates
the cache. A new tool can also be invoked directly; an unknown command triggers foreground discovery.
Missing or malformed cache files require discovery. An unwritable cache directory does not stop
commands working, but each invocation must discover again.

Set `discoveryCache: false` in `defineCli` to disable disk caching, or
`discoveryCache: { maxAgeMs: 60_000 }` for a one-minute background interval. Handwritten local
commands need no MCP server unless they explicitly call it; `--refresh-tools` opts into discovery.
If a handwritten command declares its own `--refresh-tools` flag, that flag keeps its local meaning;
use `tools --refresh-tools` for framework discovery.

## Local playground CLI

> **Run the actual CLI inside a local playground**
>
> A local playground can run the team's actual CLI, including custom commands, OAuth, interactive
> chat, confirmations, stdout/stderr and exit codes. Opt in on the server that runs on your computer:
>
> ```ts theme={null}
> import { cli } from "./cli.ts"; // the team's defineCli configuration
> await play.listen(agent, 8790, { localCli: { channel: "terminal", config: cli } });
> // For a local agent mounting its playground under /playground:
> const handler = play.handler(agent, { token, base: "/playground", localCli: { channel: "terminal", config: cli } });
> ```
>
> The named channel must be declared on the agent. Its tab opens the runner instead of simulated
> chat. Each browser gets an independent temporary sign-in and conversation directory; the test
> person picker does not grant it an account. Run `auth login`, open the sign-in link, then return
> to the runner. `chat` keeps reading input until `/exit`; reload reconnects to that session.
> **Clear output** clears the viewport; **New CLI session** ends the session and removes its local
> credentials, without clearing the account's agent history. Sessions expire after 30 minutes
> without browser activity, and a server restart starts fresh. Ending a session does not undo
> work already accepted by the agent.
>
> This runner accepts only direct loopback connections with a loopback Host and same-origin API
> requests, even if explicitly configured on a mounted handler. It is absent by default and is
> not enabled in the hosted test cloud. It invokes `runCli` in-process: no shell, arbitrary program
> execution, expansion or pipelines. Quoted arguments work. Its configured endpoints stay fixed;
> use the installed CLI for `--server`, `--token` or `skill --write`.
>
> The browser CLI runner is available locally only. Hosted browser execution is not supported.
>
> Embedded clients can set `CliConfig.directory` to isolate credential/conversation/discovery files and
> `CliIO.env` to avoid inheriting process tokens; `CliIO.signal` cancels sign-in and terminal HTTP
> requests. `CliIO.background(work)` lets an embedded host schedule discovery refreshes without
> delaying the response; invoke `work` while the session is alive. The local playground supplies this
> scheduler and cancels refreshes when the session ends. Defaults retain the installed CLI's existing
> environment and config directory.

## Terminal conversations

> [!NOTE]
>
> A terminal conversation needs the optional agent terminal handler described below. Serving the
> MCP endpoint alone enables tool commands; it does not add conversational chat.

`chat` talks directly to the agent, using the same bearer as MCP; it is not an MCP tool. `defineCli({ name: "acme", server, agent: "https://agent.example.com/agent/terminal" })`
overrides the terminal URL; the default is the MCP server's origin plus `/agent/terminal`.
`acme chat "Which orders are ready?" --json` prints one response; `acme chat` reads a TTY until
Ctrl-D or `/exit`. Without a TTY pass a message to `chat` (an interactive chat exits 2); sign-in failures exit 3.
`chat.json` is mode 600 and scoped by terminal endpoint. `--new` forgets the terminal's saved
reference; the agent keeps its one-person history across channels. It does not erase a person's
WhatsApp history. Teams can replace `chat` with their own Stricli command in `commands`.

Mount the optional terminal handler beside MCP (no model or extra dependency in the CLI):

```ts theme={null}
import { terminalServer } from "@flow-engineer/flow/mcp";
const terminal = terminalServer({ agent: () => agent, auth });
const agent = createAgent({ ...options, channels: [...channels, terminal.channel] });
// Route terminal.path to terminal.fetch(request), and MCP's paths to mcp.fetch(request).
```

`terminalServer` accepts `path` and `channel` overrides. It verifies auth on every POST,
serializes concurrent requests per person, and returns `{ conversationId, replies }` after the
turn settles. The authenticated key becomes the same person that `resolve` returns on other
channels. A body cannot choose its own person; a conversation reference from another person is
refused. Its custom UI uses Markdown and exports turns as channel `cli`.
For your own HTTP adapters, `agent.request({ channel, key, text, conversationId? })` exposes this
without the handler. Supply a verified key and serialize calls for that channel and person.

> **[Add another tool](https://docs.flow.engineer/build-agent.md)**
>
> New tools become commands automatically.

> **[Inspect calls in production](https://docs.flow.engineer/traces.md#tool-call-traces)**
>
> The server records calls from the CLI, MCP and plugins.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.