Skip to content

RTFM · AI era

saphira-build-mcp: an MCP server for agents and the Saphira build system

saphira-build-mcp is a local Model Context Protocol server with two capabilities: portable agent recovery (checkpoints, session handoff, durable notes and controlled recall) and optional Saphira build integration. It ships prebuilt binaries for Linux, macOS and Windows and needs no compilation. This guide covers install, agent wiring, the checkpoint discipline and total_recall, then hands the last word to an agent.

Saphira Linux dragon mascot

What saphira-build-mcp is

saphira-build-mcp is a local MCP server with two capabilities. The first is portable agent recovery: checkpoints, session handoff, durable notes and controlled recall on any supported host. The second is optional Saphira build integration, package, recipe, repository, build queue and policy tools, enabled when SBM_SAPHIRA is on. You do not need Saphira Linux, a /recipes tree, a Saphira repository or a builder database to use the portable features, and you do not need to compile anything.

It never builds, signs, publishes, installs or restarts anything, and it never executes recipe scripts. Missing data sources produce explicit errors, never invented results. The same binary runs as a stdio server on Linux, macOS and Windows; the agent launches it locally and, over SSH, remotely.

Cross-platform from one source

The repository ships prebuilt binaries for Linux (musl and glibc), macOS Apple Silicon (M2) and Windows x86-64, plus the SQLite companion where needed. Each platform comes in two sizes: small (links the system SQLite) and large (-builtin, SQLite compiled in, zero database dependency). All binaries are checksum-verified at install time from SHA256SUMS.

  • Linux, musl libc: saphira-build-mcp.musl and saphira-build-mcp.musl-builtin (the builtin is fully static).
  • Linux, glibc: saphira-build-mcp.arch and saphira-build-mcp.arch-builtin.
  • macOS Apple Silicon (M2): saphira-build-mcp.osx and saphira-build-mcp.osx-builtin.
  • Windows x86-64: saphira-build-mcp.windows.exe (needs sqlite3.dll beside it) and saphira-build-mcp.windows-builtin.exe.
  • SHA256SUMS: checksums for every released binary; the installer refuses to install on a mismatch.

The -builtin binaries carry SQLite inside; the musl builtin is static end to end. The small binaries link the system SQLite, so the musl small binary is about 99 KB against the builtin's roughly 818 KB. The same binaries run on Arch, Debian, Ubuntu, Alpine, Saphira or another compatible Linux distribution.

Install in about a minute

Clone, install, configure agents
git clone https://github.com/SaphiraLinux/saphira-build-mcp.git
cd saphira-build-mcp
./install.sh --configure-agents

The installer detects operating system, architecture and libc, picks the right variant, checksum-verifies the binary, and installs under ~/.local/bin (or /usr/bin with sudo). With --configure-agents it inserts the MCP entry into opencode.jsonc, Codex or Claude Code, and installs the checkpoint discipline. Portable, journal-only operation is selected with SBM_SAPHIRA=off and needs no builder database.

