Building with tabnas #
Written for an agent that has arrived here to build something. It covers the distributions you can install (five skills, an MCP server, a CLI), the grammar format you should emit, the constraints the engine imposes, how to check your work, and where the per-package instructions live. A human wanting the argument for any of this should read why tabnas instead.
1 · What you are targeting #
A tabnas grammar is data: a table of rules, each with
an open and a close phase, each phase holding
a list of alternates. An alternate matches a short token
pattern and may run an action, push a child rule, or repeat the current
one. Parsing is a for-loop over tokens with a rule stack: no recursion
and no backtracking.
That is the whole machine. You are not writing a parser; you are filling in a table. It is deliberately dull, which is what makes it a target you can hit reliably.
2 · Install the workflow: skills, MCP, CLI #
Everything the rest of this page teaches is also packaged for you to install rather than read. Three surfaces, one shared implementation:
- The skills: five
portable Agent
Skills that carry the working procedures:
build-a-plugin,create-grammar,debug-parse,test-a-grammar,upgrade-a-plugin. Each has its own page here, and each is plain markdown a platform without plugin support can read straight from the repository. - The MCP server:
@tabnas/mcpmakes the commands the skills teach executable: seven tools to parse, validate a grammar before running it, explain a failure, run fixtures, read the plugin catalogue, and compare a grammar change against the one it replaces. It isdev.tabnas/mcpin the official MCP Registry, so a client that resolves servers from the registry finds both transports declared there. - The
tabnasCLI: the same package, the same seven operations in a shell.--jsonprints byte-for-byte what the MCP tool returns (golden tests hold the two front-ends to it), so a shell transcript and an agent transcript describe the same run.
In Claude Code #
One plugin delivers all of it: the five skills plus both MCP server entries. Two commands, and the second is not optional: adding a marketplace registers a catalogue and installs nothing.
/plugin marketplace add tabnas/skills
/plugin install tabnas@tabnas In any MCP client #
Add the stdio server to your client's MCP configuration: it needs no
install step of its own; npx fetches it on first run:
npx --yes @tabnas/mcp@0.1.17 mcp
If your client cannot spawn a process, the same seven tools are
served over streamable HTTP at https://mcp.tabnas.dev/mcp: bounded by a
body cap and a per-IP rate limit, both reported by its
/.well-known/mcp. Local stdio is the recommended path:
free, private, unlimited. MCP has the manifest
JSON, each tool's contract, and the hosted endpoint's bounds.
In a shell #
npm install -g @tabnas/mcp # installs the tabnas command tabnas parse | validate | diagnose | test | plugins | compare,
each mirroring one MCP tool. The skills teach the CLI spellings, so
they work with or without a server connected.
3 · Install the engine #
npm install @tabnas/parser @tabnas/abnf go get github.com/tabnas/parser/go Pin exact versions. Everything is pre-1.0, so a caret range will refuse the next minor release without telling you.
4 · Do not start from scratch #
This is the step most worth spending time on. Extension is the cheapest
path and the one the engine is designed for: if something close to your
format already parses, start there and add rules. JSONC is JSON plus
comments; jsonic is JSONC with the quoting relaxed. Check the
package list before writing anything: or ask
the list_plugins MCP tool (tabnas plugins),
which answers from each plugin's own descriptor.
- Parsing a JSON dialect? Start from jsonic and remove or add rules.
- Need operators and precedence? Add expr rather than hand-rolling a Pratt parser.
- Need
@name-style forms? Add directive. - Need free text between delimiters? Add hoover.
- Composing several documents into one parse? Add multisource.
aontu is the worked example: a whole configuration language assembled from five of these plugins, defining no parser of its own.
5 · The rule table #
This is the form to emit. It is plain data, so you can validate it, diff it, and print it before anything runs.
A rule table, loaded and exercised
output 1 accept · 1+2 accept · 1+2+3 accept · 1+ reject · +1 reject
| Field | On an alternate, means |
|---|---|
s | Match this token sequence: one token, or several for lookahead. |
p | Push a child rule: it becomes a child node. |
r | Repeat a rule at the same stack depth: no nesting, same parent. |
a | Action: a function, a @ref name, a $-builtin, or an array of them. |
c | Condition: the alternate only applies when it holds. |
{} | The empty alternate. Ends the phase. Without one, no match is a parse error. |
p versus r is the distinction that catches
people out. Push nests, so the child's parent is the pushing
rule. Repeat stays at the same depth, so every repetition shares one
parent, which is what makes an accumulator a single value in a single
place.
6 · Actions, ideally without code #
A grammar that only recognises input returns nothing. Actions build the
result. There are three ways to attach them, and you should prefer them
in this order:
Builtin actions: no functions at all #
The engine ships $-suffixed builtins, merged into the ref
map when the grammar loads. Referenced by name, they let you emit a
grammar that is pure JSON with no code in it, which is
the safest thing you can hand to someone else, and the only kind of
grammar the MCP tools will accept.
A grammar with no code in it
output 42 => 42 · 3.5 => 3.5 · -7 => -7
Builtin Effect @object$r.node = {} @array$r.node = [] @key$Capture the matched key token. @setval$Assign the child's node as an object property. @push$Append the child's node to an array. @value$Resolve the matched scalar token. @reset$Clear the parent-seeded node. @node$ @capture$ @bubble$Rebuild the { rule, src, kids } tree. Used by the ABNF compiler.
Named refs: code, bound out of band #
Keeps the grammar declarative while the code lives in the host
program. Two kinds of name: alternate marks (open- or close-phase),
and rule-phase hooks (bo, ao,
bc, ac: before/after open and close).
Keep results on the node, not in a variable outside the parse, so
the grammar stays reusable.
A rule-phase hook and an alternate mark
output 1 => total 1, terms 1 · 1+2+3 => total 6, terms 3 · 12+3+45 => total 60, terms 3
Mark names are assigned by the compiler from each alternate's leading
discriminator. Do not guess them. Ask:
tabnas-abnf --marks -f grammar.abnf.
Inline functions #
Written straight onto the alternate as a. The most direct,
but the grammar is now code and can no longer be serialised or safely
shared.
Actions written onto the alternates
output 1 => 1 · 1+2+3 => 6 · 12+3+45 => 60
r is the rule instance, r.node the value it
carries, r.o the tokens matched in the open phase (so
r.o[0].val is the first token's value, already a number),
and r.parent / r.child the neighbouring rules.
7 · Constraints to design around #
These are the things that will bite. All of them are by design.
- Deterministic, no backtracking. Alternates are
tried in order and the first match wins. Two alternates that can't be
told apart from their leading tokens resolve to whichever comes
first: order them deliberately.
- Ambiguity is not supported. One parse or an error.
If you need every valid parse, this is the wrong engine.
- You write the lexer. tabnas separates lexing from
parsing and is about the parsing half. Built-ins like
NR
and fixed tokens cover a lot; beyond that, you assemble it.
- Left recursion is rewritten, not supported. The ABNF
compiler applies Paull's
algorithm, so
P = P a / b
becomes P = b *(a). The tree comes out flat rather than
left-nested, and a purely left-recursive rule is an error.
- A tail self-reference compiles to a repeat.
X = prefix [ sep X ] becomes a same-depth
r: repeat, so r.parent is the wrapping
rule for every repetition and r.parent.node is the
accumulator idiom: identical to a hand-written table. Other sugar
(( … ), *( … )) still desugars into
generated group rules, where parent may be synthetic;
check with tabnas-abnf --marks.
- Empty alternates matter. A phase with no matching
alternate is a parse error.
{} is how a rule ends.
8 · Verify your work #
A grammar that looks right and parses one happy-path input is not
evidence. The MCP tools and the CLI from
section 2 exist for exactly this loop.
Before reporting success:
- Validate before running.
tabnas validate --grammar g.json (the
validate_grammar tool) checks a serialized grammar
against the engine's JSON Schema and loads it in a fresh instance: so a malformed table fails as a named error, not a misparse.
- Parse valid inputs and assert the returned values. A successful parse alone does not verify the result.
- Parse known-bad inputs and confirm they're rejected. A grammar that accepts everything is a common failure.
- Read the diagnostic, not the message. On a failure,
tabnas diagnose (explain_parse_error)
returns code, position, expected tokens and the rule stack; the
code is a URL at /errors.
- Pin behaviour with fixtures. Write
.tsv rows: input, expected value or
ERROR:<code>: and run them with
tabnas test --spec fixtures.tsv
(test_grammar). One file runs against both runtimes.
- Changing a grammar others already use?
tabnas compare (compare_grammars) reports
what a change provably keeps, what it observably changes, and with
what confidence: evidence, never a bare verdict.
- Run
tabnas-abnf --marks if an action didn't fire. - Use @tabnas/debug to describe the live grammar and render it back to ABNF: if the round-trip isn't what you meant, the grammar isn't either.
- Use @tabnas/railroad to draw it when the structure is hard to hold in your head.
- Try it in the playground: it takes ABNF or a rule table and shows both the tree and the value.
9 · Per-package instructions #
Every package repository ships an AGENTS.md with its
layout, conventions, test commands, and the non-obvious things to know
before changing it. Read the one for the package you're touching: start
with parser.
Engine
Agent tooling
Languages and formats
jsonjsoncjson5jsonljsonicyamltomlinicsvxmlmarkdowncsscprotozonfeedchesssemvergbnf
Web components
Syntax plugins
Command line
Work lands in TypeScript first, then Go to match; both run the same
fixtures, so a change that doesn't really work fails loudly. Agent-written
contributions are welcome and need no disclosure: see
community.
10 · This site, machine-readable #
- llms.txt indexes the site
and llms-full.txt is
the full documentation text. Both are generated from the site's own
pages, so they cannot fall behind them.
- versions.json says
which package versions this documentation describes: worth reading
before trusting an example against whatever you have installed.
- /errors is every error code
the engine and its plugins can raise, one page per code at
/errors/<code>. A diagnostic's code is
therefore a URL you can follow.
Source for everything is under
github.com/tabnas.