Connecting a CLI
TESSERACT is an MCP server as well as an MCP client. A terminal-side agent — Claude Code, Codex, or any MCP client — can connect to the running app and drive its governed verb surface, so work done in a terminal lands in the same memory, vault and workspace the assistant uses.
This page is for whoever is on the connecting end. Every verb call flows through the same permission, cost and audit path as an in-process tool call; no MCP route skips a gate.
TESSERACT also is an MCP server — a spec-compliant Streamable-HTTP endpoint embedded in the Mirror backend, so a terminal-side agent (Claude Code CLI, Codex CLI, or any MCP client) can connect and drive the assistant’s governed verb surface. This is the “Unreal Engine” model: the editor (Mirror) embeds the server; the CLI in the terminal is the occupant driving it. Every verb call flows through the same permission → cost → audit → ActivityRecord stack as an in-process tool call — no MCP path bypasses a gate.
- Endpoint:
http://127.0.0.1:8000/mcp— served on the Mirror backend’s socket (the MCP server is embedded in the Mirror app; port =mirror.yaml::server.port). Local-only. - Auth: bearer token from
$TESSERACT_MCP_SECRET(the operator client). Default-deny: an unknown token is rejected. - Transport: Streamable-HTTP — POST JSON-RPC (
initialize→tools/list→tools/call). GET withAccept: text/event-streamopens the server-push stream (below). DELETE ends the session. - Session:
initializereturns anMcp-Session-Id= themcp_sessionActivity record (“who’s in the chair”); killable viaactivity.cancel. - Orientation:
initializealso returns serverinstructions— a live brief telling the connecting CLI what the memory and vault actually hold (current entry counts and date range), which verbs are worth reaching for, and thatapp/andruntime/are not to be written. It is computed per handshake, so it never goes stale. - Tools: the verbs across
activity / memory / vault / workspace / diary / feedback / lane / schedule / surface / budget / agentfamilies (dots → underscores, e.g.activity_list). ASK-posture verbs return anawaiting_operatorresult — approve in Mirror, then re-issue. - One shared base. The point of the surface is that work done in a CLI is not lost when that session ends.
workspace_post/workspace_reply/workspace_readare the thread the operator reads, in both directions;memory_recallandmemory_getread back what earlier sessions established;memory_save/memory_update/memory_promote/memory_forgetcurate rather than only accumulate;diary_appendcarries the narrative;feedback_proposerecords a lesson as a proposal that changes nothing by itself;vault_ingestfiles research. Whoever did the work — the assistant, a delegated CLI, or a terminal you opened yourself — the next turn sees it. workspace_askasks the operator a question without blocking on it. It posts a card they answer in its comment thread; nothing pushes the answer back, so pollworkspace_readwith the event id you got. Do other work meanwhile — that is the point of it over stopping and waiting.lane_turnsends a task to another lane and returns its completed reply in one call, rather thanlane_sendfollowed by pollinglane_read.surface_openis the one to reach for when you want the operator to see something: a URL, a file, a folder, an app, or a search phrase. It resolves the target and renders it in the cockpit when it can, or opens it in the owning application when it can’t, and the result says which. Use it instead ofsurface_spawnunless you are authoring a card from content you generated.
What is yours, and what is shared
Work belongs to the client that created it. A lane you open is owned by the principal your bearer token resolves to (lane-claude, lane-codex, terminal-manual for what TESSERACT spawns; operator for a tool you connected yourself) and another client cannot read it, send to it, turn it, attach to it, interrupt it or close it. activity_list and the SSE stream show you your own records and nothing else. The check runs in the controller daemon, not in the Mirror, so it holds on the raw IPC path too — Mirror-side filtering would be usability, not a boundary.
Collaboration is deliberate rather than default: lane_ensure takes shared_with, naming the principals allowed in alongside you. The operator principal reaches everything on purpose — these are one operator’s collaborating workers, and an operator locked out of a lane they can see in the cockpit is the worse failure.
What this is not. It stops accidental cross-client reach; it is not tenant isolation. Lanes have filesystem access, and the bearer token proves that a gateway is speaking rather than which principal it speaks for. Treat another client’s work as none of your business, not as unreachable.
How a CLI gets connected
First, the surface has to be on. TESSERACT ships with it switched off — it is the one door through which another program reaches everything the assistant knows, so it opens deliberately. The switch is in Settings → Keys → MCP and takes effect on the next start. Until then /mcp answers that it is off, and that applies to the CLIs in TESSERACT’s own terminal as much as to anything outside.
With it on, registration is automatic and nobody has to type it. Opening a terminal pane, starting a lane, or running a delegate provisions both CLIs at user scope — ~/.claude.json (mcpServers.tesseract) and ~/.codex/config.toml ([mcp_servers.tesseract]). Neither file records a secret; both reference the bearer token by environment-variable name.
The tokens those names point at are TESSERACT’s own, and it mints them for itself on first start. lane-claude, lane-codex and terminal-manual are identities the runtime issues to processes it spawns, not credentials anyone types — which is why they appear in no settings screen.
That environment variable is the real gate. Panes and lanes are children of the Mirror backend and inherit it; a shell the operator opened from the desktop does not. So a claude or codex launched inside TESSERACT wakes up connected from whatever directory it is in, and the same binary launched outside sees no hub.
In a connected session the verbs appear as mcp__tesseract__memory_search, mcp__tesseract__activity_list, and so on.
Manual registration, if a client ever needs it:
export TESSERACT_MCP_SECRET=<the operator token — Settings → Keys → MCP mints one and shows it once>claude mcp add --scope user --transport http tesseract http://127.0.0.1:8000/mcp \ --header "Authorization: Bearer $TESSERACT_MCP_SECRET"Watching activity instead of polling
activity_list is a snapshot. Between two polls a lane can spawn, run and close without ever appearing in one — so a client that only polls is not merely late, it is blind to transitions. activity.watch is the subscription that fixes it, and it is not a tool: it is the GET SSE stream on the same endpoint.
curl -N http://127.0.0.1:8000/mcp \ -H "Authorization: Bearer $TESSERACT_MCP_SECRET" \ -H "Accept: text/event-stream" \ -H "Mcp-Session-Id: <the id initialize returned>"Each frame is a JSON-RPC notification, notifications/tesseract/activity, carrying the same record shape activity_list returns. You see only your own work and whatever was shared with you — the stream applies the same ownership filter as the snapshot.
Reconnecting with Last-Event-ID: <the last id you saw> replays exactly what you missed. If it cannot — the cursor is older than the retained window, or it came from a previous run of the backend — you get notifications/tesseract/activity_gap instead, telling you to re-hydrate with activity_list. A gap notice is never silent, and a partial history is never served as a complete one. The handshake advertises all of this under capabilities.experimental["tesseract/activityStream"].
TESSERACT v1.1.3 — documentation built 9 Oct 2026