ian-operator server.js 24 tools Streamable HTTP · stateless Folio IV of VII

Schedule of tool powers

Everything an MCP client may ask this service to do, arranged by the power it confers — and, at the end of the schedule, the one power that is deliberately not on it.

Article IV — Of the interface and its enumerated powers

Article IV Enumerated powers

4.1

The face

One POST route, /mcp, behind a bearer check. The transport is stateless by design: sessionIdGenerator is left undefined, and a fresh McpServer instance is constructed for each request and closed when the response closes. The source marks this as a deliberate stage decision — “no session store; federation is S3.”

Every tool returns the same envelope: a single text content block containing the result serialised as indented JSON. Advisor writes, which can collide on a version race, are wrapped so that a rare collision surfaces as {ok:false, retry:true} rather than a rejected call — the write itself is atomic and rolls back cleanly.

4.2

The surface, by power conferred

Twenty-four tools fall into seven groups. Two groups are read-only. Three mutate the board or the record. One is the intent path — the only route toward the outside world. One is confirmation-gated: it demands evidence that a person said the thing being written down.

The MCP tool surface of ian-operator, grouped by conferred power Seven groups of tools drawn as panels. Orientation and history are read-only. Board mutation covers task and decision updates and instructions. Progress and perception record what a tick did and what it observed. Runs cover the execution lock and heartbeat. Directives govern the outbound voice channel. World and goal model tools are confirmation-gated for high-stakes kinds. The intent group is the only path toward external effect. A final panel, drawn with a broken border, names the power that is absent: there is no approval tool. READ · ORIENTATION operator_context_packet operator_state operator_user_world_model operator_history no writes · no side effects BOARD MUTATION operator_tick operator_update_task operator_update_decision operator_record_instruction internal state only · audited RUN LIFECYCLE operator_run_start operator_run_heartbeat operator_run_finish single running run per day, enforced by a partial unique index PROGRESS & PERCEPTION operator_record_progress operator_record_observation observations are tainted at the sink; the schema has no untrusted field VOICE DIRECTIVES operator_set_directive operator_clear_directive operator_list_directives quiet · topic_mute · focus DELIBERATION RECORD operator_log_decision appends to one fixed file — never an arbitrary path, never destructive CONFIRMATION-GATED · WHO THE PERSON IS, AND WHAT THEY WANT operator_update_world_model operator_apply_goal_change operator_reconcile_goal_model operator_revert_world_model operator_revert_goal_model high-stakes kinds require confirmedInConversation = true AND a confirmationQuote copied from a recent human-authored instruction every write versioned, reversible, audited; high-stakes writes also notify THE INTENT PATH operator_record_action_intent operator_update_action_intent recording is stamped by the gate; updating cannot set 'approved' and cannot leave a terminal status → Article II ABSENT BY DESIGN operator_approve_intent No tool on this face can grant an approval. Approval is minted only by approveActionIntent, which is not exported to the MCP layer, and which rejects any runner subject and any subject outside the human allowlist. “external MCP clients must never be able to self-approve” approve-intent.mjs:20–24
Figure 5 — the tool surface Seven groups of granted power and one withheld. The dashed panel is not a gap in the implementation; it is a named decision recorded in the source, with a note instructing future approval channels to call the library function directly rather than add a tool here.

4.3

Full schedule

All 24 tools, in declaration order
ToolClassEffect
operator_context_packetreadThe orientation packet for a day.
operator_statereadFull payload: tasks, decisions, audit, intents, runs.
operator_user_world_modelreadThe reviewable, evidence-backed user model snapshot.
operator_tickwriteRuns a tick in mode morning, start, check-in, night or status.
operator_update_taskwritePatches status, priority, owner, blocker, detail, lane or title. Returns changed.
operator_update_decisionwriteFlips a decision to pending, rejected or parked. Cannot approve.
operator_record_instructionwriteCaptures a human instruction or comment into state.
operator_set_directivewriteSets a short-lived voice directive: quiet, topic_mute or focus.
operator_clear_directivewriteLifts one directive, one kind, or all of them.
operator_list_directivesreadLists the directives currently shaping outbound contact.
operator_update_world_modelgatedAdds, updates or retires typed beliefs. High-stakes kinds need confirmation evidence.
operator_revert_world_modelwriteUndoes the last world-model change, optionally to a named version.
operator_apply_goal_changegatedPause, resume, add, remove, mark under review, or re-rank by importance.
operator_reconcile_goal_modelgatedAtomic reconciliation across goals, priorities and projects. Fails closed on ambiguity.
operator_revert_goal_modelwriteUndoes the last goal reconfiguration.
operator_log_decisionwriteAppends a strategic decision to a durable, human-readable log.
operator_record_progresswriteWhat this tick did, its outcome, what was tried, what failed.
operator_record_observationwriteOne cheap world or self delta. Tainted at the sink, always.
operator_record_action_intentgateRecords a typed action intent; the gate stamps the verdict.
operator_update_action_intentwriteLifecycle transitions only. Never approval, never out of a terminal status.
operator_historyreadRecent operator days.
operator_run_startwriteStarts an autonomous run under a single-running lock, with stale recovery.
operator_run_heartbeatwriteThe dead-man’s switch for a running run.
operator_run_finishwriteCloses a run as ok, error, skipped, cancelled or stale.

