mago — an MCP harness for Claude Code
mago is a Model Context Protocol server I wrote in Go, backed by a single SQLite file. Claude Code launches the binary over stdio and gets seventeen tools for creating a task, moving it through a TDD cycle, reviewing it, and closing it.
It is not an agent framework. mago never spawns anything — it can’t, it’s a stdio server with no ability to start a process. It holds state and it says no.
The idea: rules at the write, not in the prompt
A process written into a prompt is a suggestion. The model follows it for a while and then, ten thousand tokens later, quietly doesn’t — it writes the implementation before the failing test, or marks something done that was never reviewed. Nothing catches it, because nothing was ever checking.
So mago’s whole premise is to move the process out of the prompt and into the
database. “Write the failing test first” isn’t advice in a system prompt; it’s a
task_advance call that returns an error if the previous stage wasn’t recorded.
The model can’t forget a rule it isn’t holding.
Four principles fall out of that:
- Server-side gates — rules live at the write, not in the prompt.
- Self-describing returns — every tool says what happened and what to do next.
- Derive, don’t store — compute what you can instead of persisting it.
- Keep the server git-blind — it stores worktree paths; git runs outside it.
The cycle
A task walks a fixed path, and each arrow is a tool call that can be refused:
task_create the brief — the original ask, verbatim
task_start opens a lane: a git worktree + a dashboard link
task_advance → design (root cause, file:line facts, alternatives)
task_advance → red (evidence: the actual failing test output)
task_advance → green (evidence: the actual passing output)
drift_check does the diff match the brief? (criteria, scope, test fidelity)
review_scope declare the diff shape → derives the round's checker roster
review_report ×{security, validity, coverage} — verbatim reports
review_consolidate an independent reader triages, proposes ship/revise
verify the artifact was actually run, at a declared tier
review_close ship | revise
task_update status=completed
The evidence isn’t decorative. task_advance to red wants the failing output
pasted in, because a claimed red run and an observed one are different things,
and only one of them is in the database afterwards.
Every tool returns the same envelope
{
"ok": false,
"reason": "no-lane",
"next": "open a lane with task_start first — it returns the worktree command and the clickable dashboard link, the enforced first step of every task",
"issues": ["task 42 has no open lane"]
}
next is the important field. The model routes on what the last reply told it to
do, not on a protocol it’s trying to remember from far up the context. A refusal
isn’t a dead end — it’s an instruction. This turned out to matter more than any
individual gate: it means a compacted or freshly woken session can pick the task
back up from the store alone.
The gates that were worth building
Lane first. Every stage move is refused with no-lane until task_start has
opened a worktree for the task. Work happens in an isolated lane or it doesn’t
happen.
The review round is a database trigger. Not a check in Go — a
BEFORE UPDATE trigger on tasks:
CREATE TRIGGER tasks_review_gate
BEFORE UPDATE OF status ON tasks
FOR EACH ROW
WHEN NEW.status = 'completed' AND OLD.status <> 'completed'
AND NOT EXISTS (
SELECT 1 FROM review_decisions d
WHERE d.task_id = NEW.id
AND d.id = (SELECT MAX(id) FROM review_decisions WHERE task_id = NEW.id)
AND d.decision = 'ship'
AND d.stage_log_id = COALESCE((SELECT MAX(id) FROM stage_log WHERE task_id = NEW.id), 0)
)
BEGIN
SELECT RAISE(ABORT, 'review gate: completion requires a ship decision on the current code (review_close after the last stage change)');
END;
A rule that spans tables can’t be a column CHECK, and I didn’t want it to be a
convention. Putting it in the schema means it holds even if every line of Go
above it is wrong.
Freshness is pinned, not assumed. Look at stage_log_id in that trigger.
Every report, consolidation and decision records which head of the code it was
made against. Change the code after a review passed and the pass silently
un-counts — the ship was for different code, so it isn’t a ship anymore. This one
gate removed a whole category of “reviewed, then edited, then merged”.
verify. The newest one. Tests passing proves the tests pass. verify asks a
different question: was the artifact actually run, and at what tier —
delivery (the whole thing, as it ships), slice (the changed unit against a
harness), static (build green, nothing behaviorally exercised), or na (say
why). It also demands a residual: what you did not observe and are shipping
on inspection anyway. Writing that field down is uncomfortable in exactly the way
it should be.
The dispatch model
The main Claude Code session is the lead, and the lead doesn’t do the work. Each stage — investigate, write the failing tests, implement, and each review seat — runs as a background subagent, so the lead stays free to talk while code is being written. On each completion it records the result through the gated tools and advances the stage.
Since mago can’t spawn agents, it does the next best thing: dispatch_open /
dispatch_close record that a subagent is running, so the dashboard can show an
agent running chip for work that produces no diff — investigation, and the
parallel review seats.
Watching it
The server embeds a small dashboard at 127.0.0.1:8787. The live git diff of the
lane’s worktree updates in real time as a subagent writes code, which is a
genuinely odd thing to watch. The step tracker trails by one — a stage is
recorded when it finishes, with its evidence — so the diff is the live signal
and the tracker is the audit trail.
It also serves /statusline, one plain line for the Claude Code status bar:
mago #17 [red] Status-line surface … http://127.0.0.1:8787/#lane-17 +1
Wired into ~/.claude/settings.json as a statusLine command, the active lane’s
deep link sits under the input box permanently instead of scrolling away in chat.
Why build this
Mostly to learn Go, SQLite and MCP properly — one deliberate piece at a time, understanding every line. It’s vibe coded like everything else here, which is the recursive part: the tool that enforces investigate → red → green → review was built through its own gates, one small task at a time. Every migration below 0019 had to survive the round the previous ones enforce.
Nineteen migrations and about 10k lines of Go so far. The source is on GitHub: mariovisnjic/mago-mcp.