More AI skill/MCP docs (#2140)

* Iterating on AI/MCP docs

* docs iterate

* mlr skill

* docs iterate

* agent-skill.md.in

* docs iterate

* docs iterate

* edits

* codex/gemini info
This commit is contained in:
John Kerl 2026-07-04 14:45:29 -04:00 committed by GitHub
parent f87bb37194
commit 54237c04d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1400 additions and 572 deletions

View file

@ -41,7 +41,9 @@ nav:
- "Shell completion": "shell-completion.md"
- "Syntax highlighting: vimrc": "vimrc.md"
- "The REPL": "repl.md"
- "The MCP server": "mcp-server.md"
- "The Miller Agent Skill": "agent-skill.md"
- "The Miller MCP server": "mcp-server.md"
- "Miller AI internals": "ai-support.md"
- "Online help": "online-help.md"
- "How to contribute": "contributing.md"
- 'FAQs and examples':

244
docs/src/agent-skill.md Normal file
View file

@ -0,0 +1,244 @@
<!--- PLEASE DO NOT EDIT DIRECTLY. EDIT THE .md.in FILE PLEASE. --->
<div>
<span class="quicklinks">
Quick links:
&nbsp;
<a class="quicklink" href="../reference-main-flag-list/index.html">Flags</a>
&nbsp;
<a class="quicklink" href="../reference-verbs/index.html">Verbs</a>
&nbsp;
<a class="quicklink" href="../reference-dsl-builtin-functions/index.html">Functions</a>
&nbsp;
<a class="quicklink" href="../glossary/index.html">Glossary</a>
&nbsp;
<a class="quicklink" href="../release-docs/index.html">Release docs</a>
</span>
</div>
# The Miller Agent Skill
As of Miller version 6.20, released in July 2026, there are two main ways to get your AI to know
about a software tool (Miller, or others): **agent skills**, and [**MCP**](mcp-server.md). (See
[Miller and AI](ai.md) for an introduction.)
Miller ships a built-in [Agent Skill](https://www.anthropic.com/news/skills) -- a single `SKILL.md`
file -- inside the `mlr` executable, so agents that read skills directly from disk (Claude Code,
and other tools that support the Agent Skills format) can discover and drive Miller without
scraping help text or guessing at flags.
The skill is plain markdown with a YAML frontmatter header, placed where your agent already looks
for skills. The agent reads it into context once, the same way it reads any other instructions, and
from then on it runs `mlr` commands via whatever shell-executing tool it already has.
Here's what the skill file looks like:
<pre class="pre-highlight-in-pair">
<b>mlr skill print | head -n 15</b>
</pre>
<pre class="pre-non-highlight-in-pair">
---
name: miller
description: >
Drive Miller (mlr) to process CSV/TSV/JSON/etc. data. Use when constructing
mlr command lines: discover capabilities from the catalog rather than
guessing, learn the data's shape before writing expressions, validate DSL
before running, and recover from failures via structured errors.
---
# Miller agent playbook
Miller (`mlr`) is a command-line data processor for CSV, TSV, JSON, JSON
Lines, and other tabular/record formats, with SQL-like verbs (`cut`, `sort`,
`join`, `stats1`, ...) and an awk-like DSL (`put`, `filter`).
</pre>
For more background on the `mlr` commands the agent runs on your behalf, please see
[Miller AI internals](ai-support.md).
## Setup
Write the skill file to Claude Code's personal skills directory (do this before starting your
`claude` session):
<pre class="pre-highlight-in-pair">
<b>mlr skill install ~/.claude/skills/miller</b>
</pre>
<pre class="pre-non-highlight-in-pair">
Wrote /Users/kerl/.claude/skills/miller/SKILL.md
</pre>
For Codex and Gemini:
<pre class="pre-highlight-non-pair">
<b>mlr skill install ~/.agents/skills/miller</b>
</pre>
With no argument, `install` writes to `.claude/skills/miller/SKILL.md` under the current directory
instead. This is handy for a project-scoped skill checked into that project's repo rather than one
installed for every project on your machine:
<pre class="pre-highlight-in-pair">
<b>mlr skill install</b>
</pre>
<pre class="pre-non-highlight-in-pair">
Wrote .claude/skills/miller/SKILL.md
</pre>
There's no "uninstall" subcommand, since `install` only ever writes one plain file. Removing it is
an ordinary file operation:
<pre class="pre-highlight-non-pair">
<b>rm -rf ~/.claude/skills/miller</b>
</pre>
Then -- just interact with your agent as always! When you say something like `describe the data file example.csv`,
the agent will already know how to use Miller to help answer that question.
## What the Miller skill maps to
You don't have to type `skill` or anything else special in your agent session: rather you've
empowered the agent to discover things about Miller for itself. But if you're curious what's
actually placed in front of it:
<pre class="pre-highlight-in-pair">
<b>mlr skill --help</b>
</pre>
<pre class="pre-non-highlight-in-pair">
Usage: mlr skill {print|install} [options]
Puts the Miller Agent Skill (SKILL.md) where a coding agent can find it.
This is the same playbook mlr mcp serves as its "miller-playbook"
prompt/resource, packaged for agents that read Agent Skills from disk.
Subcommands:
print Write the skill content to stdout.
install [DIR] Write DIR/SKILL.md, creating DIR if needed.
Default DIR is .claude/skills/miller
-h or --help Show this message.
</pre>
And here's the file itself -- the whole thing, not an excerpt, since this and nothing else is what
the agent has to go on:
<pre class="pre-highlight-in-pair">
<b>mlr skill print</b>
</pre>
<pre class="pre-non-highlight-in-pair">
---
name: miller
description: >
Drive Miller (mlr) to process CSV/TSV/JSON/etc. data. Use when constructing
mlr command lines: discover capabilities from the catalog rather than
guessing, learn the data's shape before writing expressions, validate DSL
before running, and recover from failures via structured errors.
---
# Miller agent playbook
Miller (`mlr`) is a command-line data processor for CSV, TSV, JSON, JSON
Lines, and other tabular/record formats, with SQL-like verbs (`cut`, `sort`,
`join`, `stats1`, ...) and an awk-like DSL (`put`, `filter`).
Work this loop. Each step exists to prevent a specific, common failure.
## 1. Discover — never invent names
Everything valid is in the catalog; anything not in the catalog does not
exist. Hallucinated flag/function names are the top failure mode.
- Route an intent: `which` with e.g. `"join two files on a key"` → ranked
candidates. `confident: true` means a name matched; trust the top hit.
- Browse cheaply: `list_capabilities` with `index: true` → every
verb/function/flag/keyword with one-line summaries.
- Drill in: `list_capabilities` with `kind: "verb", names: ["join"]` → the
full entry. Prefer the structured `options` list (flag, arg, type, enum
`values`) when present; `usage_text` is the prose fallback.
- The whole catalog is cacheable against `(mlr_version,
catalog_schema_version)` — re-fetch only when either changes.
## 2. Constrain — learn the data before touching it
Call `describe_data` on the input first. It returns, per field: name, types
seen with counts, occurrence count, null count, cardinality, min/max, and —
for low-cardinality fields — every distinct value.
- Copy field names exactly from `describe_data`; never guess casing or
spelling.
- For flags like `-g` (group-by) and DSL comparisons, use values from the
`values` array, not values you expect to exist.
- Fields whose `count` is less than other fields' are absent in some records:
guard DSL with `is_present($field)`.
## 3. Validate — check DSL before spending a run
Before any `run` that includes `put` or `filter`, call `validate_dsl` with the
expression. Cost: parse-only, no data read. On `valid: false`, the `error`
document has `kind`, `hint`, and `did_you_mean` — apply the hint, don't
re-guess syntax.
## 4. Run — and read errors structurally
Call `run` with argv as a list, one element per shell word (no shell quoting):
{"args": ["--icsv", "--ojson", "cat", "data.csv"]}
Command-line shape rules that prevent most argv errors:
- Main flags (I/O formats etc.) come **before** the verb: `mlr --icsv sort -f name f.csv`.
- Format shorthands: `--icsv --ojson` (separate in/out), `--csv`/`--c2j` etc. (combined).
- Chain verbs with `then`: `["--icsv", "sort", "-f", "k", "then", "head", "-n", "3", "f.csv"]`.
- If a field value being compared in `filter` might collide with a verb flag,
end verb flags with `--` before filenames.
- Inline data goes in `stdin_text`; files go at the end of `args`.
On failure, `exit_code` is nonzero and `error` (when present) carries `kind`,
`hint`, and `did_you_mean``hint` is often a corrected command line; prefer
executing it over reasoning from the message. `stdout_truncated: true` means
the output exceeded the server's cap: narrow the query (e.g. `head`, `cut`)
rather than re-running the same command.
## Notes
- `run` cannot execute external commands (DSL `system`/`exec`, piped
redirects, `--prepipe`) unless the server was started with `--allow-shell`;
such calls fail cleanly. It **can** write files via `tee`, `split`, and DSL
output redirects — treat it as a write-capable tool.
- Long inputs: prefer `describe_data` + targeted verbs over dumping whole
files through `run`.
- One record format in, another out: Miller is format-to-format; there is no
separate conversion step.
</pre>
That playbook is prose, not named tools, but it rests on the Miller features documented
in the [Miller AI internals](ai-support.md) page.
## What using the Miller skill looks like in practice
There's no server status to check and no tool list to browse -- the skill is just text the agent
already has -- so "in practice" mostly looks like an ordinary conversation. Say you're looking at
[example.csv](example.csv) for the first time:
> **You:** In example.csv, show me the red rows.
Without the skill, a plausible guess for the DSL is `$color == "Red"` -- and Miller silently
returns nothing for it, since the real values are lowercase. With the skill installed, the agent
runs `mlr --icsv --ojson describe example.csv` on your behalf first, sees the real value set for
`color` (`yellow`, `red`, `purple`), and only then answers:
> **Agent:** Four rows have color = red: rows 2, 3, 4, and 6.
The full worked version of this example, including the exact commands run at each step, is in
[Miller and AI](ai.md#before-and-after-a-first-session-with-the-skill-installed).
## A note on sandboxing
The [MCP server](mcp-server.md) enforces a sandbox by construction: subprocesses it spawns run with
`MLR_NO_SHELL=1` unless you start it with `--allow-shell`, so an agent-constructed command line
can't execute external commands even if the agent wanted it to.
The skill file has no equivalent enforcement. It's advisory text, not a wrapper around subprocess
execution -- nothing stops an agent from running `mlr put 'end{print system("whatever")}'` with
your full shell permissions if it decides to. If you want that guarantee with the skill alone,
set the `MLR_NO_SHELL` [environment variable](reference-main-env-vars.md) yourself (or pass
`--no-shell` explicitly), rather than relying on the playbook text for isolation. If you want the
enforced version, register the [MCP server](mcp-server.md) instead of, or alongside, the skill.

109
docs/src/agent-skill.md.in Normal file
View file

@ -0,0 +1,109 @@
# The Miller Agent Skill
As of Miller version 6.20, released in July 2026, there are two main ways to get your AI to know
about a software tool (Miller, or others): **agent skills**, and [**MCP**](mcp-server.md). (See
[Miller and AI](ai.md) for an introduction.)
Miller ships a built-in [Agent Skill](https://www.anthropic.com/news/skills) -- a single `SKILL.md`
file -- inside the `mlr` executable, so agents that read skills directly from disk (Claude Code,
and other tools that support the Agent Skills format) can discover and drive Miller without
scraping help text or guessing at flags.
The skill is plain markdown with a YAML frontmatter header, placed where your agent already looks
for skills. The agent reads it into context once, the same way it reads any other instructions, and
from then on it runs `mlr` commands via whatever shell-executing tool it already has.
Here's what the skill file looks like:
GENMD-RUN-COMMAND
mlr skill print | head -n 15
GENMD-EOF
For more background on the `mlr` commands the agent runs on your behalf, please see
[Miller AI internals](ai-support.md).
## Setup
Write the skill file to Claude Code's personal skills directory (do this before starting your
`claude` session):
GENMD-CARDIFY-HIGHLIGHT-ONE
mlr skill install ~/.claude/skills/miller
Wrote /Users/kerl/.claude/skills/miller/SKILL.md
GENMD-EOF
For Codex and Gemini:
GENMD-CARDIFY-HIGHLIGHT-ONE
mlr skill install ~/.agents/skills/miller
GENMD-EOF
With no argument, `install` writes to `.claude/skills/miller/SKILL.md` under the current directory
instead. This is handy for a project-scoped skill checked into that project's repo rather than one
installed for every project on your machine:
GENMD-CARDIFY-HIGHLIGHT-ONE
mlr skill install
Wrote .claude/skills/miller/SKILL.md
GENMD-EOF
There's no "uninstall" subcommand, since `install` only ever writes one plain file. Removing it is
an ordinary file operation:
GENMD-CARDIFY-HIGHLIGHT-ONE
rm -rf ~/.claude/skills/miller
GENMD-EOF
Then -- just interact with your agent as always! When you say something like `describe the data file example.csv`,
the agent will already know how to use Miller to help answer that question.
## What the Miller skill maps to
You don't have to type `skill` or anything else special in your agent session: rather you've
empowered the agent to discover things about Miller for itself. But if you're curious what's
actually placed in front of it:
GENMD-RUN-COMMAND
mlr skill --help
GENMD-EOF
And here's the file itself -- the whole thing, not an excerpt, since this and nothing else is what
the agent has to go on:
GENMD-RUN-COMMAND
mlr skill print
GENMD-EOF
That playbook is prose, not named tools, but it rests on the Miller features documented
in the [Miller AI internals](ai-support.md) page.
## What using the Miller skill looks like in practice
There's no server status to check and no tool list to browse -- the skill is just text the agent
already has -- so "in practice" mostly looks like an ordinary conversation. Say you're looking at
[example.csv](example.csv) for the first time:
> **You:** In example.csv, show me the red rows.
Without the skill, a plausible guess for the DSL is `$color == "Red"` -- and Miller silently
returns nothing for it, since the real values are lowercase. With the skill installed, the agent
runs `mlr --icsv --ojson describe example.csv` on your behalf first, sees the real value set for
`color` (`yellow`, `red`, `purple`), and only then answers:
> **Agent:** Four rows have color = red: rows 2, 3, 4, and 6.
The full worked version of this example, including the exact commands run at each step, is in
[Miller and AI](ai.md#before-and-after-a-first-session-with-the-skill-installed).
## A note on sandboxing
The [MCP server](mcp-server.md) enforces a sandbox by construction: subprocesses it spawns run with
`MLR_NO_SHELL=1` unless you start it with `--allow-shell`, so an agent-constructed command line
can't execute external commands even if the agent wanted it to.
The skill file has no equivalent enforcement. It's advisory text, not a wrapper around subprocess
execution -- nothing stops an agent from running `mlr put 'end{print system("whatever")}'` with
your full shell permissions if it decides to. If you want that guarantee with the skill alone,
set the `MLR_NO_SHELL` [environment variable](reference-main-env-vars.md) yourself (or pass
`--no-shell` explicitly), rather than relying on the playbook text for isolation. If you want the
enforced version, register the [MCP server](mcp-server.md) instead of, or alongside, the skill.

298
docs/src/ai-support.md Normal file
View file

@ -0,0 +1,298 @@
<!--- PLEASE DO NOT EDIT DIRECTLY. EDIT THE .md.in FILE PLEASE. --->
<div>
<span class="quicklinks">
Quick links:
&nbsp;
<a class="quicklink" href="../reference-main-flag-list/index.html">Flags</a>
&nbsp;
<a class="quicklink" href="../reference-verbs/index.html">Verbs</a>
&nbsp;
<a class="quicklink" href="../reference-dsl-builtin-functions/index.html">Functions</a>
&nbsp;
<a class="quicklink" href="../glossary/index.html">Glossary</a>
&nbsp;
<a class="quicklink" href="../release-docs/index.html">Release docs</a>
</span>
</div>
# Miller AI internals
When you use the [Miller agent skill](agent-skill.md) or the [Miller MCP server](mcp-server.md),
here are the `mlr` subcommands your agent runs on your behalf to acquire support. (See also [Miller
and AI](ai.md) for an introduction.)
The new Miller subcommands as of version 6.20 allow agents to **discover** information about how to
use Miller, **constrain** attempted solutions to match the data, **validate** Miller commands before
running them, **run** them, and robustly **recover** from errors.
If you like, you can run these subcommands yourself, although you don't need to. These AI-support
subcommands are documented here for transparency.
## Discover: the machine-readable catalog
This is the machine-readable catalog of [verbs](reference-verbs.md), [DSL
functions](reference-dsl-builtin-functions.md), [flags](reference-main-flag-list.md), and
[keywords](reference-dsl-variables.md#keywords-for-filter-and-put), plus intent-to-capability
routing.
These are implemented by `mlr help --as-json` and `mlr which`.
`mlr help --as-json` emits Miller's entire help catalog as one JSON document.
The `--index` form is the cheap first call -- every capability with a
one-line summary (here trimmed, and then counted, using Miller itself):
<pre class="pre-highlight-in-pair">
<b>mlr help --as-json --index | mlr --json head -n 2</b>
</pre>
<pre class="pre-non-highlight-in-pair">
[
{
"kind": "verb",
"name": "altkv",
"summary": "Given fields with values of the form a,b,c,d,e,f emits a=b,c=d,e=f pairs."
},
{
"kind": "verb",
"name": "bar",
"summary": "Replaces a numeric field with a number of asterisks, allowing for cheesy"
}
]
</pre>
<pre class="pre-highlight-in-pair">
<b>mlr help --as-json --index | mlr --json count</b>
</pre>
<pre class="pre-non-highlight-in-pair">
[
{
"count": 661
}
]
</pre>
From the index, an agent drills into full entries one at a time: `mlr help verb sort --as-json`,
`mlr help function splitax --as-json`, `mlr help flag --ifs --as-json`, `mlr help keyword ENV
--as-json` -- each accepting one or more names. A verb entry carries a structured option list --
flag, argument placeholder, type -- alongside the familiar usage text:
<pre class="pre-highlight-in-pair">
<b>mlr help verb decimate --as-json</b>
</pre>
<pre class="pre-non-highlight-in-pair">
[
{
"name": "decimate",
"summary": "Passes through one of every n records, optionally by category.",
"ignores_input": false,
"options": [
{
"flag": "-b",
"type": "bool",
"desc": "Decimate by printing first of every n."
},
{
"flag": "-e",
"type": "bool",
"desc": "Decimate by printing last of every n (default)."
},
{
"flag": "-g",
"arg": "{a,b,c}",
"type": "csv-list",
"desc": "Optional group-by-field names for decimate counts, e.g. a,b,c."
},
{
"flag": "-n",
"arg": "{n}",
"type": "int",
"desc": "Decimation factor (default 10)."
}
],
"usage_text": "Usage: mlr decimate [options]\nPasses through one of every n records, optionally by category.\nOptions:\n-b Decimate by printing first of every n.\n-e Decimate by printing last of every n (default).\n-g {a,b,c} Optional group-by-field names for decimate counts, e.g. a,b,c.\n-n {n} Decimation factor (default 10).\n-h|--help Show this message."
}
]
</pre>
Note that `usage_text` -- what `mlr decimate --help` prints -- is rendered *from* the same
structured options, so the human help and the machine help cannot drift apart. Function entries
carry name, class, arity, help, and examples; the examples across the whole catalog are exercised by
Miller's test suite, so they never rot.
Three properties make the catalog cheap to use:
* _It's a perfect cache key._ Every document carries `mlr_version` and
`catalog_schema_version`. Miller is a static binary, so the catalog changes
only when the binary does: fetch once, cache forever, re-fetch on a version
bump. No TTLs.
* _It's deterministic._ One document per invocation, sorted entries, no
colorization -- stable for diffing and for prompt caches.
* _It's opt-in twice over._ Per-call via `--as-json`, or set-once via a
truthy `MLR_HELP_JSON` environment variable.
For routing an *intent* to a capability -- the reverse of browsing -- `mlr
which` returns ranked candidates:
<pre class="pre-highlight-in-pair">
<b>mlr which "join two files on a key" | mlr --json head -n 2</b>
</pre>
<pre class="pre-non-highlight-in-pair">
[
{
"kind": "verb",
"name": "join",
"score": 25,
"summary": "Joins records from specified left file name with records from all file names"
},
{
"kind": "function",
"name": "joink",
"score": 25,
"summary": "Makes string from map/array keys. First argument is map/array; second is separator string."
}
]
</pre>
Its exit code signals confidence -- 0 when a query word matched a
capability's name, 2 when it didn't -- so a harness can branch on status
without parsing anything.
## Constrain: the tool's shape, and the data's shape
This shows field names, types, cardinality, and value domains for your actual input data.
It's implemented by `mlr describe`.
Agents don't just hallucinate flags; they hallucinate *values*. Miller attacks that from both sides.
Where an option's domain is fixed by the binary, the catalog says so:
`type` is `enum` and `values` is the complete list. Here's one option of the
[summary](reference-verbs.md#summary) verb, extracted from the catalog --
using Miller to query Miller:
<pre class="pre-highlight-in-pair">
<b>mlr help verb summary --as-json | mlr --json put -q 'emit $options[1]'</b>
</pre>
<pre class="pre-non-highlight-in-pair">
[
{
"flag": "-a",
"arg": "{mean,sum,etc.}",
"type": "enum",
"desc": "Use only the specified summarizers.",
"values": ["field_type", "count", "null_count", "distinct_count", "mode", "sum", "mean", "stddev", "var", "skewness", "minlen", "maxlen", "min", "p25", "median", "p75", "max", "iqr", "lof", "lif", "uif", "uof"]
}
]
</pre>
Where the domain depends on your *data* -- which fields exist, what values
`filter` could compare against, what to pass to `-g` -- the
[describe](reference-verbs.md#describe) verb profiles the input in one pass:
per field, the types seen, counts, cardinality, min/max, and (for
low-cardinality fields) every distinct value:
<pre class="pre-highlight-in-pair">
<b>mlr --icsv --ojson describe then head -n 2 example.csv</b>
</pre>
<pre class="pre-non-highlight-in-pair">
[
{
"field_name": "color",
"types": {
"string": 10
},
"count": 10,
"null_count": 0,
"distinct_count": 3,
"min": "purple",
"max": "yellow",
"values": ["yellow", "red", "purple"]
},
{
"field_name": "shape",
"types": {
"string": 10
},
"count": 10,
"null_count": 0,
"distinct_count": 3,
"min": "circle",
"max": "triangle",
"values": ["triangle", "square", "circle"]
}
]
</pre>
The catalog is the *tool's* shape; `describe` is the *data's* shape. An
agent that consults both has nothing left to guess.
## Validate: check DSL before spending a run
This lets the agent parse and type-check a DSL expression before reading any input files.
It's implemented by `mlr put --explain` and `mlr filter --explain`.
`mlr put --explain` (likewise `mlr filter --explain`) parses and type-checks
an expression, then exits -- without opening any input at all:
<pre class="pre-highlight-in-pair">
<b>mlr put --explain '$z = $x + $y'</b>
</pre>
<pre class="pre-non-highlight-in-pair">
mlr put: DSL expression is valid.
</pre>
<pre class="pre-highlight-in-pair">
<b>mlr put --explain '$z = $x +'</b>
</pre>
<pre class="pre-non-highlight-in-pair">
mlr: cannot parse DSL expression.
mlr: parse error: unexpected EOF ("")
</pre>
## Run and recover: errors as data
Agents are instructed to run Miller commands using `mlr` with the `--errors-json` flag so that a
failure comes back as a structured document instead of prose.
With `--errors-json` (or `MLR_ERRORS_JSON=true` environment variable), errors arrive as a structured
document. The `kind` field gives an agent something to branch on; `hint` is a runnable next step,
not a sentence; and `did_you_mean` is computed against the same catalog the agent discovered from,
closing the self-correction loop:
<pre class="pre-highlight-in-pair">
<b>mlr --errors-json --icsv sorted -f shape example.csv</b>
</pre>
<pre class="pre-non-highlight-in-pair">
{
"error": "mlr: verb \"sorted\" not found. Please use \"mlr -l\" for a list.",
"kind": "unknown-verb",
"token": "sorted",
"hint": "Run 'mlr -l' for a list of verbs, or 'mlr help verb \u003cname\u003e' for details.",
"did_you_mean": [
"sort"
]
}
</pre>
And since Miller's DSL includes [system and exec](shell-commands.md), there's a sandbox:
`--no-shell` (or a truthy `MLR_NO_SHELL` environment variable) disables all external-command
execution -- the DSL `system` and `exec` functions, piped redirects, and `--prepipe` fail cleanly:
<pre class="pre-highlight-in-pair">
<b>mlr --no-shell -n put 'end{print system("hostname")}'</b>
</pre>
<pre class="pre-non-highlight-in-pair">
(error)
</pre>
## Summary
A typical agent profile sets all three environment variables once:
<pre class="pre-non-highlight-non-pair">
export MLR_HELP_JSON=1 # help/catalog output as JSON
export MLR_ERRORS_JSON=1 # errors as structured JSON
export MLR_NO_SHELL=1 # no external-command execution
</pre>
Put together, the sections above are a loop -- discover, constrain, validate, run -- where each step
feeds the next, and failures route back with structure instead of prose.

151
docs/src/ai-support.md.in Normal file
View file

@ -0,0 +1,151 @@
# Miller AI internals
When you use the [Miller agent skill](agent-skill.md) or the [Miller MCP server](mcp-server.md),
here are the `mlr` subcommands your agent runs on your behalf to acquire support. (See also [Miller
and AI](ai.md) for an introduction.)
The new Miller subcommands as of version 6.20 allow agents to **discover** information about how to
use Miller, **constrain** attempted solutions to match the data, **validate** Miller commands before
running them, **run** them, and robustly **recover** from errors.
If you like, you can run these subcommands yourself, although you don't need to. These AI-support
subcommands are documented here for transparency.
## Discover: the machine-readable catalog
This is the machine-readable catalog of [verbs](reference-verbs.md), [DSL
functions](reference-dsl-builtin-functions.md), [flags](reference-main-flag-list.md), and
[keywords](reference-dsl-variables.md#keywords-for-filter-and-put), plus intent-to-capability
routing.
These are implemented by `mlr help --as-json` and `mlr which`.
`mlr help --as-json` emits Miller's entire help catalog as one JSON document.
The `--index` form is the cheap first call -- every capability with a
one-line summary (here trimmed, and then counted, using Miller itself):
GENMD-RUN-COMMAND
mlr help --as-json --index | mlr --json head -n 2
GENMD-EOF
GENMD-RUN-COMMAND
mlr help --as-json --index | mlr --json count
GENMD-EOF
From the index, an agent drills into full entries one at a time: `mlr help verb sort --as-json`,
`mlr help function splitax --as-json`, `mlr help flag --ifs --as-json`, `mlr help keyword ENV
--as-json` -- each accepting one or more names. A verb entry carries a structured option list --
flag, argument placeholder, type -- alongside the familiar usage text:
GENMD-RUN-COMMAND
mlr help verb decimate --as-json
GENMD-EOF
Note that `usage_text` -- what `mlr decimate --help` prints -- is rendered *from* the same
structured options, so the human help and the machine help cannot drift apart. Function entries
carry name, class, arity, help, and examples; the examples across the whole catalog are exercised by
Miller's test suite, so they never rot.
Three properties make the catalog cheap to use:
* _It's a perfect cache key._ Every document carries `mlr_version` and
`catalog_schema_version`. Miller is a static binary, so the catalog changes
only when the binary does: fetch once, cache forever, re-fetch on a version
bump. No TTLs.
* _It's deterministic._ One document per invocation, sorted entries, no
colorization -- stable for diffing and for prompt caches.
* _It's opt-in twice over._ Per-call via `--as-json`, or set-once via a
truthy `MLR_HELP_JSON` environment variable.
For routing an *intent* to a capability -- the reverse of browsing -- `mlr
which` returns ranked candidates:
GENMD-RUN-COMMAND
mlr which "join two files on a key" | mlr --json head -n 2
GENMD-EOF
Its exit code signals confidence -- 0 when a query word matched a
capability's name, 2 when it didn't -- so a harness can branch on status
without parsing anything.
## Constrain: the tool's shape, and the data's shape
This shows field names, types, cardinality, and value domains for your actual input data.
It's implemented by `mlr describe`.
Agents don't just hallucinate flags; they hallucinate *values*. Miller attacks that from both sides.
Where an option's domain is fixed by the binary, the catalog says so:
`type` is `enum` and `values` is the complete list. Here's one option of the
[summary](reference-verbs.md#summary) verb, extracted from the catalog --
using Miller to query Miller:
GENMD-RUN-COMMAND
mlr help verb summary --as-json | mlr --json put -q 'emit $options[1]'
GENMD-EOF
Where the domain depends on your *data* -- which fields exist, what values
`filter` could compare against, what to pass to `-g` -- the
[describe](reference-verbs.md#describe) verb profiles the input in one pass:
per field, the types seen, counts, cardinality, min/max, and (for
low-cardinality fields) every distinct value:
GENMD-RUN-COMMAND
mlr --icsv --ojson describe then head -n 2 example.csv
GENMD-EOF
The catalog is the *tool's* shape; `describe` is the *data's* shape. An
agent that consults both has nothing left to guess.
## Validate: check DSL before spending a run
This lets the agent parse and type-check a DSL expression before reading any input files.
It's implemented by `mlr put --explain` and `mlr filter --explain`.
`mlr put --explain` (likewise `mlr filter --explain`) parses and type-checks
an expression, then exits -- without opening any input at all:
GENMD-RUN-COMMAND
mlr put --explain '$z = $x + $y'
GENMD-EOF
GENMD-RUN-COMMAND-TOLERATING-ERROR
mlr put --explain '$z = $x +'
GENMD-EOF
## Run and recover: errors as data
Agents are instructed to run Miller commands using `mlr` with the `--errors-json` flag so that a
failure comes back as a structured document instead of prose.
With `--errors-json` (or `MLR_ERRORS_JSON=true` environment variable), errors arrive as a structured
document. The `kind` field gives an agent something to branch on; `hint` is a runnable next step,
not a sentence; and `did_you_mean` is computed against the same catalog the agent discovered from,
closing the self-correction loop:
GENMD-RUN-COMMAND-TOLERATING-ERROR
mlr --errors-json --icsv sorted -f shape example.csv
GENMD-EOF
And since Miller's DSL includes [system and exec](shell-commands.md), there's a sandbox:
`--no-shell` (or a truthy `MLR_NO_SHELL` environment variable) disables all external-command
execution -- the DSL `system` and `exec` functions, piped redirects, and `--prepipe` fail cleanly:
GENMD-RUN-COMMAND
mlr --no-shell -n put 'end{print system("hostname")}'
GENMD-EOF
## Summary
A typical agent profile sets all three environment variables once:
GENMD-CARDIFY
export MLR_HELP_JSON=1 # help/catalog output as JSON
export MLR_ERRORS_JSON=1 # errors as structured JSON
export MLR_NO_SHELL=1 # no external-command execution
GENMD-EOF
Put together, the sections above are a loop -- discover, constrain, validate, run -- where each step
feeds the next, and failures route back with structure instead of prose.

View file

@ -16,231 +16,97 @@ Quick links:
</div>
# Miller and AI
Miller treats AI agents as first-class users. When an agent drives a
command-line tool, it fails in predictable ways: it invents flags that don't
exist, guesses values that aren't in the data, misreads error prose, and
burns whole runs discovering a typo. Miller closes off each of those failure
modes with structure:
As of version 6.20, released in July 2026, Miller supports two ways to let agents know about it:
an **agent skill** and **MCP**. Either one works -- not sure which? Start with the Miller agent skill.
* Miller's entire surface -- verbs, DSL functions, flags, keywords -- is
available as **machine-readable JSON**, so agents ground themselves in what
actually exists.
* Options with fixed domains carry their **complete value sets**, and input
data can be **profiled in one pass** -- so agents copy real values instead
of inventing them.
* DSL expressions can be **validated before running**, without reading any
input.
* **Errors are structured** -- kind, hint, did-you-mean -- so agents branch
on data rather than parsing English.
* A **sandbox flag** removes external-command execution, so an
agent-constructed command line is just data processing.
This page covers essential setup, and an example session. For more on agent skills, see [The Miller
Agent Skill](agent-skill.md); for more on MCP, see [The Miller MCP server](mcp-server.md).
Everything on this page is an ordinary command-line feature: it works from
any agent harness, system prompt, or script -- and it's equally useful for
plain shell tooling like `jq`. The [MCP server](#plug-it-in-the-mcp-server)
at the end packages it all up for MCP-speaking agents.
## Quick start
## The essentials
**To get the AI features:** install Miller 6.20 or newer ([Installing
Miller](installing-miller.md)). That's all. Everything on this page ships
inside the ordinary `mlr` binary -- there are no plugins, no separate
First, you need to **install Miller 6.20 or newer** (see [Installing Miller](installing-miller.md)).
Everything on this page ships inside the ordinary `mlr` binary -- there are no plugins, no separate
installs, no API keys, and nothing here makes network calls.
**To get your AI to use them,** pick whichever matches your setup:
* **If your agent speaks MCP** (Claude Code, Claude Desktop, Cursor, ...):
register the server -- for Claude Code that's `claude mcp add miller -- mlr
mcp` -- and you're done. The tools describe themselves, and the server
ships its own instructions and playbook, so you usually don't need to say
anything special; if the agent doesn't reach for them, a nudge like "use
the Miller tools" suffices. Details in [The MCP server](mcp-server.md).
* **If your agent just runs shell commands** (a system prompt, a
`CLAUDE.md`, Cursor rules, a script harness): paste this standing
instruction into its context:
<pre class="pre-non-highlight-non-pair">
Miller (mlr) is installed for processing CSV/TSV/JSON/etc. data. When
constructing mlr commands:
1. Discover: `mlr help --as-json --index` lists every verb/function/flag;
`mlr which "&lt;intent&gt;"` routes a goal to the right one; `mlr help
verb &lt;name&gt; --as-json` gives full details. Never invent flag or
function names.
2. Constrain: `mlr --icsv --ojson describe &lt;file&gt;` (or --ijson etc.)
shows the data's fields, types, and values. Copy names and values from it
rather than guessing them.
3. Validate: check DSL expressions with `mlr put --explain '&lt;expr&gt;'`
before using them.
4. Run with `--errors-json`; on failure, correct using the error's kind,
hint, and did_you_mean rather than re-guessing.
</pre>
A fuller, ready-made version of that lesson ships in the Miller source
tree at
[pkg/terminals/mcp/SKILL.md](https://github.com/johnkerl/miller/blob/main/pkg/terminals/mcp/SKILL.md),
in Agent Skill format -- suitable for dropping into e.g. a
`.claude/skills/miller/` directory as-is.
The rest of this page is what those instructions rest on, feature by
feature.
## Discover: the machine-readable catalog
`mlr help --as-json` emits Miller's entire help catalog as one JSON document.
The `--index` form is the cheap first call -- every capability with a
one-line summary (here trimmed, and then counted, using Miller itself):
To install as **skill file** for Claude:
<pre class="pre-highlight-in-pair">
<b>mlr help --as-json --index | mlr --json head -n 2</b>
<b>mlr skill install ~/.claude/skills/miller</b>
</pre>
<pre class="pre-non-highlight-in-pair">
[
{
"kind": "verb",
"name": "altkv",
"summary": "Given fields with values of the form a,b,c,d,e,f emits a=b,c=d,e=f pairs."
},
{
"kind": "verb",
"name": "bar",
"summary": "Replaces a numeric field with a number of asterisks, allowing for cheesy"
}
]
Wrote /Users/kerl/.claude/skills/miller/SKILL.md
</pre>
For Codex or Gemini:
<pre class="pre-highlight-non-pair">
<b>mlr skill install ~/.agents/skills/miller</b>
</pre>
If you prefer to use **MCP**:
<pre class="pre-highlight-in-pair">
<b>mlr help --as-json --index | mlr --json count</b>
<b>claude mcp add miller -- mlr mcp</b>
</pre>
<pre class="pre-non-highlight-in-pair">
[
{
"count": 661
}
]
Added stdio MCP server miller with command: mlr mcp to local config
File modified: /Users/kerl/.claude.json [project: /Users/kerl/git/johnkerl/miller]
</pre>
From the index, an agent drills into full entries one at a time: `mlr help
verb sort --as-json`, `mlr help function splitax --as-json`, `mlr help flag
--ifs --as-json`, `mlr help keyword ENV --as-json` -- each accepting one or
more names. A verb entry carries a structured option list -- flag, argument
placeholder, type -- alongside the familiar usage text:
<pre class="pre-highlight-non-pair">
<b>codex mcp add miller -- mlr mcp</b>
</pre>
<pre class="pre-highlight-non-pair">
<b>gemini mcp add miller mlr mcp</b>
</pre>
## Before and after: a first session with the skill installed
If you're new to Miller, or you've used Miller before but this is your first time on 6.20 or newer,
here's a worked example: install the skill, then watch what changes about talking to your AI
assistant.
One thing to be clear on before the example: you never type `mlr` yourself in this section. You
type plain English to your agent, same as always. Every `mlr` command shown below is the agent's
*own* work -- what it runs on your behalf, in the background, to answer you. They're printed here
so you can see exactly what changes, not because you'd type them.
### Before: an agent guessing at your data
Say you're looking at [example.csv](example.csv) for the first time. You type this, and nothing
else, to your AI assistant:
> **You:** In example.csv, show me the red rows.
Without the skill, a reasonable-sounding guess for the DSL might be `$color == "Red"`. Here's that
guess, run exactly as the agent would run it, behind the scenes, on your machine:
<pre class="pre-highlight-non-pair">
<b>mlr --csv filter '$color == "Red"' example.csv</b>
</pre>
Nothing comes back -- no error, no rows, no warning, exit code 0. And here's the trap: the agent
still owes you an answer, so it turns that silence into a sentence:
> **Agent:** I checked example.csv and there aren't any rows where color is red.
That's wrong -- there are four -- but it *reads* like a fact, because a wrong guess about *your
data*, unlike a wrong flag or function name, doesn't look like a failure on Miller's end. It looks
like an empty result, which could just as easily have been true.
### After: an agent that checks first
Same question, word for word, with the skill installed:
> **You:** In example.csv, show me the red rows.
The skill's playbook puts a step between your question and any guesswork: "constrain -- learn the
data before touching it." So before writing any comparison, the agent runs a `describe`, again
invisibly, on your behalf:
<pre class="pre-highlight-in-pair">
<b>mlr help verb decimate --as-json</b>
</pre>
<pre class="pre-non-highlight-in-pair">
[
{
"name": "decimate",
"summary": "Passes through one of every n records, optionally by category.",
"ignores_input": false,
"options": [
{
"flag": "-b",
"type": "bool",
"desc": "Decimate by printing first of every n."
},
{
"flag": "-e",
"type": "bool",
"desc": "Decimate by printing last of every n (default)."
},
{
"flag": "-g",
"arg": "{a,b,c}",
"type": "csv-list",
"desc": "Optional group-by-field names for decimate counts, e.g. a,b,c."
},
{
"flag": "-n",
"arg": "{n}",
"type": "int",
"desc": "Decimation factor (default 10)."
}
],
"usage_text": "Usage: mlr decimate [options]\nPasses through one of every n records, optionally by category.\nOptions:\n-b Decimate by printing first of every n.\n-e Decimate by printing last of every n (default).\n-g {a,b,c} Optional group-by-field names for decimate counts, e.g. a,b,c.\n-n {n} Decimation factor (default 10).\n-h|--help Show this message."
}
]
</pre>
Note that `usage_text` -- what `mlr decimate --help` prints -- is rendered
*from* the same structured options, so the human help and the machine help
cannot drift apart. Function entries carry name, class, arity, help, and
examples; the examples across the whole catalog are exercised by Miller's
test suite, so they never rot.
Three properties make the catalog cheap to use:
* **It's a perfect cache key.** Every document carries `mlr_version` and
`catalog_schema_version`. Miller is a static binary, so the catalog changes
only when the binary does: fetch once, cache forever, re-fetch on a version
bump. No TTLs.
* **It's deterministic.** One document per invocation, sorted entries, no
colorization -- stable for diffing and for prompt caches.
* **It's opt-in twice over.** Per-call via `--as-json`, or set-once via a
truthy `MLR_HELP_JSON` environment variable.
For routing an *intent* to a capability -- the reverse of browsing -- `mlr
which` returns ranked candidates:
<pre class="pre-highlight-in-pair">
<b>mlr which "join two files on a key" | mlr --json head -n 2</b>
</pre>
<pre class="pre-non-highlight-in-pair">
[
{
"kind": "verb",
"name": "join",
"score": 25,
"summary": "Joins records from specified left file name with records from all file names"
},
{
"kind": "function",
"name": "joink",
"score": 25,
"summary": "Makes string from map/array keys. First argument is map/array; second is separator string."
}
]
</pre>
Its exit code signals confidence -- 0 when a query word matched a
capability's name, 2 when it didn't -- so a harness can branch on status
without parsing anything.
## Constrain: the tool's shape, and the data's shape
Agents don't just hallucinate flags; they hallucinate *values*. Miller
attacks that from both sides.
Where an option's domain is fixed by the binary, the catalog says so:
`type` is `enum` and `values` is the complete list. Here's one option of the
[summary](reference-verbs.md#summary) verb, extracted from the catalog --
using Miller to query Miller:
<pre class="pre-highlight-in-pair">
<b>mlr help verb summary --as-json | mlr --json put -q 'emit $options[1]'</b>
</pre>
<pre class="pre-non-highlight-in-pair">
[
{
"flag": "-a",
"arg": "{mean,sum,etc.}",
"type": "enum",
"desc": "Use only the specified summarizers.",
"values": ["field_type", "count", "null_count", "distinct_count", "mode", "sum", "mean", "stddev", "var", "skewness", "minlen", "maxlen", "min", "p25", "median", "p75", "max", "iqr", "lof", "lif", "uif", "uof"]
}
]
</pre>
Where the domain depends on your *data* -- which fields exist, what values
`filter` could compare against, what to pass to `-g` -- the
[describe](reference-verbs.md#describe) verb profiles the input in one pass:
per field, the types seen, counts, cardinality, min/max, and (for
low-cardinality fields) every distinct value:
<pre class="pre-highlight-in-pair">
<b>mlr --icsv --ojson describe then head -n 2 example.csv</b>
<b>mlr --icsv --ojson describe example.csv | mlr --json filter '$field_name == "color"'</b>
</pre>
<pre class="pre-non-highlight-in-pair">
[
@ -255,105 +121,76 @@ low-cardinality fields) every distinct value:
"min": "purple",
"max": "yellow",
"values": ["yellow", "red", "purple"]
},
{
"field_name": "shape",
"types": {
"string": 10
},
"count": 10,
"null_count": 0,
"distinct_count": 3,
"min": "circle",
"max": "triangle",
"values": ["triangle", "square", "circle"]
}
]
</pre>
The catalog is the *tool's* shape; `describe` is the *data's* shape. An
agent that consults both has nothing left to guess.
## Validate: check DSL before spending a run
`mlr put --explain` (likewise `mlr filter --explain`) parses and type-checks
an expression, then exits -- without opening any input at all:
That's the real value set for `color`: lowercase, three values, no `"Red"` anywhere. The agent
corrects its own comparison before running anything user-facing:
<pre class="pre-highlight-in-pair">
<b>mlr put --explain '$z = $x + $y'</b>
<b>mlr --icsv --opprint filter '$color == "red"' example.csv</b>
</pre>
<pre class="pre-non-highlight-in-pair">
mlr put: DSL expression is valid.
color shape flag k index quantity rate
red square true 2 15 79.2778 0.0130
red circle true 3 16 13.8103 2.9010
red square false 4 48 77.5542 7.4670
red square false 6 64 77.1991 9.5310
</pre>
## Run and recover: errors as data
-- and only now replies to you:
With `--errors-json` (or a truthy `MLR_ERRORS_JSON` environment variable),
errors arrive as a structured document. The `kind` field gives an agent
something to branch on; `hint` is a runnable next step, not a sentence; and
`did_you_mean` is computed against the same catalog the agent discovered
from, closing the self-correction loop:
> **Agent:** Four rows have color = red: rows 2, 3, 4, and 6 (all square or circle, quantity
> ranging from about 13.8 to 79.3).
<pre class="pre-highlight-in-pair">
<b>mlr --errors-json --icsv sortt -f shape example.csv</b>
</pre>
<pre class="pre-non-highlight-in-pair">
{
"error": "mlr: verb \"sortt\" not found. Please use \"mlr -l\" for a list.",
"kind": "unknown-verb",
"token": "sortt",
"hint": "Run 'mlr -l' for a list of verbs, or 'mlr help verb \u003cname\u003e' for details.",
"did_you_mean": [
"sort"
]
}
</pre>
Same question, same data, same underlying `mlr` binary -- the only thing that changed is that the
agent looked before it leapt, and you never saw the intermediate `describe` unless you asked to.
That one habit, *check the data before writing a comparison*, is the skill in miniature; the rest
of the playbook applies the same idea to verb and function names (discover), DSL syntax (validate),
and error messages (recover) -- see [Miller AI internals](ai-support.md) for how each of those works.
And since Miller's DSL includes [system and exec](shell-commands.md), there's
a sandbox: `--no-shell` (or a truthy `MLR_NO_SHELL` environment variable)
disables all external-command execution -- the DSL `system` and `exec`
functions, piped redirects, and `--prepipe` fail cleanly:
## Why AI support
<pre class="pre-highlight-in-pair">
<b>mlr --no-shell -n put 'end{print system("hostname")}'</b>
</pre>
<pre class="pre-non-highlight-in-pair">
(error)
</pre>
Miller treats AI agents as first-class users. When an agent drives a command-line tool, the agent
can fail in predictable ways: it invents flags that don't exist, guesses values that aren't in the
data, misreads error prose, and burns whole runs discovering a typo. Miller closes off each of those
failure modes with the following structure:
A typical agent profile sets all three environment variables once:
* Miller's entire surface -- verbs, DSL functions, flags, keywords -- is
available as **machine-readable JSON**, so agents ground themselves in what
actually exists.
* Options with fixed domains carry their **complete value sets**, and input
data can be **profiled in one pass**, so that agents copy real values instead
of inventing them.
* DSL expressions can be **validated before running**, without reading any
input.
* **Errors are structured** -- kind, hint, did-you-mean -- so agents branch
on data rather than parsing English.
* A **sandbox flag** removes external-command execution, so an
agent-constructed command line is just data processing.
<pre class="pre-non-highlight-non-pair">
export MLR_HELP_JSON=1 # help/catalog output as JSON
export MLR_ERRORS_JSON=1 # errors as structured JSON
export MLR_NO_SHELL=1 # no external-command execution
</pre>
Every one of those is an ordinary command-line feature, documented in [Miller AI
internals](ai-support.md): each works from any agent harness, system prompt, or script.
Put together, the sections above are a loop -- discover, constrain,
validate, run -- where each step feeds the next and failures route back with
structure instead of prose.
## Skill file or MCP: which should you use?
## Plug it in: the MCP server
For day one, the short version: start with the skill; add MCP later if you want it. They aren't
exclusive; nothing stops you running both.
If your agent speaks the [Model Context
Protocol](https://modelcontextprotocol.io) -- Claude Code, Claude Desktop,
Cursor, and many others -- everything above is one line away. For Claude
Code:
**Miller agent skill file:**
<pre class="pre-highlight-in-pair">
<b>claude mcp add miller -- mlr mcp</b>
</pre>
- Plus: One command, one static file -- no process, no client registration, nothing to reconnect.
- Plus: Works with any agent that reads Agent Skills from disk, not just MCP clients.
- Minus: No enforcement: it's advisory text, so no automatic `--no-shell` sandbox, no output caps or timeouts.
- Minus: The agent parses plain `mlr` text output and exit codes itself -- no structured JSON per call.
That's the whole setup. The server's five tools are exactly the features on
this page -- `list_capabilities` and `which` for discovery, `describe_data`
to constrain, `validate_dsl` to validate, and `run` (sandboxed with
`--no-shell` by default) to execute -- plus a shipped playbook, as MCP prompt
and resource, teaching the agent the loop. Then just talk to your agent
about your data:
**Miller MCP server:**
* "Which fields in `data.csv` have missing values?"
* "Convert this CSV to JSON, keeping only rows where status is active."
* "Join `a.csv` and `b.csv` on id, and give me the mean rate per group."
- Plus: Structured typed calls in, structured JSON back -- no text-parsing on the agent's side.
- Plus: Sandboxed by default (`MLR_NO_SHELL=1`), output-capped, timeout-guarded.
- Minus: One more moving part: per-client registration, plus a subprocess to spawn and reconnect each session.
- Minus: Only helps agents that actually speak MCP.
See [The MCP server](mcp-server.md) for the full tool reference and server
options.
In one line: the skill is less setup and the most portable, with weaker guarantees; MCP is a bit
more setup, with stronger guarantees, for a narrower set of clients.

View file

@ -1,16 +1,124 @@
# Miller and AI
Miller treats AI agents as first-class users. When an agent drives a
command-line tool, it fails in predictable ways: it invents flags that don't
exist, guesses values that aren't in the data, misreads error prose, and
burns whole runs discovering a typo. Miller closes off each of those failure
modes with structure:
As of version 6.20, released in July 2026, Miller supports two ways to let agents know about it:
an **agent skill** and **MCP**. Either one works -- not sure which? Start with the Miller agent skill.
This page covers essential setup, and an example session. For more on agent skills, see [The Miller
Agent Skill](agent-skill.md); for more on MCP, see [The Miller MCP server](mcp-server.md).
## Quick start
First, you need to **install Miller 6.20 or newer** (see [Installing Miller](installing-miller.md)).
Everything on this page ships inside the ordinary `mlr` binary -- there are no plugins, no separate
installs, no API keys, and nothing here makes network calls.
To install as **skill file** for Claude:
GENMD-CARDIFY-HIGHLIGHT-ONE
mlr skill install ~/.claude/skills/miller
Wrote /Users/kerl/.claude/skills/miller/SKILL.md
GENMD-EOF
For Codex or Gemini:
GENMD-CARDIFY-HIGHLIGHT-ONE
mlr skill install ~/.agents/skills/miller
GENMD-EOF
If you prefer to use **MCP**:
GENMD-CARDIFY-HIGHLIGHT-ONE
claude mcp add miller -- mlr mcp
Added stdio MCP server miller with command: mlr mcp to local config
File modified: /Users/kerl/.claude.json [project: /Users/kerl/git/johnkerl/miller]
GENMD-EOF
GENMD-CARDIFY-HIGHLIGHT-ONE
codex mcp add miller -- mlr mcp
GENMD-EOF
GENMD-CARDIFY-HIGHLIGHT-ONE
gemini mcp add miller mlr mcp
GENMD-EOF
## Before and after: a first session with the skill installed
If you're new to Miller, or you've used Miller before but this is your first time on 6.20 or newer,
here's a worked example: install the skill, then watch what changes about talking to your AI
assistant.
One thing to be clear on before the example: you never type `mlr` yourself in this section. You
type plain English to your agent, same as always. Every `mlr` command shown below is the agent's
*own* work -- what it runs on your behalf, in the background, to answer you. They're printed here
so you can see exactly what changes, not because you'd type them.
### Before: an agent guessing at your data
Say you're looking at [example.csv](example.csv) for the first time. You type this, and nothing
else, to your AI assistant:
> **You:** In example.csv, show me the red rows.
Without the skill, a reasonable-sounding guess for the DSL might be `$color == "Red"`. Here's that
guess, run exactly as the agent would run it, behind the scenes, on your machine:
GENMD-RUN-COMMAND
mlr --csv filter '$color == "Red"' example.csv
GENMD-EOF
Nothing comes back -- no error, no rows, no warning, exit code 0. And here's the trap: the agent
still owes you an answer, so it turns that silence into a sentence:
> **Agent:** I checked example.csv and there aren't any rows where color is red.
That's wrong -- there are four -- but it *reads* like a fact, because a wrong guess about *your
data*, unlike a wrong flag or function name, doesn't look like a failure on Miller's end. It looks
like an empty result, which could just as easily have been true.
### After: an agent that checks first
Same question, word for word, with the skill installed:
> **You:** In example.csv, show me the red rows.
The skill's playbook puts a step between your question and any guesswork: "constrain -- learn the
data before touching it." So before writing any comparison, the agent runs a `describe`, again
invisibly, on your behalf:
GENMD-RUN-COMMAND
mlr --icsv --ojson describe example.csv | mlr --json filter '$field_name == "color"'
GENMD-EOF
That's the real value set for `color`: lowercase, three values, no `"Red"` anywhere. The agent
corrects its own comparison before running anything user-facing:
GENMD-RUN-COMMAND
mlr --icsv --opprint filter '$color == "red"' example.csv
GENMD-EOF
-- and only now replies to you:
> **Agent:** Four rows have color = red: rows 2, 3, 4, and 6 (all square or circle, quantity
> ranging from about 13.8 to 79.3).
Same question, same data, same underlying `mlr` binary -- the only thing that changed is that the
agent looked before it leapt, and you never saw the intermediate `describe` unless you asked to.
That one habit, *check the data before writing a comparison*, is the skill in miniature; the rest
of the playbook applies the same idea to verb and function names (discover), DSL syntax (validate),
and error messages (recover) -- see [Miller AI internals](ai-support.md) for how each of those works.
## Why AI support
Miller treats AI agents as first-class users. When an agent drives a command-line tool, the agent
can fail in predictable ways: it invents flags that don't exist, guesses values that aren't in the
data, misreads error prose, and burns whole runs discovering a typo. Miller closes off each of those
failure modes with the following structure:
* Miller's entire surface -- verbs, DSL functions, flags, keywords -- is
available as **machine-readable JSON**, so agents ground themselves in what
actually exists.
* Options with fixed domains carry their **complete value sets**, and input
data can be **profiled in one pass** -- so agents copy real values instead
data can be **profiled in one pass**, so that agents copy real values instead
of inventing them.
* DSL expressions can be **validated before running**, without reading any
input.
@ -19,198 +127,27 @@ modes with structure:
* A **sandbox flag** removes external-command execution, so an
agent-constructed command line is just data processing.
Everything on this page is an ordinary command-line feature: it works from
any agent harness, system prompt, or script -- and it's equally useful for
plain shell tooling like `jq`. The [MCP server](#plug-it-in-the-mcp-server)
at the end packages it all up for MCP-speaking agents.
Every one of those is an ordinary command-line feature, documented in [Miller AI
internals](ai-support.md): each works from any agent harness, system prompt, or script.
## The essentials
## Skill file or MCP: which should you use?
**To get the AI features:** install Miller 6.20 or newer ([Installing
Miller](installing-miller.md)). That's all. Everything on this page ships
inside the ordinary `mlr` binary -- there are no plugins, no separate
installs, no API keys, and nothing here makes network calls.
For day one, the short version: start with the skill; add MCP later if you want it. They aren't
exclusive; nothing stops you running both.
**To get your AI to use them,** pick whichever matches your setup:
**Miller agent skill file:**
* **If your agent speaks MCP** (Claude Code, Claude Desktop, Cursor, ...):
register the server -- for Claude Code that's `claude mcp add miller -- mlr
mcp` -- and you're done. The tools describe themselves, and the server
ships its own instructions and playbook, so you usually don't need to say
anything special; if the agent doesn't reach for them, a nudge like "use
the Miller tools" suffices. Details in [The MCP server](mcp-server.md).
- Plus: One command, one static file -- no process, no client registration, nothing to reconnect.
- Plus: Works with any agent that reads Agent Skills from disk, not just MCP clients.
- Minus: No enforcement: it's advisory text, so no automatic `--no-shell` sandbox, no output caps or timeouts.
- Minus: The agent parses plain `mlr` text output and exit codes itself -- no structured JSON per call.
* **If your agent just runs shell commands** (a system prompt, a
`CLAUDE.md`, Cursor rules, a script harness): paste this standing
instruction into its context:
**Miller MCP server:**
GENMD-CARDIFY
Miller (mlr) is installed for processing CSV/TSV/JSON/etc. data. When
constructing mlr commands:
1. Discover: `mlr help --as-json --index` lists every verb/function/flag;
`mlr which "&lt;intent&gt;"` routes a goal to the right one; `mlr help
verb &lt;name&gt; --as-json` gives full details. Never invent flag or
function names.
2. Constrain: `mlr --icsv --ojson describe &lt;file&gt;` (or --ijson etc.)
shows the data's fields, types, and values. Copy names and values from it
rather than guessing them.
3. Validate: check DSL expressions with `mlr put --explain '&lt;expr&gt;'`
before using them.
4. Run with `--errors-json`; on failure, correct using the error's kind,
hint, and did_you_mean rather than re-guessing.
GENMD-EOF
- Plus: Structured typed calls in, structured JSON back -- no text-parsing on the agent's side.
- Plus: Sandboxed by default (`MLR_NO_SHELL=1`), output-capped, timeout-guarded.
- Minus: One more moving part: per-client registration, plus a subprocess to spawn and reconnect each session.
- Minus: Only helps agents that actually speak MCP.
A fuller, ready-made version of that lesson ships in the Miller source
tree at
[pkg/terminals/mcp/SKILL.md](https://github.com/johnkerl/miller/blob/main/pkg/terminals/mcp/SKILL.md),
in Agent Skill format -- suitable for dropping into e.g. a
`.claude/skills/miller/` directory as-is.
The rest of this page is what those instructions rest on, feature by
feature.
## Discover: the machine-readable catalog
`mlr help --as-json` emits Miller's entire help catalog as one JSON document.
The `--index` form is the cheap first call -- every capability with a
one-line summary (here trimmed, and then counted, using Miller itself):
GENMD-RUN-COMMAND
mlr help --as-json --index | mlr --json head -n 2
GENMD-EOF
GENMD-RUN-COMMAND
mlr help --as-json --index | mlr --json count
GENMD-EOF
From the index, an agent drills into full entries one at a time: `mlr help
verb sort --as-json`, `mlr help function splitax --as-json`, `mlr help flag
--ifs --as-json`, `mlr help keyword ENV --as-json` -- each accepting one or
more names. A verb entry carries a structured option list -- flag, argument
placeholder, type -- alongside the familiar usage text:
GENMD-RUN-COMMAND
mlr help verb decimate --as-json
GENMD-EOF
Note that `usage_text` -- what `mlr decimate --help` prints -- is rendered
*from* the same structured options, so the human help and the machine help
cannot drift apart. Function entries carry name, class, arity, help, and
examples; the examples across the whole catalog are exercised by Miller's
test suite, so they never rot.
Three properties make the catalog cheap to use:
* **It's a perfect cache key.** Every document carries `mlr_version` and
`catalog_schema_version`. Miller is a static binary, so the catalog changes
only when the binary does: fetch once, cache forever, re-fetch on a version
bump. No TTLs.
* **It's deterministic.** One document per invocation, sorted entries, no
colorization -- stable for diffing and for prompt caches.
* **It's opt-in twice over.** Per-call via `--as-json`, or set-once via a
truthy `MLR_HELP_JSON` environment variable.
For routing an *intent* to a capability -- the reverse of browsing -- `mlr
which` returns ranked candidates:
GENMD-RUN-COMMAND
mlr which "join two files on a key" | mlr --json head -n 2
GENMD-EOF
Its exit code signals confidence -- 0 when a query word matched a
capability's name, 2 when it didn't -- so a harness can branch on status
without parsing anything.
## Constrain: the tool's shape, and the data's shape
Agents don't just hallucinate flags; they hallucinate *values*. Miller
attacks that from both sides.
Where an option's domain is fixed by the binary, the catalog says so:
`type` is `enum` and `values` is the complete list. Here's one option of the
[summary](reference-verbs.md#summary) verb, extracted from the catalog --
using Miller to query Miller:
GENMD-RUN-COMMAND
mlr help verb summary --as-json | mlr --json put -q 'emit $options[1]'
GENMD-EOF
Where the domain depends on your *data* -- which fields exist, what values
`filter` could compare against, what to pass to `-g` -- the
[describe](reference-verbs.md#describe) verb profiles the input in one pass:
per field, the types seen, counts, cardinality, min/max, and (for
low-cardinality fields) every distinct value:
GENMD-RUN-COMMAND
mlr --icsv --ojson describe then head -n 2 example.csv
GENMD-EOF
The catalog is the *tool's* shape; `describe` is the *data's* shape. An
agent that consults both has nothing left to guess.
## Validate: check DSL before spending a run
`mlr put --explain` (likewise `mlr filter --explain`) parses and type-checks
an expression, then exits -- without opening any input at all:
GENMD-RUN-COMMAND
mlr put --explain '$z = $x + $y'
GENMD-EOF
## Run and recover: errors as data
With `--errors-json` (or a truthy `MLR_ERRORS_JSON` environment variable),
errors arrive as a structured document. The `kind` field gives an agent
something to branch on; `hint` is a runnable next step, not a sentence; and
`did_you_mean` is computed against the same catalog the agent discovered
from, closing the self-correction loop:
GENMD-RUN-COMMAND-TOLERATING-ERROR
mlr --errors-json --icsv sortt -f shape example.csv
GENMD-EOF
And since Miller's DSL includes [system and exec](shell-commands.md), there's
a sandbox: `--no-shell` (or a truthy `MLR_NO_SHELL` environment variable)
disables all external-command execution -- the DSL `system` and `exec`
functions, piped redirects, and `--prepipe` fail cleanly:
GENMD-RUN-COMMAND
mlr --no-shell -n put 'end{print system("hostname")}'
GENMD-EOF
A typical agent profile sets all three environment variables once:
GENMD-CARDIFY
export MLR_HELP_JSON=1 # help/catalog output as JSON
export MLR_ERRORS_JSON=1 # errors as structured JSON
export MLR_NO_SHELL=1 # no external-command execution
GENMD-EOF
Put together, the sections above are a loop -- discover, constrain,
validate, run -- where each step feeds the next and failures route back with
structure instead of prose.
## Plug it in: the MCP server
If your agent speaks the [Model Context
Protocol](https://modelcontextprotocol.io) -- Claude Code, Claude Desktop,
Cursor, and many others -- everything above is one line away. For Claude
Code:
<pre class="pre-highlight-in-pair">
<b>claude mcp add miller -- mlr mcp</b>
</pre>
That's the whole setup. The server's five tools are exactly the features on
this page -- `list_capabilities` and `which` for discovery, `describe_data`
to constrain, `validate_dsl` to validate, and `run` (sandboxed with
`--no-shell` by default) to execute -- plus a shipped playbook, as MCP prompt
and resource, teaching the agent the loop. Then just talk to your agent
about your data:
* "Which fields in `data.csv` have missing values?"
* "Convert this CSV to JSON, keeping only rows where status is active."
* "Join `a.csv` and `b.csv` on id, and give me the mean rate per group."
See [The MCP server](mcp-server.md) for the full tool reference and server
options.
In one line: the skill is less setup and the most portable, with weaker guarantees; MCP is a bit
more setup, with stronger guarantees, for a narrower set of clients.

View file

@ -46,7 +46,7 @@ As a first check, you should be able to run `mlr --version` at your system's com
<b>mlr --version</b>
</pre>
<pre class="pre-non-highlight-in-pair">
mlr 6.0.0
mlr 6.20.1
</pre>
A note on documentation:

View file

@ -26,9 +26,8 @@ Note that the [Miller releases page](https://github.com/johnkerl/miller/releases
As a first check, you should be able to run `mlr --version` at your system's command prompt and see something like the following:
GENMD-CARDIFY-HIGHLIGHT-ONE
GENMD-RUN-COMMAND
mlr --version
mlr 6.0.0
GENMD-EOF
A note on documentation:

View file

@ -14,22 +14,69 @@ Quick links:
<a class="quicklink" href="../release-docs/index.html">Release docs</a>
</span>
</div>
# The MCP server
# The Miller MCP server
Miller ships with a built-in [Model Context Protocol](https://modelcontextprotocol.io)
server, so AI agents (Claude Code, Claude Desktop, Cursor, and other MCP
clients) can discover and drive Miller without scraping help text or guessing
at flags. (For the overview of Miller's whole AI feature set -- with or
without MCP -- see [Miller and AI](ai.md).)
As of Miller version 6.20, released in July 2026, there are two main ways to get your AI to know
about a software tool (Miller, or others): [**agent skills**](agent-skill.md), and **MCP**. (See
[Miller and AI](ai.md) for an introduction.)
The server speaks JSON-RPC over stdin/stdout (MCP's "stdio" transport): the
MCP client spawns `mlr mcp` as a subprocess. No network port is opened, and
the server exits when the client disconnects. Example registration, for
Claude Code:
Miller ships with a built-in [Model Context Protocol](https://modelcontextprotocol.io) server
included within the `mlr` executable, so AI agents (Claude Code, Claude Desktop, Cursor, and other
MCP clients) can discover and drive Miller without scraping help text or guessing at flags.
The server speaks JSON-RPC over stdin/stdout (MCP's **stdio** transport): the MCP client spawns `mlr
mcp` as a subprocess. No network port is opened, and the server exits when the client disconnects.
## Setup
Example registration for some common CLI agents (do this before starting your session):
<pre class="pre-highlight-in-pair">
<b>claude mcp add miller -- mlr mcp</b>
</pre>
<pre class="pre-non-highlight-in-pair">
Added stdio MCP server miller with command: mlr mcp to local config
File modified: /Users/kerl/.claude.json [project: /Users/kerl/git/johnkerl/miller]
</pre>
<pre class="pre-highlight-non-pair">
<b>codex mcp add miller -- mlr mcp</b>
</pre>
<pre class="pre-highlight-non-pair">
<b>gemini mcp add miller mlr mcp</b>
</pre>
You can undo that as follows:
<pre class="pre-highlight-in-pair">
<b>claude mcp remove miller</b>
</pre>
<pre class="pre-non-highlight-in-pair">
Removed MCP server "miller" from local config
File modified: /Users/kerl/.claude.json [project: /Users/kerl/git/johnkerl/miller]
</pre>
<pre class="pre-highlight-non-pair">
<b>codex mcp remove miller</b>
</pre>
<pre class="pre-highlight-non-pair">
<b>gemini mcp remove miller</b>
</pre>
Then -- just interact with your agent as always! When you say something like `describe the data file example.csv`,
the agent will already know how to use Miller to help answer that question.
![pix/mcp-describe.png](pix/mcp-describe.png)
For more background on the `mlr` commands the agent runs on your behalf, please see
[Miller AI internals](ai-support.md).
## What the Miller MCP tools map to
As shown below, you don't have to type `mcp` in your agent sessions: rather you've empowered the
agent to discover things about Miller. But if you're curious what the AI agent will see:
<pre class="pre-highlight-in-pair">
<b>mlr mcp --help</b>
@ -68,25 +115,68 @@ Options:
-h or --help Show this message.
</pre>
## What the tools map to
Each MCP tool is a thin wrapper over a Miller feature you can also, if you like, use directly from
the command line:
Each MCP tool is a thin wrapper over a Miller feature you can also use
directly from the command line:
* `list_capabilities` is [`mlr help --as-json`](online-help.md) -- the
machine-readable catalog of verbs, DSL functions, flags, and keywords.
* `which` is `mlr which` -- natural-language intent to ranked capabilities.
* `validate_dsl` is `mlr put --explain` / `mlr filter --explain` -- parse and
type-check a DSL expression without reading any input.
* `describe_data` is [`mlr describe`](reference-verbs.md#describe) -- field
- `list_capabilities` is [`mlr help --as-json`](online-help.md): the
machine-readable catalog of [verbs](reference-verbs.md),
[DSL functions](reference-dsl-builtin-functions.md), [flags](reference-main-flag-list.md), and
[keywords](reference-dsl-variables.md#keywords-for-filter-and-put).
- `which` is `mlr which`: turns natural-language intent into ranked capabilities.
- `validate_dsl` is `mlr put --explain` / `mlr filter --explain`: to parse and
type-check a DSL expression before reading any input files.
- `describe_data` is [`mlr describe`](reference-verbs.md#describe): this shows field
names, types, cardinality, and value domains for input data.
* `run` executes an `mlr` command line and reports exit code, output, and --
- `run` executes an `mlr` command line and reports exit code, output, and --
on failure -- the structured error document from `mlr --errors-json`.
The catalog tools are answered in-process; the others run this same `mlr`
binary as a subprocess, so agents see exactly what a terminal user sees.
See also the [Miller AI internals page](ai-support.md) for more information.
## Sandboxing: --no-shell
## What Miller MCP looks like in practice
Here are some screenshots from a Claude Code session.
At the shell, before starting `claude`, we've first run
<pre class="pre-highlight-in-pair">
<b>claude mcp add miller -- mlr mcp</b>
</pre>
<pre class="pre-non-highlight-in-pair">
Added stdio MCP server miller with command: mlr mcp to local config
File modified: /Users/kerl/.claude.json [project: /Users/kerl/git/johnkerl/miller]
</pre>
Then, inside Claude code, we type `/mcp`:
![pix/mcp-slash.png](pix/mcp-slash.png)
Then we select Miller:
![pix/mcp-manage.png](pix/mcp-manage.png)
The status shows it's installed. Note that there is no long-running Miller "server" process: this is
just Claude remembering to run things like `mlr mcp ...` in order to get how-to instructions from
the `mlr` executable you already have installed.
![pix/mcp-status.png](pix/mcp-status.png)
The MCP tools are names for Claude to remember -- you don't have to. For transparency, though, here they are:
![pix/mcp-tools.png](pix/mcp-tools.png)
Here are descriptions of a couple of them:
![pix/mcp-describe-data.png](pix/mcp-describe-data.png)
![pix/mcp-list-capabilities.png](pix/mcp-list-capabilities.png)
When you're in your AI session, you don't have to type `mcp` or the specific names of Miller MCP tools.
Rather, you just interact as always, and the AI remembers to call Miller MCP tools on your behalf.
For example:
![pix/mcp-describe.png](pix/mcp-describe.png)
## A note on sandboxing
Miller's DSL includes [`system` and `exec`](shell-commands.md), and
`--prepipe`/piped redirects also run external commands. So that an
@ -95,19 +185,7 @@ subprocesses started by the MCP server run with `MLR_NO_SHELL=1`: those
features fail cleanly instead of executing. Start the server with
`mlr mcp --allow-shell` to turn that off.
The same gate is available outside the MCP server: pass `--no-shell` to any
`mlr` invocation, or set the `MLR_NO_SHELL` environment variable to a truthy
value. Note that Miller can still write files when asked to (`tee`, `split`,
DSL output redirection) -- the gate is specifically about executing external
commands.
## The agent playbook
The server also exposes a playbook -- as MCP prompt `miller-playbook` and MCP
resource `miller://playbook` -- encoding the loop that makes an agent
effective with Miller: **discover** capabilities from the catalog rather than
inventing them, **constrain** to the data's actual fields and values via
`describe_data`, **validate** DSL before running it, and **run** with
structured-error recovery. The same text lives in the Miller source tree at
[pkg/terminals/mcp/SKILL.md](https://github.com/johnkerl/miller/blob/main/pkg/terminals/mcp/SKILL.md)
in Agent Skill format.
The same gate is available outside the MCP server: pass `--no-shell` to any `mlr` invocation, or set
the `MLR_NO_SHELL` [environment variable](reference-main-env-vars.md) to `true`. Note that Miller
can still write files when asked to (`tee`, `split`, DSL output redirection): the gate is
specifically about executing external commands.

View file

@ -1,43 +1,127 @@
# The MCP server
# The Miller MCP server
Miller ships with a built-in [Model Context Protocol](https://modelcontextprotocol.io)
server, so AI agents (Claude Code, Claude Desktop, Cursor, and other MCP
clients) can discover and drive Miller without scraping help text or guessing
at flags. (For the overview of Miller's whole AI feature set -- with or
without MCP -- see [Miller and AI](ai.md).)
As of Miller version 6.20, released in July 2026, there are two main ways to get your AI to know
about a software tool (Miller, or others): [**agent skills**](agent-skill.md), and **MCP**. (See
[Miller and AI](ai.md) for an introduction.)
The server speaks JSON-RPC over stdin/stdout (MCP's "stdio" transport): the
MCP client spawns `mlr mcp` as a subprocess. No network port is opened, and
the server exits when the client disconnects. Example registration, for
Claude Code:
Miller ships with a built-in [Model Context Protocol](https://modelcontextprotocol.io) server
included within the `mlr` executable, so AI agents (Claude Code, Claude Desktop, Cursor, and other
MCP clients) can discover and drive Miller without scraping help text or guessing at flags.
<pre class="pre-highlight-in-pair">
<b>claude mcp add miller -- mlr mcp</b>
</pre>
The server speaks JSON-RPC over stdin/stdout (MCP's **stdio** transport): the MCP client spawns `mlr
mcp` as a subprocess. No network port is opened, and the server exits when the client disconnects.
## Setup
Example registration for some common CLI agents (do this before starting your session):
GENMD-CARDIFY-HIGHLIGHT-ONE
claude mcp add miller -- mlr mcp
Added stdio MCP server miller with command: mlr mcp to local config
File modified: /Users/kerl/.claude.json [project: /Users/kerl/git/johnkerl/miller]
GENMD-EOF
GENMD-CARDIFY-HIGHLIGHT-ONE
codex mcp add miller -- mlr mcp
GENMD-EOF
GENMD-CARDIFY-HIGHLIGHT-ONE
gemini mcp add miller mlr mcp
GENMD-EOF
You can undo that as follows:
GENMD-CARDIFY-HIGHLIGHT-ONE
claude mcp remove miller
Removed MCP server "miller" from local config
File modified: /Users/kerl/.claude.json [project: /Users/kerl/git/johnkerl/miller]
GENMD-EOF
GENMD-CARDIFY-HIGHLIGHT-ONE
codex mcp remove miller
GENMD-EOF
GENMD-CARDIFY-HIGHLIGHT-ONE
gemini mcp remove miller
GENMD-EOF
Then -- just interact with your agent as always! When you say something like `describe the data file example.csv`,
the agent will already know how to use Miller to help answer that question.
![pix/mcp-describe.png](pix/mcp-describe.png)
For more background on the `mlr` commands the agent runs on your behalf, please see
[Miller AI internals](ai-support.md).
## What the Miller MCP tools map to
As shown below, you don't have to type `mcp` in your agent sessions: rather you've empowered the
agent to discover things about Miller. But if you're curious what the AI agent will see:
GENMD-RUN-COMMAND
mlr mcp --help
GENMD-EOF
## What the tools map to
Each MCP tool is a thin wrapper over a Miller feature you can also, if you like, use directly from
the command line:
Each MCP tool is a thin wrapper over a Miller feature you can also use
directly from the command line:
* `list_capabilities` is [`mlr help --as-json`](online-help.md) -- the
machine-readable catalog of verbs, DSL functions, flags, and keywords.
* `which` is `mlr which` -- natural-language intent to ranked capabilities.
* `validate_dsl` is `mlr put --explain` / `mlr filter --explain` -- parse and
type-check a DSL expression without reading any input.
* `describe_data` is [`mlr describe`](reference-verbs.md#describe) -- field
- `list_capabilities` is [`mlr help --as-json`](online-help.md): the
machine-readable catalog of [verbs](reference-verbs.md),
[DSL functions](reference-dsl-builtin-functions.md), [flags](reference-main-flag-list.md), and
[keywords](reference-dsl-variables.md#keywords-for-filter-and-put).
- `which` is `mlr which`: turns natural-language intent into ranked capabilities.
- `validate_dsl` is `mlr put --explain` / `mlr filter --explain`: to parse and
type-check a DSL expression before reading any input files.
- `describe_data` is [`mlr describe`](reference-verbs.md#describe): this shows field
names, types, cardinality, and value domains for input data.
* `run` executes an `mlr` command line and reports exit code, output, and --
- `run` executes an `mlr` command line and reports exit code, output, and --
on failure -- the structured error document from `mlr --errors-json`.
The catalog tools are answered in-process; the others run this same `mlr`
binary as a subprocess, so agents see exactly what a terminal user sees.
See also the [Miller AI internals page](ai-support.md) for more information.
## Sandboxing: --no-shell
## What Miller MCP looks like in practice
Here are some screenshots from a Claude Code session.
At the shell, before starting `claude`, we've first run
GENMD-CARDIFY-HIGHLIGHT-ONE
claude mcp add miller -- mlr mcp
Added stdio MCP server miller with command: mlr mcp to local config
File modified: /Users/kerl/.claude.json [project: /Users/kerl/git/johnkerl/miller]
GENMD-EOF
Then, inside Claude code, we type `/mcp`:
![pix/mcp-slash.png](pix/mcp-slash.png)
Then we select Miller:
![pix/mcp-manage.png](pix/mcp-manage.png)
The status shows it's installed. Note that there is no long-running Miller "server" process: this is
just Claude remembering to run things like `mlr mcp ...` in order to get how-to instructions from
the `mlr` executable you already have installed.
![pix/mcp-status.png](pix/mcp-status.png)
The MCP tools are names for Claude to remember -- you don't have to. For transparency, though, here they are:
![pix/mcp-tools.png](pix/mcp-tools.png)
Here are descriptions of a couple of them:
![pix/mcp-describe-data.png](pix/mcp-describe-data.png)
![pix/mcp-list-capabilities.png](pix/mcp-list-capabilities.png)
When you're in your AI session, you don't have to type `mcp` or the specific names of Miller MCP tools.
Rather, you just interact as always, and the AI remembers to call Miller MCP tools on your behalf.
For example:
![pix/mcp-describe.png](pix/mcp-describe.png)
## A note on sandboxing
Miller's DSL includes [`system` and `exec`](shell-commands.md), and
`--prepipe`/piped redirects also run external commands. So that an
@ -46,19 +130,7 @@ subprocesses started by the MCP server run with `MLR_NO_SHELL=1`: those
features fail cleanly instead of executing. Start the server with
`mlr mcp --allow-shell` to turn that off.
The same gate is available outside the MCP server: pass `--no-shell` to any
`mlr` invocation, or set the `MLR_NO_SHELL` environment variable to a truthy
value. Note that Miller can still write files when asked to (`tee`, `split`,
DSL output redirection) -- the gate is specifically about executing external
commands.
## The agent playbook
The server also exposes a playbook -- as MCP prompt `miller-playbook` and MCP
resource `miller://playbook` -- encoding the loop that makes an agent
effective with Miller: **discover** capabilities from the catalog rather than
inventing them, **constrain** to the data's actual fields and values via
`describe_data`, **validate** DSL before running it, and **run** with
structured-error recovery. The same text lives in the Miller source tree at
[pkg/terminals/mcp/SKILL.md](https://github.com/johnkerl/miller/blob/main/pkg/terminals/mcp/SKILL.md)
in Agent Skill format.
The same gate is available outside the MCP server: pass `--no-shell` to any `mlr` invocation, or set
the `MLR_NO_SHELL` [environment variable](reference-main-env-vars.md) to `true`. Note that Miller
can still write files when asked to (`tee`, `split`, DSL output redirection): the gate is
specifically about executing external commands.

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

BIN
docs/src/pix/mcp-manage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
docs/src/pix/mcp-slash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
docs/src/pix/mcp-status.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
docs/src/pix/mcp-tools.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -11,8 +11,13 @@ import (
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
)
// PlaybookText is the Miller Agent Skill / playbook content: the same text
// served here as the "miller-playbook" MCP prompt/resource, and reused
// as-is by `mlr skill` (pkg/terminals/skill) for agents that read Agent
// Skills from disk rather than over MCP.
//
//go:embed SKILL.md
var playbookText string
var PlaybookText string
const playbookPromptName = "miller-playbook"
const playbookResourceURI = "miller://playbook"
@ -117,7 +122,7 @@ func playbookPromptHandler(_ context.Context, _ *mcpsdk.GetPromptRequest) (*mcps
Messages: []*mcpsdk.PromptMessage{
{
Role: "user",
Content: &mcpsdk.TextContent{Text: playbookText},
Content: &mcpsdk.TextContent{Text: PlaybookText},
},
},
}, nil
@ -129,7 +134,7 @@ func playbookResourceHandler(_ context.Context, _ *mcpsdk.ReadResourceRequest) (
{
URI: playbookResourceURI,
MIMEType: "text/markdown",
Text: playbookText,
Text: PlaybookText,
},
},
}, nil

View file

@ -315,6 +315,6 @@ func TestParseStructuredError(t *testing.T) {
}
func TestPlaybookHasFrontmatter(t *testing.T) {
assert.True(t, strings.HasPrefix(playbookText, "---\n"))
assert.Contains(t, playbookText, "name: miller")
assert.True(t, strings.HasPrefix(PlaybookText, "---\n"))
assert.Contains(t, PlaybookText, "name: miller")
}

View file

@ -19,6 +19,7 @@ const (
Regtest = "regtest"
Repl = "repl"
Script = "script"
Skill = "skill"
Version = "version"
Which = "which"
)
@ -32,6 +33,7 @@ var Names = []string{
Regtest,
Repl,
Script,
Skill,
Version,
Which,
}

View file

@ -0,0 +1,92 @@
// Entrypoint for `mlr skill`: puts the Miller Agent Skill (SKILL.md) where a
// coding agent can find it on disk, for tools that read Agent Skills
// directly rather than over MCP.
//
// The content is identical to what `mlr mcp` serves as its "miller-playbook"
// prompt/resource (pkg/terminals/mcp/SKILL.md, exported as mcp.PlaybookText)
// -- this is a second delivery path for the same text, not a second source
// of truth.
package skill
import (
"fmt"
"os"
"path/filepath"
"github.com/johnkerl/miller/v6/pkg/terminals/mcp"
)
const defaultInstallDir = ".claude/skills/miller"
func skillUsage(o *os.File) {
fmt.Fprintf(o, "Usage: mlr skill {print|install} [options]\n")
fmt.Fprintf(o, "Puts the Miller Agent Skill (SKILL.md) where a coding agent can find it.\n")
fmt.Fprintf(o, "This is the same playbook mlr mcp serves as its \"miller-playbook\"\n")
fmt.Fprintf(o, "prompt/resource, packaged for agents that read Agent Skills from disk.\n")
fmt.Fprintf(o, "\n")
fmt.Fprintf(o, "Subcommands:\n")
fmt.Fprintf(o, " print Write the skill content to stdout.\n")
fmt.Fprintf(o, " install [DIR] Write DIR/SKILL.md, creating DIR if needed.\n")
fmt.Fprintf(o, " Default DIR is %s\n", defaultInstallDir)
fmt.Fprintf(o, "\n")
fmt.Fprintf(o, " -h or --help Show this message.\n")
}
// SkillMain is the entrypoint called by the terminals dispatcher for `mlr skill`.
func SkillMain(args []string) int {
args = args[1:] // strip "skill"
if len(args) == 0 {
skillUsage(os.Stderr)
return 1
}
switch args[0] {
case "-h", "--help":
skillUsage(os.Stdout)
return 0
case "print":
return printMain(args[1:])
case "install":
return installMain(args[1:])
default:
fmt.Fprintf(os.Stderr, "mlr skill: subcommand \"%s\" not recognized.\n", args[0])
return 1
}
}
func printMain(args []string) int {
if len(args) != 0 {
fmt.Fprintf(os.Stderr, "mlr skill print: takes no arguments.\n")
return 1
}
fmt.Print(mcp.PlaybookText)
return 0
}
func installMain(args []string) int {
dir := defaultInstallDir
switch len(args) {
case 0:
case 1:
dir = args[0]
default:
fmt.Fprintf(os.Stderr, "mlr skill install: takes at most one argument (the target directory).\n")
return 1
}
if err := os.MkdirAll(dir, 0o755); err != nil {
fmt.Fprintf(os.Stderr, "mlr skill install: could not create %s: %v\n", dir, err)
return 1
}
path := filepath.Join(dir, "SKILL.md")
if err := os.WriteFile(path, []byte(mcp.PlaybookText), 0o644); err != nil {
fmt.Fprintf(os.Stderr, "mlr skill install: could not write %s: %v\n", path, err)
return 1
}
fmt.Printf("Wrote %s\n", path)
return 0
}

View file

@ -15,6 +15,7 @@ import (
"github.com/johnkerl/miller/v6/pkg/terminals/regtest"
"github.com/johnkerl/miller/v6/pkg/terminals/repl"
"github.com/johnkerl/miller/v6/pkg/terminals/script"
"github.com/johnkerl/miller/v6/pkg/terminals/skill"
"github.com/johnkerl/miller/v6/pkg/version"
)
@ -41,6 +42,7 @@ func init() {
{registry.Regtest, regtest.RegTestMain},
{registry.Repl, repl.ReplMain},
{registry.Script, script.ScriptMain},
{registry.Skill, skill.SkillMain},
{registry.Version, showVersion},
{registry.Which, help.WhichMain},
}