A tiny MCP server that lets LLM coding agents read and write TOML and Markdown files as if they were a structured database — with schemas to keep agents honest.
ta exposes these tools over MCP stdio:
get— read one record by id (raw bytes by default, structured fields withfields=[...]), or every record under an id prefix.list_sections— enumerate record ids under a scope, in file-parse order.create— create a new record; fails if the id already exists.typeis required and db-qualified (<db>.<type>).update— PATCH-style update of an existing record; partial overlays, atomic re-validation.delete— remove a record by id, or a whole file by id prefix.search— structured + regex search across records under a scope.schema— inspect or mutate the resolved schema (get, create, update, delete on db / type / field levels).
Build plan: docs/PLAN.md.
From a clone of this repo:
mage installThis builds ta and drops the binary at $HOME/.local/bin/ta. That directory is on the default $PATH on modern Unix, so no Go toolchain is needed to run ta — only to build it.
Requires Go 1.26 or newer at build time. The binary is pure Go and statically linkable.
For Claude Code, register ta with the claude mcp add CLI — not by hand-editing a config file. From inside your project (or the bare root of a bare-repo-plus-worktree layout), run:
claude mcp add --transport stdio ta -- taBreakdown:
--transport stdio— howtaspeaks MCP (over child-process stdin/stdout).- First
ta— the name the server is registered under (tools appear asmcp__ta__*). --— separator; everything after is the spawn command, not a Claude flag.- Second
ta— the command to spawn (shell-resolved via$PATH).
No --scope flag → defaults to local scope, which writes to ~/.claude.json under the current project's cwd and keeps the registration private to your machine. Pass --scope project if you want the registration committed to the repo (lands in .mcp.json at the project root, managed by the CLI — don't hand-edit it).
Verify the registration landed with:
claude mcp listta reads no runtime arguments; all tool arguments arrive over MCP. Use ta --help for a summary of CLI flags (--version, --log-startup, --project).
The MCP server resolves its schema from one project per process. By default that's the spawn cwd — which works whenever the MCP client launches ta with cwd set to the project root (e.g. starting Claude Code from inside the project checkout).
For launchers that cannot control the spawn cwd, pass --project <abs-path> in the registration command. With claude mcp add:
claude mcp add --transport stdio ta -- ta --project /abs/path/to/projectOr hand-rolled in a .mcp.json your launcher accepts:
{
"mcpServers": {
"ta": {
"command": "ta",
"args": ["--project", "/abs/path/to/project"]
}
}
}The flag must be absolute, must exist, and must contain .ta/schema.toml. Empty / unset → cwd fallback. The flag wins over cwd when both are present.
Each project carries one schema at <project>/.ta/schema.toml. The runtime reads exactly that one file — no home-layer cascade, no ancestor walk. If the project has no schema, ta errors with a clear message.
A schema declares one or more dbs. Each db lists the file paths it owns (TOML or Markdown — format inferred from the path extension) and the record types those files may contain.
Example .ta/schema.toml:
[plans]
paths = ["plans.toml"]
description = "Planning records."
[plans.task]
description = "A unit of work."
[plans.task.fields.id]
type = "string"
required = true
[plans.task.fields.status]
type = "string"
required = true
enum = ["todo", "doing", "blocked", "done"]
[plans.task.fields.body]
type = "string"With that schema in place, an agent can create a task:
{
"name": "create",
"arguments": {
"path": "/abs/path/to/project",
"id": "plans.task-001",
"type": "plans.task",
"data": {
"id": "task-001",
"status": "doing",
"body": "## Approach\n\nStart by..."
}
}
}The on-disk bracket header IS the id — [plans.task-001] in plans.toml. The record's type lives in .ta/index.toml, never in the id. Validation failures come back as structured JSON — the agent sees exactly which field failed which rule.
mage check # fmtcheck, vet, test, tidy — full-module commit gate
mage build # produces ./bin/ta
mage install # builds and drops the binary at $HOME/.local/bin/ta
mage fmt # run gofumpt (latest, auto-installed) in-placeRun mage -l for the full target list.
When multiple agents work on the same checkout in a cascade, full-module mage test gives a verdict polluted by sibling agents' WIP. Each agent runs ONLY the tests their slice owns:
mage testFunc TestMyThing # one test, whole module
mage testFunc 'TestA|TestB|TestC' # several tests, pipe-joined regex
TA_TEST_PKG=./internal/ops mage testFunc TestX # narrow scope further
mage testPkg ./internal/ops # full package, end-to-end
mage check # full module — orchestrator-levelTest output auto-detects TTY status via laslig/gotestout — agents and CI pipes get plain text, humans on a terminal get a styled summary. No env-var prefix needed; the targets above just work in either context. Cascade methodology § "QA Placement" (CASCADE_METHODOLOGY.md) covers the level-by-level discipline, and docs/cascade-reference.md §1 "Test-scope Isolation" details the slice-scoping; the rule of thumb is agents test only what their slice owns; QA escalates one level up.
If you're running ta's build / QA agents in a cascade (planner → builder → QA-proof + QA-falsification), the LSP daemon (gopls for ta) caches workspace state that lags behind the build agent's writes. A QA agent spawned next reads from a stale LSP and reports diagnostics that don't reflect disk truth. Mage check is authoritative, but the QA agent reads LSP, not mage.
Pattern: a PreToolUse hook on the Agent tool that recycles the LSP daemon when the spawned agent is a QA variant. Machine-local example shipped with ta:
# ~/.claude/hooks/pre_agent_lsp_refresh.sh
# Fires on Agent spawn. When subagent_type matches qa-proof or
# qa-falsification, kills gopls so the next LSP call gets a fresh index.
INPUT=$(cat)
if printf '%s' "$INPUT" | grep -qE '"subagent_type"[[:space:]]*:[[:space:]]*"[^"]*qa-(proof|falsification)[^"]*"'; then
pkill -f 'gopls' 2>/dev/null || true
fi
exit 0Register in ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Agent",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/pre_agent_lsp_refresh.sh" }]
}
]
}
}The concept is universal across LSP-based languages — extend the script with tsserver, pylsp, rust-analyzer etc. when needed. Documented in docs/cascade-reference.md §2 "Pre-QA LSP Refresh — Universal Discipline".
Dogfood plan: the long-term goal is for ta itself to manage these hooks (alongside agents, instructions docs, skills, rules) via shipped schemas. Once the dogfood phase lands claude_hooks schema support, ta init will install the hook into <project>/.claude/hooks/ automatically and every ta dev gets it without manual setup. Until then, install machine-local from this section.
Animated VHS recordings of every interactive bubbletea surface live under cmd/ta/testdata/vhs/. Three high-traffic flows inline below; the full per-tape index follows.
Bare ta (no args) drops into the root subcommand menu — the entry point most devs hit first.
space on a group header toggles every visible leaf at once — the UX the previous huh-based form could not express.
ta create without all required fields drops into an interactive form covering required + optional fields with inline validation.
smoke.gif/.txt— minimum-viable smoke recording proving the VHS pipeline + golden contract are wired.menu.gif/.txt— baretaroot subcommand menu.picker_project.gif/.txt— multi-category picker initial render.picker_filter.gif/.txt— filter mode narrowing leaves.picker_select_all.gif/.txt—spacetoggling all visible leaves in a group.picker_bootstrap_home.gif/.txt—ta init --bootstrap-homebootstrap picker.confirm_overwrite.gif/.txt— confirm prompt for overwrite.form_create.gif/.txt—ta createinteractive form.
Re-record with mage Vhs (requires the vhs binary on $PATH).
Apache-2.0 — see LICENSE.


