← Back to projects

mago — an MCP harness for Claude Code

ExperimentActive· go, sqlite, mcp, claude-code, tooling

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:

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.