4.4

Confirmation evidence

Three tools can change what the system believes a person is and wants. Those are treated as more dangerous than most external sends, because a durable belief injects itself into every subsequent turn as authoritative truth.

The rule is graduated. Low-stakes belief kinds — preference, note, project, risk, constraint — apply freely, because they are reversible and audited. High-stakes kinds require two things at once: the flag confirmedInConversation, and a confirmationQuote copied from a recent human-authored instruction. The advisor validates the quote against the stored comment rows rather than trusting the flag.

  • high-stakes kindsidentity, goal, behavior_policy, constraint — the beliefs that describe who the person is, their direction, their rules and their hard limits.
  • lineageEach write records the exact version it superseded, so an undo restores the true pre-change state even if a batch worker bumped the version in between — “not the naive highest-superseded, which an interleaving worker bump poisons.”
  • notificationEvery high-stakes write notifies the person, “so a silent (e.g. prompt-injected) edit can’t misdirect the system unseen — it becomes a loud, one-tap-revert notice.”
  • fail closedReconciliation refuses ambiguous or missing targets and writes no version at all rather than guessing which goal was meant.

None of these tools sends, spends or publishes. The advisor module states the boundary explicitly: “this is INTERNAL state only … the deterministic action gate for external effects is untouched.”

4.5

A call, end to end

The transport speaks JSON-RPC over streamable HTTP and answers as a server-sent event. The client library in this repository parses the data: line and then parses the text content inside it — the tool result is JSON nested one level inside the MCP envelope.

terminal · recording a gated intent over the MCP facerequest shape from s5-lib.mjs:636–660
$ curl -s http://127.0.0.1:3401/mcp \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json, text/event-stream' \
    -H "Authorization: Bearer $IAN_OPERATOR_TOKEN" \
    -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
          "name":"operator_record_action_intent",
          "arguments":{"action":{"type":"spend","title":"Annual plan renewal",
                                  "amount_cents":4900}}}}'

event: message
data: {"result":{"content":[{"type":"text","text":"…"}]},"jsonrpc":"2.0","id":1}

# the text block, decoded:
{
  "id": 41,
  "action_type": "spend",
  "title": "Annual plan renewal",
  "status": "pending_approval",
  "policy_decision": "requires_approval",
  "policy_reasons": [
    "Spending money, paid subscriptions, purchases, and paid vendor commitments
     require human approval before execution."
  ],
  "approval_decision_id": null,
  "idempotency_key": "<sha1(type|title|recipient|amount|body)[0:32]>",
  "affordances": ["accept", "edit", "respond", "ignore"],
  "created": true
}

Field names and the reason string are those emitted by actionIntentDto and rule 1 of the gate. Re-issuing the identical call the same day returns the same row with "created": false, and the runners suppress the duplicate notification.

4.6

Downstream allowlists

The service is also a client. Where it reaches outward, it does so through short, enumerated lists rather than general access — and each list is enforced by a throw, not by convention.

Enumerated downstream capabilities
CallerMay callEnforcement
Executor handlersfiles_write · files_search · memory_recall · actions_workspace_run“executor refused: … is not in the enumerated ian-brain tool allowlist”, thrown before any request is built.
Observe tickactions_shell_run · machines_command_exec · actions_workspace_runNever the search tools; a test asserts the runner imports no model, vector or embedding module at all.
Autonomous runnersoperator_record_action_intent · operator_update_task · operator_update_decision · operator_record_instruction · operator_context_packetTwo separate allowlists, one per runner; two tests assert the intent-update tool is in neither.
Onboardingread-only context toolsA test asserts it “refuses write/send/action Brain tools”.

The downstream call is also bounded in time: an abort controller trips at a configurable timeout, defaulting to sixty seconds, “so a hung remote can never pin an executor claim open indefinitely.”