This guide exercises Cairnline as a standalone, agent-neutral MCP server. It uses a rootless project so the flow works for planning, research, writing, ops, or design work as well as code projects.
Cairnline stores coordination state. It does not launch agents, choose models,
grant tools, create worktrees, read secrets, or bypass any client-side sandbox.
Agents and hosts should treat execution_mode, desired_agent_kind, and
skill_ids as hints that they map to their own runtime policy.
From a checkout:
go run ./cmd/cairnline -db /tmp/cairnline-quickstart.dbOr install the command and run it from anywhere:
go install github.com/hecatehq/cairnline/cmd/cairnline@latest
cairnline -version
cairnline -db "$HOME/.local/share/cairnline/cairnline.db"The server speaks MCP over newline-delimited JSON-RPC on stdin/stdout. Most MCP
clients hide the JSON-RPC envelope behind a tool UI. The examples below show
the exact tools/call payloads so they can be replayed in tests, scripts, or a
raw stdio session.
The core workflow is:
operator creates project
operator creates role
operator creates work item
operator queues assignment
compatible agent finds assignment
agent claims assignment
agent reads context
agent records evidence
agent completes assignment
Before creating records, hosts can ask Cairnline to describe its portable contract:
{"jsonrpc":"2.0","id":0,"method":"tools/call","params":{"name":"coordination.capabilities","arguments":{}}}The response includes structuredContent with supported execution_modes,
assignment_statuses, the effective assignment-claim lease TTL and recovery
tools, skill metadata paths, the recommended MCP-pull flow, and the runtime
responsibilities that remain owned by the consuming agent host.
Generated IDs are returned in tool response text. Copy the proj_...,
role_..., work_..., and asgn_... IDs into later calls.
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"projects.create","arguments":{"name":"Launch planning","description":"Coordinate the next public launch checklist."}}}Optional workspace-backed projects can pass roots, but roots are metadata
only. Cairnline does not create folders or Git worktrees:
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"projects.create","arguments":{"name":"Code review","roots":[{"id":"root_main","path":"/Users/alice/src/example","kind":"git","active":true}],"default_root_id":"root_main"}}}Replace PROJECT_ID with the generated project id.
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"roles.create","arguments":{"project_id":"PROJECT_ID","name":"Reviewer","description":"Reviews plans and evidence before closeout.","instructions":"Check that the work is understandable, sourced, and ready for operator approval.","default_execution_mode":"mcp_pull","default_skill_ids":["review"]}}}Roles describe responsibility. They are not agent presets, provider settings, model settings, or permission grants.
Replace ROLE_ID with the generated role id.
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"work_items.create","arguments":{"project_id":"PROJECT_ID","title":"Review launch checklist","brief":"Check the current checklist, identify gaps, and record evidence for operator review.","priority":"normal","owner_role_id":"ROLE_ID","reviewer_role_ids":["ROLE_ID"]}}}Replace WORK_ITEM_ID with the generated work item id.
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"assignments.create","arguments":{"project_id":"PROJECT_ID","work_item_id":"WORK_ITEM_ID","role_id":"ROLE_ID","execution_mode":"mcp_pull","desired_agent_kind":"any","skill_ids":["review"]}}}execution_mode describes the desired coordination style:
manual: a human/operator works the assignment outside MCP.mcp_pull: a compatible MCP client or agent polls, claims, and reports back.external_adapter: an orchestrator may launch a non-MCP adapter.orchestrated: an orchestrator may supervise execution.
Cairnline does not perform the launch for any of these modes.
Read the assignment with assignments.get, then pass its exact coordination
fields and updated_at back as the expected snapshot. Only the replacement
fields may differ:
{"jsonrpc":"2.0","id":"4-edit","method":"tools/call","params":{"name":"assignments.update","arguments":{"project_id":"PROJECT_ID","assignment_id":"ASSIGNMENT_ID","expected":{"work_item_id":"WORK_ITEM_ID","role_id":"ROLE_ID","root_id":"","execution_mode":"mcp_pull","desired_agent":{"kind":"any","skill_ids":["review"]}},"expected_updated_at":"ASSIGNMENT_UPDATED_AT","replacement":{"work_item_id":"WORK_ITEM_ID","role_id":"NEW_ROLE_ID","root_id":"","execution_mode":"manual","desired_agent":{"kind":"human","skill_ids":["review"]}}}}}If the assignment was edited, claimed, or prepared after the read, the update
returns a conflict. Re-read before deciding whether the new state should be
changed. Breaking (alpha): assignments.update now uses this nested
compare-and-set shape and no longer changes lifecycle or execution fields.
Handoff edits, status changes, and deletes also require the exact
updated_at returned by handoffs.get. To accept a handoff and ensure one
follow-up assignment in a single durable operation, call:
Breaking (alpha): handoffs.update, handoffs.update_status, and
handoffs.delete now require expected_updated_at; stale or tokenless writes
fail closed instead of replacing newer handoff state.
The same guard applies to live assistant update_handoff actions: the embedded
handoff must carry the exact updated_at revision that was read.
{"jsonrpc":"2.0","id":"handoff-follow-up","method":"tools/call","params":{"name":"handoffs.accept_with_follow_up","arguments":{"project_id":"PROJECT_ID","work_item_id":"WORK_ITEM_ID","handoff_id":"HANDOFF_ID","expected_updated_at":"HANDOFF_UPDATED_AT","idempotency_key":"ONE-KEY-PER-OPERATOR-ACTION","intent":"accept_and_ensure_follow_up"}}}The handoff must name a target role. Cairnline derives the target work item,
root, execution mode, and role skills from portable project state. When no
assignment is linked, it creates one pristine and queued, links it, and marks
the handoff accepted in one transaction; it never claims or launches that new
assignment. An existing linked assignment is reused and returned in its current
authoritative state, even if it has already progressed or finished. Retry an
uncertain request with the same idempotency key; Cairnline returns the original
outcome and assignment identity with their current authoritative state instead
of creating another assignment. If an operator later closes or changes that
link, the replay conflicts rather than overriding the newer decision. Reusing a
key for a different request, or writing from a stale updated_at, also
conflicts.
An agent can ask for queued assignments matching its kind and skills:
{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"assignments.next","arguments":{"project_id":"PROJECT_ID","agent_kind":"any","skill_ids":["review"]}}}The result includes compatible queued assignments. The agent host decides whether it is allowed to do the work.
Replace ASSIGNMENT_ID with the generated assignment id. claimed_by should be
a stable local label for the claiming agent or host session.
{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"assignments.claim","arguments":{"project_id":"PROJECT_ID","assignment_id":"ASSIGNMENT_ID","claimed_by":"local-agent-reviewer"}}}Claiming prevents another compatible agent from picking the same queued work. It
does not grant tools, writes, network access, credentials, or model access. The
structured result contains claim.id, claim.acquired_at, and
claim.expires_at. Save claim.id as CLAIM_ID and pass it to every later
assignment lifecycle mutation. It is a concurrency fence, not an authentication
secret.
After a host restart, assignments.get and assignments.list expose the active
claim_id and claim_expires_at in fallback text as well as structured data;
after start they expose the retained claim_fence instead.
The default pre-start lease is five minutes; read the effective ttl_seconds
from coordination.capabilities. Positive custom TTLs are rounded up to whole
seconds with a one-second minimum. If context or host preparation takes longer,
renew before expiry:
{"jsonrpc":"2.0","id":"6-renew","method":"tools/call","params":{"name":"assignments.renew_claim","arguments":{"project_id":"PROJECT_ID","assignment_id":"ASSIGNMENT_ID","claim_id":"CLAIM_ID"}}}Renewal at or after claim.expires_at conflicts. An expired claim remains
visible and claimed until an authorized host reconciles any prepared resources
and explicitly requeues it:
{"jsonrpc":"2.0","id":"6-recover","method":"tools/call","params":{"name":"assignments.recover_claim","arguments":{"project_id":"PROJECT_ID","assignment_id":"ASSIGNMENT_ID","expected_claim_id":"CLAIM_ID"}}}Recovery is only for expired pre-start claimed reservations. Cairnline never
uses a timer to cancel, steal, or requeue running, approval-blocked, or review
work.
Breaking (alpha): existing MCP pull clients must migrate to fenced claims.
assignments.prepare and assignments.release now take claim_id instead of
claimed_by, while assignments.update_status and assignments.complete
require claim_id. Read it from either the text or structured
assignments.claim result. Embedders with custom Go stores must also implement
the seven new claim-lease methods, and snapshot producers should emit version 2
to preserve claim generations; version 1 remains importable as unleased
host-authoritative history.
{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"assignments.context","arguments":{"project_id":"PROJECT_ID","assignment_id":"ASSIGNMENT_ID"}}}Use this context to build the host-specific prompt or work packet. The context
includes the project's enabled durable memory entries (memory), so a portable
host does not need a side channel to recover promoted project memory. Skill
metadata and source locators are provenance hints; Cairnline does not inject
SKILL.md bodies or fetch source locators.
After claiming, a host may attach its execution reference or generated context snapshot without marking work as started:
{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"assignments.prepare","arguments":{"project_id":"PROJECT_ID","assignment_id":"ASSIGNMENT_ID","claim_id":"CLAIM_ID","execution_ref":{"kind":"task_run","run_id":"local-run-1"},"context_snapshot_id":"HOST_CONTEXT_SNAPSHOT_ID"}}}Preparation returns an invalid-arguments error if the claim id is missing, and a conflict if it is expired or no longer current.
{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"assignments.update_status","arguments":{"project_id":"PROJECT_ID","assignment_id":"ASSIGNMENT_ID","claim_id":"CLAIM_ID","status":"running","execution_ref":{"kind":"task_run","run_id":"local-run-1"}}}}execution_ref is a structured, host-neutral reference to the execution the
host attached to this assignment. All fields are optional and opaque to
Cairnline:
kind: host execution shape, for exampletask_run,chat_session, orexternal_adapter;task_id: host-scoped id of the queued/background unit of work;run_id: host-scoped id of a single execution attempt;session_id: host-scoped id of an interactive session driving the work;trace_id: host observability link, for example an OpenTelemetry trace id;pending_approvals: count of host-side approval gates currently blocking the execution.
Starting work retires the pre-start expiry, but the assignment retains the same claim id as a fencing generation. Continue passing it to progress and completion calls so a worker from an older recovered claim cannot overwrite the current run.
Breaking (alpha): execution_ref must be an object. The pre-structured
bare-string form is rejected with an invalid-arguments error, and stores that
still hold string refs must be rebuilt or re-seeded from the host's
authoritative data.
When the host pauses the execution on a human approval gate, report it:
{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"assignments.update_status","arguments":{"project_id":"PROJECT_ID","assignment_id":"ASSIGNMENT_ID","claim_id":"CLAIM_ID","status":"awaiting_approval","execution_ref":{"kind":"task_run","run_id":"local-run-1","pending_approvals":1}}}}awaiting_approval is a first-class assignment status so portable readers can
distinguish "blocked on a human decision" from "actively executing" without
host-specific overlays. Set the status back to running once the approval
resolves. The approval object itself stays host-owned.
{"jsonrpc":"2.0","id":11,"method":"tools/call","params":{"name":"evidence.record","arguments":{"project_id":"PROJECT_ID","work_item_id":"WORK_ITEM_ID","assignment_id":"ASSIGNMENT_ID","title":"Checklist review notes","body":"Reviewed the launch checklist and found two missing owner approvals.","locator":"notes://launch-checklist-review","source_kind":"note","external_id":"launch-review-1","provider":"local","trust_label":"agent_reported"}}}Evidence locators are stored as metadata. Clients must validate schemes before rendering locators as links or opening them.
{"jsonrpc":"2.0","id":12,"method":"tools/call","params":{"name":"assignments.complete","arguments":{"project_id":"PROJECT_ID","assignment_id":"ASSIGNMENT_ID","claim_id":"CLAIM_ID","status":"completed","execution_ref":{"kind":"task_run","run_id":"local-run-1"}}}}Use status:"failed" when the agent cannot complete the work. The work item
remains durable coordination state for an operator or another agent to inspect.
{"jsonrpc":"2.0","id":13,"method":"tools/call","params":{"name":"work_items.closeout_readiness","arguments":{"project_id":"PROJECT_ID","work_item_id":"WORK_ITEM_ID"}}}Closeout readiness is advisory. Cairnline tells the operator whether assignments, evidence, reviews, and handoffs look clear; the client decides what UI or approval step to show.
Cairnline also exposes read-only resources for clients that prefer resource pickers over tool calls. Discover parameterized URI shapes:
{"jsonrpc":"2.0","id":14,"method":"resources/templates/list"}Then read concrete resources such as the assignment launch packet:
{"jsonrpc":"2.0","id":15,"method":"resources/read","params":{"uri":"cairnline://projects/PROJECT_ID/assignments/ASSIGNMENT_ID/launch-packet"}}When a tool call fails, the result sets isError: true, keeps the human message
in content, and adds a machine-readable code in structuredContent:
{
"content": [{"type": "text", "text": "root \"x\" not found"}],
"structuredContent": {"error": {"code": "not_found", "message": "root \"x\" not found"}},
"isError": true
}Branch on structuredContent.error.code rather than parsing the message. The
codes are not_found, invalid, already_exists, conflict, and internal;
see the catalog and suggested host HTTP mapping in
Tool Error Codes.
Use a durable database for normal local use:
{
"mcpServers": {
"cairnline": {
"command": "cairnline",
"args": ["-db", "/Users/alice/.local/share/cairnline/cairnline.db"]
}
}
}For development from a checkout:
{
"mcpServers": {
"cairnline-dev": {
"command": "go",
"args": ["run", "./cmd/cairnline", "-db", "/tmp/cairnline-dev.db"],
"cwd": "/path/to/cairnline"
}
}
}Cairnline records intent and provenance. The consuming host still owns:
- model/provider selection;
- tools, writes, network, and filesystem policy;
- approval prompts and user confirmation;
- credentials, cookies, secrets, and login sessions;
- workspace checkout, branch, and worktree creation;
- conversion from desired-agent and skill hints into host-specific agent settings.