# MCP tools


cairn provides fourteen MCP tools. They are the wire surface an agent calls to plan in the task graph and resume into it: one observe tool, eight writes, and five reads. Each entry below mirrors the tool's registered schema, so the page tracks the served surface and does not drift from it.

Every tool carries a capability gate in its metadata: `:cairn/observe`, `:cairn/write`, or `:cairn/read`. The gate is the boundary, not the tool name — read and write are decided by capability, not by which tools are exposed (see [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities)). A call whose gate is not granted runs nothing.

Errors travel on two channels. Protocol faults — malformed JSON-RPC, an unknown tool, a missing required argument the schema rejects — surface as transport-level errors. Tool-execution failures surface as a normal tool result with `isError: true`, carrying the verbatim text the model reads and self-corrects from: `name is required.`, `No current task; create or select one first.`, and the rest quoted in the entries below. Every returned and error string on this page is copied from source; treat the punctuation as load-bearing.

Most tools resolve a target task the same way: the `task_id` argument when given, otherwise the [current task pointer](/cairn/concepts/the-current-task-pointer). When neither names a task, a current-scoped call fails with `No current task; create or select one first.` Task arguments accept a bare slug or a depot-prefixed one; the depot prefix is stripped before lookup.

## Tools by capability

| Tool | Capability | One line |
| --- | --- | --- |
| [`observe`](#observe) | `:cairn/observe` | Record a freeform observation on the current task. |
| [`task_create`](#task-create) | `:cairn/write` | Create a top-level task. |
| [`task_fork`](#task-fork) | `:cairn/write` | Create a child task and make it current. |
| [`task_link`](#task-link) | `:cairn/write` | Add a typed edge to a target task. |
| [`task_sever`](#task-sever) | `:cairn/write` | Remove a typed edge to a target task. |
| [`task_set_metadata`](#task-set-metadata) | `:cairn/write` | Set a free key/value on a task. |
| [`task_update_status`](#task-update-status) | `:cairn/write` | Set a task's status. |
| [`handoff`](#handoff) | `:cairn/write` | Scaffold a resumable handoff. |
| [`task_query_write`](#task-query-write) | `:cairn/write` | Run a TQ query that may mutate or define views. |
| [`task_search`](#task-search) | `:cairn/read` | Full-text search over observations. |
| [`task_get`](#task-get) | `:cairn/read` | Read computed task state. |
| [`timeline`](#timeline) | `:cairn/read` | Recent events for a task. |
| [`task_query`](#task-query) | `:cairn/read` | Run a read-only TQ query. |
| [`task_bootstrap`](#task-bootstrap) | `:cairn/read` | Orient on a task in one call. |

## Tool reference

The entries below run observe, then the write tools, then the reads. Each one uses the same field order: summary, capability gate, signature, parameters, returns, semantics, example, see also.

### observe

Record a freeform observation on the current task. This is the cheapest call and the work heartbeat.

Capability gate: `:cairn/observe`.

Signature: `observe(text, task_id?)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `text` | string | yes | The observation. Blank text fails with `text is required.` |
| `task_id` | string | no | Task to observe on. Defaults to the current task. |

Returns: `Observed on <task>.` Records one `observation` event.

Semantics: an observation never moves the current pointer; it appends to the task you are already on (or to `task_id`). Observations are the corpus [`task_search`](#task-search) ranks over.

Example: `observe(text="bm25 ranking confirmed against t/cairn-search.lisp")` on task `2026-06-22-tune-search` → `Observed on 2026-06-22-tune-search.`

See also: [The cairn-method](/cairn/concepts/the-cairn-method) — [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs).

### task_create

Create a top-level task. Adopts the new task as current only when no current task is set.

Capability gate: `:cairn/write`.

Signature: `task_create(name, description?)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | Descriptive task name. Blank fails with `name is required.` Slugified and stamped with today's UTC date; a name too thin to slugify is rejected. |
| `description` | string | no | Longer description stored on the task. |

Returns: `Created <slug>.` Records one `task.create` event.

Semantics: the slug is `YYYY-MM-DD-<slugified-name>`. Minting is idempotent over the date prefix — a name you already date-prefixed is stripped back to its core before today's prefix is stamped, so the prefix stays singular. The task becomes current only if none was set; an existing pointer is left undisturbed.

Example: `task_create(name="tune search ranking")` with no current task → `Created 2026-06-22-tune-search-ranking.`

See also: [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier) — [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs).

### task_fork

Create a child task linked to a parent and make the child current. The default edge is `phase-of`, so forking builds a plan's phase backbone.

Capability gate: `:cairn/write`.

Signature: `task_fork(name, from?, edge_type?, description?)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | Child task name. Blank fails with `name is required.` |
| `from` | string | no | Parent slug. Defaults to the current task; with neither, fails with `No parent task; pass from or select a task first.` The parent argument is `from`. |
| `edge_type` | string | no | One of `phase-of`, `depends-on`, `related`. Defaults to `phase-of`. An out-of-enum value fails with `"<value>" is not a valid edge type; expected one of phase-of, depends-on, related.` |
| `description` | string | no | Longer description for the child. |

Returns: `Forked <child> from <parent> (<edge>).` Records `task.create` for the child and `task.fork` on the parent.

Semantics: `task_fork` always switches the current pointer to the new child. A task cannot fork from itself; that fails with `A task cannot fork from itself.`

Example: `task_fork(name="add prefix query support")` while current on `2026-06-22-tune-search` → `Forked 2026-06-22-add-prefix-query-support from 2026-06-22-tune-search (phase-of).`

See also: [Plan and resume](/cairn/get-started/plan-and-resume) — [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier) — [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields).

### task_link

Create a typed edge from the current task to a target task.

Capability gate: `:cairn/write`.

Signature: `task_link(target_id, edge_type, task_id?)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `target_id` | string | yes | Edge destination slug. Blank fails with `target_id is required.` |
| `edge_type` | string | yes | One of `phase-of`, `depends-on`, `related`. An out-of-enum value fails with `"<value>" is not a valid edge type; expected one of phase-of, depends-on, related.` |
| `task_id` | string | no | Edge source. Defaults to the current task. |

Returns: `Linked <src> -> <target> (<edge>).` Records one `task.link` event.

Semantics: links the resolved source to `target_id`. A task cannot link to itself; that fails with `A task cannot link to itself.` Use `depends-on` for lateral ordering inside a plan and `related` for cross-references; `phase-of` names structural parentage. See [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields) for the closed enum.

Example: `task_link(target_id="2026-06-22-tokenizer-fix", edge_type="depends-on")` on `2026-06-22-add-prefix-query-support` → `Linked 2026-06-22-add-prefix-query-support -> 2026-06-22-tokenizer-fix (depends-on).`

See also: [`task_sever`](#task-sever) — [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields) — [The task graph](/cairn/concepts/the-task-graph).

### task_sever

Remove a typed edge from the current task to a target task.

Capability gate: `:cairn/write`.

Signature: `task_sever(target_id, edge_type, task_id?)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `target_id` | string | yes | Edge destination slug. Blank fails with `target_id is required.` |
| `edge_type` | string | yes | One of `phase-of`, `depends-on`, `related`. An out-of-enum value fails with `"<value>" is not a valid edge type; expected one of phase-of, depends-on, related.` |
| `task_id` | string | no | Edge source. Defaults to the current task. |

Returns: `Severed <src> -> <target> (<edge>).` Records one `task.sever` event.

Semantics: the inverse of [`task_link`](#task-link) — it removes the named edge. Severing is recorded as its own event, so the graph's history shows when an edge was dropped.

Example: `task_sever(target_id="2026-06-22-tokenizer-fix", edge_type="depends-on")` on `2026-06-22-add-prefix-query-support` → `Severed 2026-06-22-add-prefix-query-support -> 2026-06-22-tokenizer-fix (depends-on).`

See also: [`task_link`](#task-link) — [The task graph](/cairn/concepts/the-task-graph).

### task_set_metadata

Set a free key/value on a task — for example `display-name`, `phase`, `objective`, `acceptance`, or `tags`.

Capability gate: `:cairn/write`.

Signature: `task_set_metadata(key, value, task_id?)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `key` | string | yes | Metadata key. Blank fails with `key is required.` |
| `value` | string | yes | Value to store. Required by the schema; a null value is coerced to the empty string rather than rejected. |
| `task_id` | string | no | Task to annotate. Defaults to the current task. |

Returns: `Set <key> on <task>.` Records one `task.set-metadata` event.

Semantics: metadata is open — any key is accepted, and setting an existing key overwrites it. Most keys are read back as open text. A handful map to typed [queryable fields](/cairn/reference/edges-statuses-and-fields) (for instance `display-name`); the rest stay free-form.

Example: `task_set_metadata(key="acceptance", value="prefix queries return ranked snippets")` on `2026-06-22-add-prefix-query-support` → `Set acceptance on 2026-06-22-add-prefix-query-support.`

See also: [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields) — [Tasks, observations, handoffs](/cairn/concepts/tasks-observations-handoffs).

### task_update_status

Set a task's status. Idempotent.

Capability gate: `:cairn/write`.

Signature: `task_update_status(status, task_id?, reopen?)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string | yes | One of `open`, `active`, `completed`, `abandoned`, `blocked`. Missing fails with `status is required.`; out of enum fails with `"<status>" is not a valid status; expected one of open, active, completed, abandoned, blocked.` |
| `task_id` | string | no | Task to update. Defaults to the current task. |
| `reopen` | boolean | no | When true and `status` is omitted, sets the status to `active` to revive a completed task. |

Returns: `<task> is now <status>.` Records one `task.update-status` event.

Semantics: status is lower-cased before validation. Re-applying the same status is a no-op event-wise but still returns the line. Marking a phase `completed` is what advances the plan frontier (see [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier)).

Example: `task_update_status(status="completed")` on `2026-06-22-tokenizer-fix` → `2026-06-22-tokenizer-fix is now completed.`

See also: [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields) — [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier).

### handoff

Scaffold a resumable handoff for the current task. The summary is the load-bearing field; an optional path points at a written note.

Capability gate: `:cairn/write`.

Signature: `handoff(summary, path?, task_id?)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `summary` | string | yes | One-line summary of where the work stands. Blank fails with `summary is required.` |
| `path` | string | no | File path for the note. Defaults to a timestamped file under the task scratchpad. |
| `task_id` | string | no | Task to hand off. Defaults to the current task. |

Returns: `Handoff scaffolded for <task> at <path>` — no trailing period. Records one `handoff.create` event.

Semantics: `handoff` is deterministic. It mints a path, writes a skeleton (frontmatter, a computed state snapshot, and empty `## Recent work` / `## Next steps` sections), records the event, and returns the path. It never drives an authoring turn — the caller overwrites the skeleton with the rich body. An existing file at the path is left in place. The richer, interactive authoring flow is the [`/handoff` slash command](/cairn/cli/slash-commands), which is separate from this tool.

Example: `handoff(summary="prefix queries land; tokenizer fix is next")` on `2026-06-22-add-prefix-query-support` → `Handoff scaffolded for 2026-06-22-add-prefix-query-support at <scratchpad>/2026-06-22_14-30-05_prefix-queries-land-tokenizer-fix-is-next.md`

See also: [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs) — [Slash commands](/cairn/cli/slash-commands).

### task_query_write

The write surface for the TQ query language, accepting the `!`-forms that [`task_query`](#task-query) refuses.

Capability gate: `:cairn/write`.

Signature: `task_query_write(query)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | string | yes | A single TQ s-expression. Accepts mutating steps (`:set-status!`, `:set!`, `:link!`, `:unlink!`) and the view forms `define!` / `undefine!`. |

Returns: the rendered query result as text. A mutation step records one append-only event per task it touches and returns the set, so a trailing `(:count)` reports how many tasks were affected. `define!` records the named view in the durable log.

Semantics: mutations converge on re-run — the same query applied twice reaches the same state. Sub-query operands inside set operations always evaluate read-only, even on this surface. `(define! "name" Q)` records `Q` as a reusable named view resolvable as `(query "name")`; a user view shadows a built-in of the same name. Read-only queries also run here, but prefer [`task_query`](#task-query) for them.

Example: `task_query_write(query="(-> (query \"plan-frontier\") (:set-status! \"active\"))")` → marks every ready phase `active` and returns the set.

See also: [The TQ language](/cairn/reference/tq-language) — [Views](/cairn/reference/views) — [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities).

### task_search

Full-text ranked search over observations. Returns matching tasks with a snippet.

Capability gate: `:cairn/read`.

Signature: `task_search(query, limit?, task_id?)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | string | yes | Free-text search. Blank fails with `query is required.` Sanitized into an FTS5 MATCH expression. |
| `limit` | integer | no | Maximum results. Defaults to 10, clamped to a maximum of 50. |
| `task_id` | string | no | Restrict the search to one task's observations. |

Returns: ranked rows (task slug plus a snippet), or `No matching observations for "<query>".` when nothing matches. A query the engine cannot parse fails with `Could not parse the search query "<query>".` Records no event.

Semantics: results are BM25-ranked. The sanitizer is forgiving so identifiers and paths search literally: a token carrying any of the special characters `" ( ) : - . / ^ * +` is phrase-quoted (so `tools.lisp:45-78` and `depends-on` match as written), a trailing `*` stays a prefix query, and tokens join with implicit AND. A query that sanitizes to nothing usable returns the no-match line rather than erroring.

Example: `task_search(query="prefix query*")` → ranked observations across tasks whose text contains a token starting with `prefix` and the token `query`.

**Deferred scope.** A `scope` parameter exists in the runner (defaulting to observations) but is not in the registered wire schema, so tool-call search is deferred. Treat observation search as the advertised contract.

See also: [The task graph](/cairn/concepts/the-task-graph) — [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs).

### task_get

Read computed task state: status, description, parent, children, edges, metadata, and recent observations.

Capability gate: `:cairn/read`.

Signature: `task_get(task_id?)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `task_id` | string | no | Task to read. Defaults to the current task. |

Returns: a rendered state block, or `No task <slug>.` when the slug is unknown. Records no event.

Semantics: the state is computed from the event log, not stored as a row of flags. The block carries the slug and status, then any description, parent, children, outgoing edges, metadata pairs, and up to five most recent observations. When more than five observations exist, a final pointer line — `… N earlier observations — call timeline with full=true, types=observation to read them all` — names how to read the rest. Each edge line prints the edge type first, then the destination slug. It is the same snapshot embedded at the top of [`task_bootstrap`](#task-bootstrap).

Example: `task_get(task_id="2026-06-22-add-prefix-query-support")` →

```text
2026-06-22-add-prefix-query-support  [active]
  add prefix query support
  parent: 2026-06-22-tune-search
  edges:
    depends-on 2026-06-22-tokenizer-fix
  recent:
    - prefix queries land; tokenizer fix is next
```

See also: [`timeline`](#timeline) — [`task_bootstrap`](#task-bootstrap) — [The task graph](/cairn/concepts/the-task-graph).

### timeline

Recent events for a task, most recent first.

Capability gate: `:cairn/read`.

Signature: `timeline(task_id?, types?, full?, before_seq?, after_seq?, limit?)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `task_id` | string | no | Task to read. Defaults to the current task. |
| `types` | string | no | Comma-separated event-type filter, validated against the live vocabulary. Omitted or blank means every type. |
| `full` | boolean | no | When true, render each event verbatim — a header line plus the untruncated multi-line body — instead of the one-line digest. Defaults to false. |
| `before_seq` | integer | no | Exclusive upper seq bound; also the page-back cursor printed when output is truncated. |
| `after_seq` | integer | no | Exclusive lower seq bound. |
| `limit` | integer | no | Number of events. Defaults to 20. |

Returns: a header line `<task> — last <N> events` followed by one line per event (sequence number, event type, and a one-line digest of its payload). With `full=true`, each event instead renders as a header `<seq>  <type>  (<YYYY-MM-DD HH:MM> UTC)` and its verbatim body, block-indented when multi-line. Output is capped at a soft character ceiling: when the next event would exceed it, emission stops and prints `… truncated at <N> chars; <M> events remaining; pass before_seq=<seq> to continue`. Records no event.

Semantics: where [`task_get`](#task-get) shows computed state, `timeline` shows the raw event stream that produced it — `observation`, `task.create`, `task.fork`, `task.link`, `task.sever`, `task.update-status`, `task.set-metadata`, `handoff.create`, and so on. By default each digest is collapsed to a single line and truncated; `full=true` renders bodies verbatim, making `timeline` the way to read a task's complete observation history. Filter to one or more event types with `types` — an unknown type errors with a nearest-match suggestion rather than silently returning nothing. Window or page with `before_seq` (exclusive) and `after_seq` (exclusive): feeding the `before_seq` cursor from a truncated page back into the next call returns the older events with no gap and no overlap. Read it to reconstruct how a task got where it is.

Example: `timeline(task_id="2026-06-22-add-prefix-query-support", limit=3)` →

```text
2026-06-22-add-prefix-query-support — last 3 events
  7  handoff.create  prefix queries land; tokenizer fix is next → ...
  6  task.update-status  → active
  5  observation  bm25 ranking confirmed against t/cairn-search.lisp
```

Example: `timeline(types="observation", full=true)` renders every observation on the current task verbatim — full multi-line bodies, no 140-character digest cut — newest first.

See also: [`task_get`](#task-get) — [Events, projection, and reconcile](/cairn/concepts/events-projection-and-reconcile).

### task_query

Run a read-only query over the task graph and return text. The query is a single s-expression in cairn's query language (TQ).

Capability gate: `:cairn/read`.

Signature: `task_query(query)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | string | yes | One TQ s-expression. A Lisp form, not a bare word — names and probes are wrapped in parens. |

Returns: the rendered query result as text. Every form yields a value or a structured error, never a crash or a silent no-op. Records no event.

Semantics: a query is a `SOURCE` optionally threaded through `STEP`s with `->`, for example `(-> (current) (:follow :phase-of) (:ids))`. Transformer steps compose node-set to node-set; shaper steps such as `(:select :slug :status)`, `(:count)`, and `(:ids)` terminate into a value. Pass a reflective form on its own to learn the surface: `(schema)` lists the sources, steps, predicates, and write forms, with steps and predicates carrying their kinds and argument signatures — call it first; `(views)` lists named views, `(fields)` the queryable fields, `(edges)` the edge types. These are sources that list names, so project with `(:select …)`. An unknown field, predicate, step, source, or edge errors with a nearest-match suggestion rather than a silent miss. Timestamps (`created-ts`, `updated-ts`, `status-ts`) are universal-time, so a date intent is one predicate — `(on :updated-ts "2026-06-24")`, `(since …)`, `(before …)` — and an integer operand to a numeric comparison against a timestamp is read as Unix seconds. Run a named view by wrapping it as `(query "plan")` or `(query "plan-frontier")`; a bare word like `plan` is a query error, since it is not a source form. The `!`-forms — mutations and view definitions — are refused here; use [`task_query_write`](#task-query-write).

Example: `task_query(query="(-> (current) (:follow :phase-of) (:select :slug :status))")` → the current task's phase-of children with their statuses.

See also: [The TQ language](/cairn/reference/tq-language) — [Views](/cairn/reference/views).

### task_bootstrap

Orient on a task in one call: its computed state, neighbors, open handoffs, and recent observations.

Capability gate: `:cairn/read`.

Signature: `task_bootstrap(task_id?)`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `task_id` | string | no | Task to orient on. Defaults to the current task; with neither, fails with `No task to bootstrap; pass task_id or select a task first.` An unknown slug fails with `No task <slug>.` and lists recent slugs. |

Returns: the [`task_get`](#task-get) state block, then any open handoffs. Records no event.

Semantics: `task_bootstrap` is the single-call session-start read. An explicit `task_id` switches the current pointer to that task — orienting on a task makes it current, so spawned sessions and the injected context agree on what is in focus. With no `task_id`, it orients on the current task and adopts it only when none is set; the pointer switches only after the task is found.

Example: `task_bootstrap(task_id="2026-06-22-add-prefix-query-support")` → the task's state block and its open handoffs.

See also: [The current task pointer](/cairn/concepts/the-current-task-pointer) — [The cairn-method](/cairn/concepts/the-cairn-method).
