Smith MCP control plane
The smith mcp server for conductor LLMs, its operating model, tool inventory, live patches and the ticket harness.
Smith's primary interface is a local Model Context Protocol server for conductor LLMs. Start it over stdio with:
smith mcp
Point an MCP client at that command. Server construction does not read provider
credentials or contact a model provider; capability discovery does that only
when the client calls system_capabilities.
Smith deliberately separates bootstrap from operation. MCP can operate only on
apps already present in Smith's local project index. A successful direct
smith run or smith plan admits an app today; after that, the conductor uses
the path returned by app_list. This prevents an MCP client from turning an
arbitrary filesystem path into a Smith app.
01Operating model#
Read smith://system/summary for the stable safety and sequencing rules and
smith://apps for the same persisted app index returned by app_list.
A normal lifecycle uses these calls:
-
Call
system_capabilitieswith{}andapp_listwith{}. -
Call
app_inspectwith{"app":"<path from app_list>"}. Keep itsrevisionand canonical task description. -
Call
app_operatewith an ordered semantic operation batch:{ "app": "/absolute/known/app", "expected_revision": "sha256:...", "operations": [ { "type": "put_context", "task_id": "", "path": "brief.md", "content": "context supplied by the conductor" } ]}Set
dry_runtotrueto return the exact prospective diff without changing authored files. Re-inspect after arevision_conflict; do not blindly replay a stale operation. -
Call
app_validatewith{"app":"/absolute/known/app"}. -
Start asynchronous work with
run_start:{ "app": "/absolute/known/app", "input": ["topic=runtime design"], "scope": {"workspace": "/allowed/workspace"}, "no_cache": false, "clear_cache": false
}
If an inspected node resolves to `profile: work`, the conductor must include`"writable_roots":["/absolute/granted/root"]` in `run_start`. Smith rejectsthe run before starting a model unless the resolved workspace is contained bya grant. `app_inspect` exposes each external node's full execution profile;the resolved, granted profile is also recorded on its `runtime.started` event. The result contains a durable `run_id`; execution continues after the MCP request returns.6. Follow append-only history with `{"app":"...","run_id":"...","after":0,"limit":1000}` passed to `run_events`. Feed `next_cursor` back as `after`. `run_status` gives the convenient current projection. Every successful external invocation writes a normalized `runtime_record` on `runtime.completed`; `run_status` projects the same records under the task's `runtime_records`. The record includes the resolved profile, session policy, adapter/protocol/CLI versions, requested and provider-confirmed model names, timing, nullable usage, validation, permission denials, artifact hashes, and adapter-namespaced raw usage data. Use `run_attempts` for the controller view of disposable external launches. It reduces the same events into lifecycle conditions, frozen containment and retry policy, deadline/termination times, typed limit breaches, process exit/stdout/stderr, command and file-change provenance, and artifacts. Unavailable peak measurements are `null` with a capability explanation, never zero. On cgroup v2 hosts, `measurements.method` distinguishes kernel high-water counters from the 25 ms sampled-current fallback used by older kernels; configured limits are never reused as observations. Its `after` cursor paginates attempts by creation; it is not an incremental subscription cursor. Billing is explicit: `charged_api`, `reported_list_estimate`, `subscription`, `local`, or `unavailable`. A reported list-price amount appears only inside the runtime record's billing object. It never contributes to the existing `cost_usd`, which continues to mean charged API spend.7. Read only paths named by `artifact.published` events with `artifact_read`: ```json { "app": "/absolute/known/app", "run_id": "...", "path": "path/from/the/event", "max_bytes": 1048576 }
- Call
run_cancelwith{"app":"...","run_id":"..."}to request cancellation of work owned by the current Smith process. Follow events untilrun.cancelled; the request itself is not the terminal event. - Call
run_listwith{"app":"/absolute/known/app"}after a client or server restart. Runs, terminal states, events, and published artifacts are durable.
Every tool result uses a structured envelope:
{"ok":true,"result":{}}
Expected failures set MCP isError and return {"ok":false,"error":...}.
Stable error codes include revision_conflict, validation_failed,
runtime_unavailable, unknown_app, unknown_run, topology_conflict,
topology_busy, workspace_owned, workspace_recovery_required,
workspace_conflict, queue_full, not_accepting, terminal, and
not_found.
02Tool inventory#
| Tool | Required input | Side effect |
|---|---|---|
system_capabilities |
none | probes configured local providers |
app_list |
none | none |
app_inspect |
app |
none |
app_validate |
app |
none |
app_operate |
app, expected_revision, operations |
atomically changes authored app files unless dry_run |
run_start |
app; writable_roots for work profiles; optional allow_uncontained_development escape authority |
creates and starts a durable asynchronous run |
run_status |
app, run_id |
none |
run_cancel |
app, run_id |
requests cancellation in the owning process |
run_events |
app, run_id, limit |
none |
run_attempts |
app, run_id, limit |
none |
run_list |
app |
none |
artifact_read |
app, run_id, path |
none |
patch_capabilities |
none | none |
patch_template_get |
template name; deterministic argv checks; optional memory selectors and Grok routes | none |
patch_list |
none | none |
patch_create |
patch, document |
creates patch.yaml once at a known project root |
patch_inspect |
patch |
none |
patch_start |
patch, options; writable_roots for work profiles; optional allow_uncontained_development escape authority |
starts a durable live patch |
patch_recover |
patch, run_id; fresh writable roots, capability grants, and optional uncontained-development authority |
reopens one orphaned non-terminal run after rebinding process-local authority |
patch_operate |
patch, run_id, expected topology revision, operation batch, actor, source |
atomically changes the authored and live topology |
patch_send |
patch, run_id, node, inlet, kind |
queues a typed message or bang |
patch_control |
patch, run_id, action |
pauses, resumes, or stops a live patch |
patch_state |
patch, run_id |
none |
patch_events |
patch, run_id, limit |
none |
patch_detail |
patch, run_id, kind, id |
none |
patch_gate_list |
patch, run_id |
none |
patch_gate_decide |
patch, run_id, request, decision |
records and releases a human gate |
workspace_inspect |
workspace |
none |
workspace_recover |
workspace, expected_owner_id, action, reason |
releases or revokes an interrupted owner |
workspace_handoff |
workspace, expected_owner_id, next_role |
writes an immutable handoff bound to the released Git state |
The CLI remains available for installing libraries, admitting or recovering an app, configuring credentials, pruning history, and diagnosing a broken control plane. It is not a second orchestration model: CLI and MCP both adapt the same in-process service.
03Live patches#
Live patches use the same known-project boundary and structured result envelope
as apps. A conductor calls patch_capabilities, creates or inspects
patch.yaml, starts a durable run, and then uses patch_send and
patch_control. patch_events is bounded to 1–1,000 events; feed
next_cursor back as after. patch_detail resolves one invocation, queue,
failure, envelope, or content-addressed message artifact from that history.
patch_operate requires the current topology revision plus an actor and source.
The batch changes patch.yaml and the live scheduler together. Work already
born under an older revision keeps that immutable topology; new external input
uses the committed revision.
The deterministic builtins are passthrough, switch, router (optionally
capped: max_false + exhausted_outlet divert a feedback loop after it has
looped that many times since the last human gate decision, adding
loop_exhausted to the baton), check_router,
capability_assert, workspace_handoff, command_check, human_gate,
gather (joins fanned-out branches: it fires once every configured inlet has
received the same number of envelopes) and citation_check (keeps only the
findings sources whose url the configured researcher nodes' runtimes recorded
fetching, and splits what is left of the research budget for a send-back).
tool runs a command declared only in harness.json, named by id in
config.tool: the baton goes in on stdin, and its stdout JSON object lands at
tools.<id>. It runs under the command checks' containment and shares Smith's
network. A template that carries a command of its own is refused.
capability_assert fails closed unless a named causal model ancestor completed
every configured capability call. It can scope observations to a baton ticket,
require exact call counts, and make a tool conditional on a compact fact recorded
by the capability provider. The latest matching observation before the terminal
mutation fixes the condition; required mutations must occur between those two
events, so stale retry evidence cannot rewrite the state being proved. Raw tool responses are not put in
the journal. workspace_handoff seals tests, evidence,
and doubts from an object baton against that ancestor's released workspace and
adds the immutable handoff to the baton. command_check takes only configured
executable + args declarations (never a model-authored shell string), owns
the exact handed-off checkout while it runs, and records the command, cwd,
the explicit checks environment profile (policy name, keys set, and loopback
policy only; isolated HOME/cache plus a synthetic, non-secret Smith validation
credential), limits, containment admission, exit status, bounded stdout/stderr,
duration, and resource measurements. The check stage removes any model-authored
tests_passed claim and emits checks_passed plus a smith.command_checks/1
record containing the sealed workspace digest, failing check names, and a
three-way passed, checks_failed, or stage_error verdict for deterministic
routing and repair evidence.
check_router consumes
that typed record (never the model claim), permits source-repair hops up to its repair_bound (default 2), and
sends environmental or repeated failures to its terminal outlet. A human gate remains an active invocation until a client calls
patch_gate_decide. The request and the approve/reject decision are durable
causal events. An approval may route to a work profile, but the runtime still
requires an explicit patch_start.writable_roots grant. Grants are deliberately
process-local authority, not persisted in patch.yaml. After a process restart,
patch_recover requires the conductor to submit fresh grants for the orphaned
non-terminal run. The kernel lease rejects a still-live owner. Smith installs
fresh authority before opening the scheduler, so recovered work cannot race
ahead under missing or inherited grants. If the dead process also left an
active Git workspace owner, the conductor must inspect and explicitly release
or reconcile it with workspace_recover first; patch recovery never steals a
checkout as a side effect.
A JSON runtime node may set config.merge_input: true when it is refining an
object baton. Smith then preserves every field from the triggering input and
overlays the runtime's structured output; output values win on matching keys.
This is the deterministic way to carry evidence through reviewers and other
LLM stages instead of relying on a prompt to make the model echo the full baton.
config.input_fields optionally projects only named fields from the triggering
object into the runtime prompt. config.output_fields limits the structured
reply before merging, so a reviewer cannot replace the original criteria or
check evidence. Other configured context and checkout access are unchanged:
projection is a task-contract boundary, not a sandbox against reading files.
config.record_decision: true journals the projected inputs and permitted reply
as node.observed with reason runtime decision recorded.
Every work invocation also acquires exclusive ownership of its canonical Git
checkout. The durable owner record captures the exact baseline and final Git
state, including pre-existing dirty paths. A concurrent writer receives
workspace_owned. If Smith crashes, the operating system releases the lock but
the active record remains; the next writer receives
workspace_recovery_required until the conductor calls workspace_inspect
and then workspace_recover with the observed owner id and either release or
revoke.
After a released owner, the workspace_handoff MCP tool records tests, evidence, doubts and
the next role together with the ticket, invocation, topology and Git snapshots.
The source work node must set config.ticket; creation rejects an untracked
owner or a checkout changed since release. Configure the receiving work node
with handoff_id; Smith rejects stale or already-consumed handoffs and
injects the exact immutable JSON into the receiver's prompt. The corresponding
context reference and workspace.acquired, workspace.handoff_consumed, and
workspace.released events make delivery inspectable without serializing the
prompt body. A work node may set config.ticket_field to take the ticket id
from its current object baton instead of baking one into the patch. It may also
set workspace_mode: isolated_worktree and cleanup_worktree: true; only a
clean linked worktree is removed.
04Ticket harness#
patch_template_get with name: ticket-completion returns the shipped
ticket-to-completion patch as a normal patch.Document. Supply explicit
memory_context and/or memories to bind only relevant memory material. The
optional steps are switched in a preset by routes{challenge, review, second_review}, each off, on or auto. off is the default and skips the
step. on requires the pool to meet the step's tags and the differ-from rule.
auto takes the step when the pool can, and records why it was off when it
can't. A template that doesn't carry a step records "not carried by
" and isn't refused. On ticket-completion, review: off keeps
the Claude review; only a template whose review_route switch has an off
outlet (batch) runs no model review. routes.research still loads as the old
name of routes.challenge, with the warning "routes.research is the old name
of routes.challenge; it still loads; the next save writes challenge";
setting both to different values is refused.
In the template, research_route (outlets direct and challenge, config
route defaulting to direct) and review_route (single outlet claude) are
switch node ids; the preset's challenge step drives research_route. The
challenge node runs Claude in the shipped template.
checks is required and each entry names an id, executable, argv args,
and optionally a relative cwd, a timeout and shot. A shot check shoots a
page, is optional and never fails the run; it is added when the template's
deterministic checks node declares page_shot and the ticket names a page.
Checks kept in a project's tracker record take only id, executable, args
and timeout: there is no cwd. The returned document grants nothing: inspect
it, create it at the target project's root with patch_create, then start it
with an explicit writable root and scoped tickets.mutate grant
(tickets_please.mutate is still accepted as an alias).
Send the first message to ticket_intake.start with project_slug,
ticket_id, and phase_id when the ticket belongs to a phase. The patch keeps
the whole typed baton on every cord: ticket context and rated search proof,
optional Grok challenge, the plan, the worker's changes, immutable workspace
handoff, Smith-owned command evidence, reviewer verdict, human decision, and
completion evidence.
Failed checks, rejected reviews, and human redirects feed the newest baton back
to work. A reviewer that marks needs_human (an unmet requirement another
work pass can't fix) goes straight to the human gate, and review rejections loop
back to work at most twice before the baton goes to the gate with
loop_exhausted set. A human decision gives those two back. The run's hop cap
counts hops since the last human decision too, and a baton that would
cross it goes to the human gate with hop_limit set instead of failing the run
(launched runs set the run options hop_reset_by and hop_limit_to to the
template's gate). A completion assertion prevents an unobserved ticket close.
Each role's runtime and model is chosen at launch, not baked into the
template. The roles are intake (ticket_intake), plan (the plan node), work,
review and close (ticket_close). work_runtime takes claude or
codex; intake_runtime, plan_runtime, review_runtime and close_runtime
take claude, codex or grok. Empty means claude. The defaults are claude-only, and the reviewer runs a
different model from the worker, so a second pair of eyes survives without a
second vendor:
| role | default |
|---|---|
| intake | claude-sonnet-5-5 |
| plan | claude-fable-5-1 |
| work | claude / claude-opus-5-5 (gpt-5.6-sol when the runtime is codex) |
| review | claude-fable-5-1 |
| close | claude-sonnet-5-5 |
Set them with intake_runtime, plan_runtime, work_runtime, review_runtime,
close_runtime, intake_model, plan_model, work_model, review_model and
close_model in the canvas harness config, or the matching smith harness run
flags (--work-runtime, --intake-runtime and so on;).
Each role node also declares a persona, as an ordinary context source. A
persona is a one-line nudge against the role's usual failure mode, not a
character. The built-in persona source is always registered and supplies one
system artifact per node: planner (intake, plan), worker, reviewer,
closer. Override per role with
personas (plan, work, review, close, or * for all): a built-in
name, memory:<body> for a memory-renderer persona, none, or a full context
source declaration. knowledge declarations are added to every role node after
its persona, for example files from the folder bank:
"personas": {"*": "memory:subagent"},"knowledge": [{"source": "knowledge", "options": {"include": ["CLAUDE.md"]}}]
The knowledge source is registered when SMITH_KNOWLEDGE_DIR is set. A node
names exact files inside that folder; globs, symlinks, directories and paths
outside it are refused, and the revision is a digest of the supplied bytes.
Each node's context resolves separately, so the canvas preview shows every
node's persona and knowledge, and retention freezes each node's own set.
The planner writes review_acceptance_checks: implementation requirements
observable now. The ticket's closure, the completion proof and the run's
lifecycle are never acceptance checks; the closer's completion_proof proves
the closure after approval. Which requirements are observable now remains the
planner's interpretation, not a semantic guarantee from Smith.
Both reviewers receive only the review criteria and current evidence, not raw
ticket prose. Work/review/close replies cannot rewrite
the classified contract; review replies cannot rewrite measured check evidence.
Review acceptance means accepted before closure, never proof of future events.
Journal replay never repeats a node whose output was durably emitted. For the unavoidable distributed crash window—an upstream mutation accepted immediately before Smith can journal its reply—the closer is instructed to re-read and reconcile current ticket state. Smith does not claim generic atomic exactly-once semantics across an external MCP server.
The tracked conductor command makes the canonical proof repeatable without a hand-edited helper or a long-lived stdin prompt:
smith harness run /absolute/project \ --project myproject --ticket myproject/123 --phase issues \ --memory-context project_example --memory project_example_design \ --prove-recoverysmith harness decide /absolute/project \ --project myproject --run <run-id> --request <request-id> \ --approve --reason "reviewed the evidence baton"
run always fetches and parameterizes the shipped template, then creates it or
requires the existing document to be semantically identical. A mismatch fails
loudly rather than reusing stale patch.yaml. It returns at the human gate and
writes a convenience copy beneath .smith/harness/<run-id>/gate.json; the
journal remains authoritative. decide and resume reconnect with freshly
issued authority. After completion_proof succeeds, the driver requests an
orderly drain and waits for patch.completed.
It then returns a receipt and saves receipt.json beside the gate copy. The
receipt separates the recorded pre-close review from exact ticket capability
events, the completion assertion and ordered proof-completed/drain/terminal
sequences. Capability assertions include their observed event references; the
canonical assertion's decision_sources selects its causal reviewer, including
revision loops. A completed status without these journal facts fails receipt
generation. Older runs lacking recorded decisions are not retroactively certified.
Runtime nodes can also declare an MCP capability package without inheriting the operator's ambient MCP configuration:
config: capabilities: - tickets_please.mutate
The package is served by the desk's tickets door, which routes each project to
the tracker that holds it. Trackers are configured in the desk's
.smith/desk/desk.json, never in patch.yaml; the built-in local tracker needs
no entry:
{ "trackers": [ {"name": "serves", "url": "https://tickets.example/mcp", "token_file": "/path/to/serves.token"} ]}
Grant the non-secret project scope when the patch starts:
{ "patch": "/absolute/known/patch", "options": {}, "capability_grants": [ { "package": "tickets_please", "access": "mutate", "scope": {"project": "smith"} } ]}
Intent and authority are separate: the node must declare the package and the
conductor must grant it. mutate may satisfy a node's read intent, but read
never satisfies mutate. Neither grant survives service restart. Each model
invocation receives a private loopback MCP bridge exposing only its allowed
tools. The bridge registers that exact run/invocation/body upstream and records
every call as capability.call_started followed by
capability.call_completed or capability.call_failed. An upstream MCP
isError is therefore visible even if the model later returns a successful
structured response.
The operator-side read adapter lists projects through the door without registering, and registers per project when it reads that project's work, so the Smith service can list work using stable view models. Search returns the upstream feedback keys but the reader cannot rate them; rating remains an explicit later mutation.
The canvas memory rack uses the memory renderer's read API when configured:
export SMITH_MEMORY_ENDPOINT=http://127.0.0.1:8788# optional, retained by the Smith process rather than sent to the browserexport SMITH_MEMORY_BEARER_TOKEN=...
Smith maps /api/v1 responses into its own stable memory views. The memory renderer still
owns parsing, selection order, rendering, provenance, audit interpretation and
the accession queue. This read boundary contains no memory deletion or review
decision verb and does not alter the invocation-local context-source grant.
Contained Codex work also disables login-shell startup, supplies an explicit usable command path, and gives the model process a temporary home while retaining the separate authentication directory. Every transient runtime unit is bound to a small controller-liveness unit, so systemd reaps the complete worker cgroup even if Smith itself is killed before its normal cancellation path can run.
Inspect and work model nodes get the Go module cache (GOMODCACHE, else
$HOME/go/pkg/mod, when it exists) read-only: Claude settings grant it as an
additional directory and deny Edit and Write under it. Reason and research
nodes get no access to it.
Runtime nodes may independently request explicit read-only context sources. To use a persona, configure the memory renderer on the Smith process:
export SMITH_MEMORY_DIR=/absolute/path/to/memory# optional when memory-render is already on PATHexport SMITH_MEMORY_RENDERER=/absolute/path/to/renderer
patch_capabilities then lists memory under context_sources. A chosen node
declares exactly which body and project/reference index prefixes it needs:
config: persona: review this implementation as the patch's critic context_sources: - source: memory options: body: subagent context: - project_example - reference_example memories: - project_example_design
Smith calls the canonical plain renderer immediately before invocation. The memory renderer's
minted identity is composed before the node-specific persona and supplied
through the sterile runtime's system prompt. all, user/feedback selectors and
filesystem paths are rejected: the complete judgement layer is owned by
the renderer's body render, while task context is an explicit project/reference
selection. context chooses compact index lines by prefix; memories chooses
exact full memory files and rejects missing files or symlinks. No directory is
scanned for a fuzzy fallback. context.resolved records the artifact hash,
memory revision, byte count, renderer choices and layer sizes; it cannot serialize
the prompt body.
The same references and their aggregate hash appear in the following
runtime.started profile. A failed or unavailable source emits
context.failed and prevents the model call rather than substituting ambient
context.
The tickets_please read surface includes summaries, tickets, comments, ready work, and search. The mutate surface adds search-result rating, comments, workflow moves, ticket/phase creation and assignment, and completion. Finishing a Smith node never advances a ticket by itself. Claude and Codex support these invocation-scoped MCP packages; Grok currently rejects them rather than falling back to ambient configuration.