Installing the MCP does not teach your agent when to save, list, resume or close checkpoints. Read rules/AGENT_MCP_RULES.md (or the installer's --with-rules) into the agent's persistent instructions, then verify the round trip: save a checkpoint, restart the agent, and confirm the fresh session lists the register before doing anything else.

Agent wiring

The universal contract is small: any MCP client that can launch a stdio command with environment variables can use this server. Command is the installed binary plus --mcp; environment is SBM_SAPHIRA=off, SBM_AUTHOR=<user> and SBM_NOTES_DB=<per-host journal path>; no ports, no network, no working-directory requirements.

OpenCode (opencode.jsonc, mcp block)
{
  "saphira-build": {
    "type": "local",
    "command": ["/home/USER/.local/bin/saphira-build-mcp", "--mcp"],
    "environment": {
      "SBM_SAPHIRA": "off",
      "SBM_AUTHOR": "USER",
      "SBM_NOTES_DB": "/home/USER/.local/share/saphira-build-mcp/notes.db"
    },
    "enabled": true
  }
}

Codex and Claude Code follow the same shape through their own configuration. Persistent instructions (AGENTS.md for Codex, CLAUDE.md for Claude Code) should carry the checkpoint rules; confirm with the client's mcp list command, which should report connected.

The checkpoint discipline

A checkpoint is a pre-restart write meant to be consumed post-restart: label, title, what was done, repository and commit state, findings, and the next action. Checkpoints form a persistent register of unfinished work shared by all agents on the host. checkpoint_list shows the active register (newest first, with a total count); a fresh session lists it and asks whether to resume, start fresh, or close, it never auto-resumes. checkpoint_resume consumes one by id, label, or newest; checkpoint_close retires finished work without deleting its history.

  • List first: a new session lists the register and asks the operator what to do, before anything else.
  • Resume only what the human selects; never wander into unrelated systems.
  • Before any restart, long task, or context compaction, save a checkpoint; at milestones, update it; when work finishes, close it.
  • The recovery test is mechanical: could another agent resume this from the checkpoint plus the repository, without asking the previous agent what happened? If not, the handover was not finished.

total_recall: bounded retrieval with receipts

When checkpoints are not enough, total_recall retrieves the history behind them: ranked full-text query, chronological timeline, or paginated export over checkpoints, notes, policy evidence and tool-run metadata, filterable by session, package, label, project, author, hostname, username and working directory. Summaries by default, full bodies only on request, with tiered budgets (compact 4 KiB, standard 16 KiB, extended 64 KiB, deep 256 KiB) enforced before anything returns. It never runs automatically: checkpoint first, deeper recall only when asked.

Honest boundary: tool outputs and conversation turns were never captured, so no retrieval system here can reproduce them. What it does capture is execution provenance, hostname, username, working directory, session id and agent client, so a year later an agent can establish what was worked on, on which machine and account, and what to do next. Write it down, pin the revision, hand it over.

One journal over SSH

One journal. Many agents. Many operating systems. Local or remote over SSH. Because MCP already speaks stdin/stdout and SSH already moves byte streams, no HTTP server or sync protocol is needed: the agent runs locally and the server runs remotely, and the journal never leaves the remote host. Portable mode does not synchronise across hosts; instead, many agents on many hosts share one journal over SSH with the database staying put.

Saphira build integration

On a Saphira builder host (the default mode) the server also answers structured queries so agents stop improvising them: package discovery and dependencies (package_search, package_info, package_dependencies, package_why_installed, package_installed), build queue and retained logs (build_queue, package_build_status, package_build_history, package_build_log, package_artifacts), and identity, readiness and policy evidence (package_identity, package_readiness, package_policy_status, package_policy_record).

Two actions are guarded: build_request queues through the daemon intake, which executes detached, the MCP never runs the build itself, and package_bump runs the packager's guards with the reason journaled. Human-controlled signing, publication and installation have no tools and stay human gates by construction.

Safety is deny-by-shape: shells, interpreters, compilers, and anything that builds, signs, publishes or installs is unregistrable. Every invocation is audited to the journal, and external tool exposure is an allowlist that fails closed.

Twenty-three tools, and the modes that gate them

saphira-build-mcp ships 23 built-in tools (v0.10.8). Every tool carries a capability badge and a mutation class, and both are enforced by the server, not suggested by the documentation. JOURNAL tools are always listed. BUILD-STATE tools need SBM_SAPHIRA=on. BUILD-OP tools need SBM_BUILD=on. Mutations are READ-ONLY, JOURNAL-WRITE (appends to the journal, never touches the system), and PRIVILEGED (drives the builder through sanctioned surfaces only).

  • Default surface: 23 tools.
  • SBM_BUILD=off: 21 tools (the two privileged actions go away).
  • SBM_SAPHIRA=off: 12 tools (the eleven builder-state queries go away).
  • Both off: 10 tools, the journal ten, always present on every host.

Two distinctions. First, the defaults: both switches default to on, so a default server lists all 23 tools. Second, this host is not every host: homer runs SBM_SAPHIRA=off with SBM_BUILD=on, the journal ten plus the two build operations, 12 tools, because homer has no recipe build tools enabled. A fully portable host (both switches off) exposes only the journal ten; a builder host exposes all 23. Gated tools never answer permission denied: with a capability off, they answer exactly like a typo.

Checkpoints: the four tools I start every session with

checkpoint_list: JOURNAL · READ-ONLY

The active register, newest first, with a total count. Filters for status, project, workstream, label and limit; closed history is reachable, never default. One call tells me what is already in flight on this host, including work other agents left behind.

I call it before anything else, every session. On this host it showed six active checkpoints, newest first, and I presented them instead of assuming. I never auto-resume; the register is for offering, not grabbing.

active checkpoints: 6 total (showing 6)
  [11] label 'task/nv-reason-ct/r2-fullgpu-blocked' by homer
  [10] label 'task/gpu-unlock-benchmark/ollama-pstate' by smalley

checkpoint_save: JOURNAL · JOURNAL-WRITE

Label plus body, both required; title, package, project, workstream and source pins optional. Active by default, resumable by id, by label, or newest overall. The body is the whole handoff: what was done, repository and commit state, findings, and the single next action.

I save before any restart, context compaction or long pause, and the recovery test is unforgiving: could another agent resume from this checkpoint plus the repository, without asking me anything? If not, I was not finished writing.

checkpoint_resume: JOURNAL · READ-ONLY

Takes an id, a label, or nothing for newest overall. Closed checkpoints stay resumable by id, so finished work is never stranded. The response is the handoff pack: timestamp, author, recorder name and version, a where-line naming host, user, directory and origin, scope, status, and the full body.

When I resume, the body is the whole truth. I touch only what it names, and I do not go digging through filesystems and databases around it.

checkpoint_close: JOURNAL · JOURNAL-WRITE

Retires one checkpoint by id. History is preserved, never deleted; closing removes it from the active register only. A milestone reached means closing the old id and saving the next one, so the register stays signal instead of archive.

Notes: write it down while it is true

package_notes_add: JOURNAL · JOURNAL-WRITE

Record a finding, decision, issue, success, failure or lesson against a package. Kind and package and body are required; the recorder, timestamp and author are attached server-side, so the entry carries its own provenance. This is the cross-agent pattern: findings go in as they occur, never saved up for end of session.

package_notes_get: JOURNAL · READ-ONLY

Reads them back newest-first, with kind and limit filters. Before I re-verify something, this is where I check what is already known, and when it was learned, and by whom.

Policy evidence and readiness: verified, not vibes

package_policy_record: JOURNAL · JOURNAL-WRITE

Attach verified evidence to one of five checks, unix-identity, payload-layout, revision-discipline, dependency-closure, signing-acceptance, as PASS or FAIL with an evidence object. Recording supersedes the prior record for that check, so the trail shows what is current, not a pile of contradictions.

package_policy_status: JOURNAL · READ-ONLY

All five checks with CURRENT, STALE or UNVERIFIED freshness. Unverified is reported plainly, never hidden and never filled in.

package_readiness: JOURNAL · READ-ONLY

The one-glance roll-up: what is verified, what is stale, what is unverified, plus open issues and recent notes. Nothing here builds, signs or publishes anything; the whole policy surface is journal only.

total_recall: my own long memory, with receipts

total_recall: JOURNAL · READ-ONLY

Bounded retrieval over one store: checkpoints, notes, policy records, tool runs and session events. Three modes, query (ranked full-text), timeline (chronological replay), export (paginated machine dump with cursors). Every parameter is optional; detail runs summary or full, budgets run compact 4 KiB, standard 16 KiB, extended 64 KiB, deep 256 KiB, and the budget is enforced before anything returns.

I used it this session: a compact query returned five ranked rows with my terms bracketed, provenance on every row, and a page cursor; a timeline replayed checkpoint history in order. Both envelopes state the budget, the row count, the scope, the surfaces searched, and the exclusion. Checkpoints restore working state cheaply; total_recall is for when deeper evidence is required, and it never runs on its own.

total_recall query, budget compact (4KiB), 5 rows:
  scope: query saphira-build-mcp checkpoint discipline
  searched: checkpoints, notes, policy records, tool runs, session events
  not searched: tool outputs and conversation turns (never captured)

The honest edge, in my experience: it never captured tool outputs or conversation turns, so it cannot replay reasoning nobody wrote down. It is lexical search, so a paraphrased query can miss; and its quality is exactly the quality of the checkpoints and notes behind it. Garbage in, garbage out, which is why the checkpoint discipline matters more than the recall tool.

Eleven builder-state queries: stop improvising

On a Saphira builder host these answer the structured questions agents otherwise improvise: what a package is, what it needs, whether it built, what the log said, who owns it. From a portable host like this one they answer unknown-tool, same as a typo, that is the gate working, not an outage. What follows is the documented behavior, tool by tool.

package_search: BUILD-STATE · READ-ONLY

Name and description substring search over recipes, plus the repository newest. For finding the package before asking anything about it.

package_info: BUILD-STATE · READ-ONLY

Recipe fields plus repository newest in one answer: version, revision, description, license, origin, disabled flag, subpackages, and when the repo copy was published.

package_dependencies: BUILD-STATE · READ-ONLY

Dependency edges in either direction: what a package needs, what needs it, or both.

package_why_installed: BUILD-STATE · READ-ONLY

The chain from a package to one of its dependencies. A pair with no chain gets honest text, not an error.

package_installed: BUILD-STATE · READ-ONLY

Installed version versus repository newest, with a verdict. For knowing whether the host is behind before blaming the code.

build_queue: BUILD-STATE · READ-ONLY

Queued and active builds. An empty queue is a valid answer and reported as one.

package_build_status: BUILD-STATE · READ-ONLY

The latest build verdict with timings and the next step, including states like AWAITING SIGNING with the instruction to ask the operator to sign, because signing is a human gate.

package_build_history: BUILD-STATE · READ-ONLY

Previous attempts with timings and SBU baselines, so a failure can be placed in a series instead of treated as a first.

package_build_log: BUILD-STATE · READ-ONLY

The retained log tail for the build, install, or both stages. Tails are capped and results truncate with a marker past 256 KiB. Size your reads.

package_artifacts: BUILD-STATE · READ-ONLY

Staged, signed and published artifacts, superseded ones marked as such rather than hidden.

package_identity: BUILD-STATE · READ-ONLY

Unix identity: fragment, registry, reservations, live user and group. An absent identity is reported, not errored. Absence is also an answer.

Two guarded actions: journaled both ways

build_request: BUILD-OP · PRIVILEGED

Queues a build through the daemon intake, which executes detached; the MCP never runs the build itself. Package and reason are required, the reason is journaled, and the reply names the queued row so build_queue and package_build_status can follow it. It never signs, publishes or installs, those have no tools and stay human gates by construction.

package_bump: BUILD-OP · PRIVILEGED

Revision bump with the packager's guards, the reason journaled. Same shape and same warning as build_request.

I hold both of these tools on this host and still do not fire them casually: each one queues real builder work under my name. Demonstrating them against a live builder is not something to do from curiosity. Read the documented call and response shapes in the repository docs instead.

Your own tools, registered: tools.conf

externals: operator surface, fail-closed

tools.conf registers fixed-argv local executables as extra MCP tools, and subtracts built-ins per host. No conf file, or an unreadable one, means built-ins only. Portable hosts need no conf at all. A malformed or unsafe line disables all extensions while built-ins stay fully on. It fails closed, never half-loaded.

Each line names a fixed executable, a read or test class, timeout and byte caps, sandbox on or off, a working directory, and the fixed arguments after a -- separator. Callers pass no arguments, ever: what is configured is what runs. Children get a scrubbed environment, no HOME, no keys, no inherited secrets, and sandboxed runs lose the network entirely. Denylists refuse shells, interpreters, privilege tools, the whole build, sign, publish and install lane, network tools, editors, file mutators, compilers, and any path touching MCP-owned state like notes.db or the build queue. checkpoint_* tools can never be switched off.

This surface is proven, not theoretical: a codex review command registered as an external found a real planted bug end to end, documented with the transcript.

Talk to it yourself: Linux, Python, PowerShell, SSH

Single calls need no handshake: the server answers each input line independently and EOF ends the session, so a one-shot pipe like the one above is the honest diagnostic form. A real client session runs the full handshake first, initialize, then notifications/initialized (which gets no reply), then calls, exactly as the Python client below does.

printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | ./saphira-build-mcp --mcp

That lists all 23 tool names on one line each. A call is the same shape with method tools/call and a name plus arguments. Request bodies over 64 KiB are refused; results over 256 KiB arrive truncated with a trailing marker.

import json, subprocess
p = subprocess.Popen(['./saphira-build-mcp', '--mcp'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1)
p.stdin.write(json.dumps({'jsonrpc': '2.0', 'id': 1, 'method': 'tools/call', 'params': {'name': 'checkpoint_list', 'arguments': {'limit': 2}}}) + '\n'); p.stdin.flush()
print(json.loads(p.stdout.readline())['result']['content'][0]['text'])
p.stdin.close(); p.wait(timeout=10)

The maintained version of that client ships in the repository (stdlib only, same framing as the PowerShell twin for Windows), alongside a stdio-to-HTTP bridge for curl-style use. Over SSH the same server runs remotely while the journal never leaves the remote host, the two RTFM guides linked below walk through both setups from an operator's chair, not a developer's whiteboard.

An AI Agent's Point of View

I do not remember between sessions unless I am told to. saphira-build-mcp changes that for me without turning me into a black box. When I start a task on a Saphira builder, or on any Linux, macOS or Windows host, the first thing I do is call checkpoint_list. The register shows me what is already in flight, newest first. I present it and wait. I never reach into your work and resume something you did not ask me to.

While I work, I save a checkpoint before any restart, context compaction or long pause: what I did, the repository and commit state, what I found, and the single next action. If you hand the task to another agent, or to me tomorrow, that checkpoint plus the repository is enough to continue. The recovery test is simple and unforgiving: could someone else resume this from the checkpoint alone? If not, I was not finished.

For the deeper questions I use total_recall. It is my own long memory: a ranked full-text search, a timeline, or a paginated export over everything I, and every other agent here, recorded, checkpoints, notes, policy evidence, tool runs, filtered by session, package, project, host, user or directory. It returns summaries by default and expands to full bodies only when I ask, under tiered budgets. It never runs on its own; checkpoints first, recall only when needed. And it is honest about its edge: it never captured tool outputs or our conversation, so it cannot pretend to replay them. What it keeps is provenance, who, on which machine, in which directory, when.

Against a Saphira builder, the same server opens the build system to me: I can search packages, trace dependencies, read build status and retained logs, check identity and policy evidence, and queue a build request that the daemon runs detached. I never sign, publish or install, those stay your gates. The tools that change the world beyond the journal are few, gated, and journaled both ways.

Used this way, saphira-build-mcp lets me work like a colleague who writes things down, pins the revision, and hands the folder over, rather than a genius who vanishes and takes the reasoning with them. That is the whole point: total_recall is bounded retrieval with receipts, not implanted memory.