null rather than missing, dates are ISO 8601 strings, and ids are UUID strings. A malformed argument fails as an invalid-params error. A call that is well formed but cannot be done, such as an action that is illegal in the piece’s current phase, fails as a tool error with a sentence explaining why.
The consent rule
Consent and safety explains these rules in full; this section is the summary. The client’s call is your consent. A client acting on your request already has your go-ahead, so actions that create or publish work run on the call, with no card in Fermata. Two actions always wait for a confirmation card:stopAgents, because it kills running work, and markDone, because it can tear down a worktree. The card names the piece and the project, and you answer Allow or Deny. A denied card fails the call with no change to the piece, and nothing on the server times a waiting card out.
If you want a card in front of runs and pull requests too, turn on Ask before runs and pull requests in Settings → MCP. It is off by default. With it on, nine more actions wait for a card: startExecution, resumeAgents, retryFailedAgents, resumeTask, editTaskBrief, messageTask, answerTaskQuestion, createPR, and approveSpec with lane auto. Releasing a draft on the Auto lane waits for one too, whether through release_draft or create_draft with release set. The setting is read on every call, so a change applies to the next one.
A client that gives up on a waiting call sends a cancellation, and Fermata denies the call and removes its card. A card for a piece that belongs to no project shows in every open project window.
Fermata’s own agents
Agents that Fermata itself runs cannot use the server’s write tools. Once you register the server with Claude Code, every session Fermata starts loads it too, and without this rule an agent inside one piece could start runs or open pull requests on others. The rule covers every kind of session: build agents on any permission profile, the spec interview, reviews, feedback sessions, and sessions you open on the canvas. The planning passes Fermata runs in the background have the write tools removed outright. The write tools arecreate_piece, create_draft, update_draft, cancel_draft, release_draft, register_project, open_piece_workspace, piece_action, snooze_cue, and reveal. A call to one of them from inside Fermata is refused with: “Fermata’s own agents can’t drive Fermata through its MCP server. Ask the person to do this from the app or from their own MCP client.” The read tools stay available under the session’s normal approval rules.
The rule matches the server by the name Register gives it, fermata. If you add the endpoint to Claude Code by hand under a different name, Fermata’s own agents are not fenced off from it.
list_projects
Lists every project registered in Fermata. Read, runs on the call.
Takes no arguments.
Returns {"projects": [...]}, one row per project: id (the projectId other tools take), name, path (the repository folder), and branch (the branch Fermata tracks for it).
list_echoes
Lists the Echoes, the remote machines pieces and sessions can run on. Read, runs on the call.
Takes no arguments.
Returns {"echoes": [...]}, one row per Echo: id and name (either is what runAt accepts), kind (ownMac, bitriseMac, or bitriseLinux), and state, the last health check (ready, asleep, stopped, unreachable, or checking), or unknown before the first one. Echoes is an Edge feature that starts off, so until it is turned on in Settings › Edge, list_echoes returns an empty list.
register_project
Registers a directory as a Fermata project so pieces can be created in it. If the directory, or a folder above it, is already a project, that project comes back unchanged. Write, runs on the call.
Returns
{ok, projectId, name, path, alreadyRegistered}.
The directory must be a git repository with at least one commit, because every piece runs in its own git worktree. A directory that is not a repository is refused with an error that names the fix: run git init there, make a first commit, and call register_project again. Nothing is registered, and the tool never initializes a repository for you.
list_pieces
Lists pieces and Backlog drafts, optionally scoped to one project. Read, runs on the call.
Returns
{"pieces": [...]}. Every row carries kind, either "piece" or "draft", and drafts follow the pieces.
A piece row carries id, name, projectId, phase ({key, display}), gate (the decision waiting on you, or null while the machine is working), activity (the live run state), taskCount, completedTaskCount, progress (0 to 1), prURL, createdAt, updatedAt, completedAt, and isArchived. While a piece waits on other pieces, activity is waiting; when a piece it waits on fails, stops, or is archived, it is dependencyBlocked, which needs you.
A draft row carries id, name (the draft’s title), projectId, lane (manual, auto, or loop), phase (always {"key": "backlog", "display": "Backlog"}), createdAt, hasSpec, refine, modelId, source ({key, url, kind} for a draft started from a tracker issue or linked to one with keys, otherwise null), related (the further linked keys as {key, url, kind}, always present and empty when there are none), and attachmentCount.
Both kinds of row also carry the piece dependency fields:
dependsOn: one{id, key, name, origin, state, ignored}per link.idis the blocker’s id, or null for a tracker issue that is not a piece yet.keyis the blocker’s tracker key, or null when it has none.originismanual,linear,github, orsplit.stateis the blocker’s phase as the board shows it (Agents 2/4,Done),Backlogfor a draft,openorclosedfor a tracker issue, anddeletedfor a blocker that no longer exists.waiting: true while the dependencies actually hold the item back. Only the Auto and Loop lanes are held, and only before building starts, so a Manual item is neverwaitingunless a split holds it.blockingKeys: what is still unmet or has gone wrong, whatever the lane, each as the blocker’s key when it has one and its id otherwise. Every entry is a valuepiece_actionremoveDependencyaccepts.
dependsOn: [], waiting: false, and blockingKeys: []. Links are kept, only hidden.
A piece row also carries its split state, read-only:
splitProposals: one{id, title, direction, source, revisedFrom}per split waiting for a person in Fermata.directionisprerequisite(the new piece lands first) orfollowUp,sourceisagentormanual, andrevisedFromis the title the proposal was first shown under when a later agent pass revised it, otherwise null.splitLinks: one{originalID, splitOffID, direction, proposalId}per split the piece took part in, on both pieces of the split.proposalIdis the proposal the split-off piece was created from, or null on a link written before it was recorded.splitHold:{reason, blockingPieceIds}while an unmet link to a piece of the same split holds the piece, in any lane, otherwise null. The split relationship decides, not the link’sorigin, so a link a person adds back between two pieces of a split holds too. An archived piece reports null.piece_actionrefusesapproveSpecandgenerateStrategywithreasonwhile it is set, andlegalActionsleaves them out. Only Start anyway in Fermata overrides it.splitPending:{reason, proposalIds}while split proposals wait for the person at the “Review split” gate, otherwise null.reasonopens with why when Auto-split is off and stopped an Auto or Loop piece.splitStart:{startSplitPiecesAutomatically, mode, waitsForYou, reason}.startSplitPiecesAutomaticallyis the Pieces setting as it is now.modeis how the piece’s latest split started its pieces, read from the setting when the split was applied:automatic(the new pieces keep the original’s Auto or Loop lane),waitsForYou(the setting was off, so the new pieces wait at Spec ready in the Manual lane and the original stops at its strategy review), ormanual(the original was Manual), otherwise null.waitsForYouis true until the person moves the piece on, andreasonsays why it waits when no split link holds it, otherwise null.
runAt as {choice, resolved, echoName, removed}. choice is the item’s own setting (thisMac, an Echo id, or null to follow the project), resolved is where it runs (thisMac or an Echo id), echoName names that Echo, and removed is true when the Echo it resolves to has been removed; such a piece waits and never falls back to this Mac. A piece row also carries echo as {id, name, state, reason}, where state is local, connecting, connected, waiting (with reason), or removed, and id and name are null for a local piece.
Releasing a draft creates a piece with the same id, so a client polling one id sees its row turn from kind: "draft" into kind: "piece".
get_piece
Reads one piece or one draft by id. Read, runs on the call.
For a piece, returns everything its
list_pieces row carries plus branchName, worktreePath, config, the agent graph as tasks and edges, the linked sessions, legalActions (the piece_action values accepted right now), reviewProgress during review, artifacts (which documents exist), splitEvents (the piece’s split history as {at, kind, summary, startMode, proposalId}, oldest first, kept across relaunches; startMode is set on the events of an applied split, and proposalId on the applied events that created a piece from a proposal), and, during Spec, interview with any unanswered questions.
For a draft, returns its list_pieces row plus seed, spec (the full text, or null), and legalActions: ["update_draft", "cancel_draft", "release_draft"], followed by addDependency and removeDependency while piece dependencies are on.
An id that is neither fails with “Unknown pieceId”.
get_artifact
Reads one of a piece’s documents or structured records. Read, runs on the call.
Returns
{pieceId, kind, present, format, content}. The four documents come back as markdown, and the rest as JSON. An artifact that does not exist yet returns present: false with null content rather than an error.
workReview returns the work review’s stored record: the verdict, the acceptance criteria it checked, what it cost, and two fields worked out the same way the app works them out: outcome (pass or fail) and failedTasks, a list of {taskId, ordinal, name} for the agents the review failed. A piece with no work review returns present: false and the message “This piece has no work review yet.”
list_sessions
Lists sessions with their state, piece, model, effort, branch, worktree, timestamps, cost, and whether one is waiting on a tool approval. Read, runs on the call.
Each row carries
error: the reason a failed session failed, and null for any other session.
get_session
Reads one session: its list_sessions row plus, optionally, the tail of its transcript. Read, runs on the call.
create_piece
Creates a piece and starts its spec interview from a prompt. The interview carries on in the background: poll get_piece for questions and answer them with piece_action interviewMessage. Write, runs on the call.
Returns
{ok, pieceId, name, phase}.
An Echo named in runAt must exist. An unknown id or name, or a name two Echoes share, fails the call before anything is created, and the error lists the Echo names you can use. The same rule applies to create_draft and update_draft. A piece’s Run at is fixed once its agents start on an Echo; after that only Run here in the app moves it. Echoes is an Edge feature that starts off; while it is off, a runAt that names an Echo fails the call, and thisMac or an omitted runAt runs the piece on this Mac.
create_draft
Files a draft into a project’s Backlog from a pre-written spec, a rough seed, or a tracker issue. Filing does not start anything: the draft waits for release_draft, or for you to release it in the app. There are two exceptions. With release set, the draft is released in the same call, exactly as release_draft would. A loop draft filed while that project’s Loop is playing starts at once. Write, runs on the call; with release set on an Auto draft it waits for a card when the setting is on, and the card shows before anything is filed, so a denied card leaves nothing behind.
An
issue is fetched from a Stream Source linked to the project, of the same kind as the key. It needs Loop Intake, and it fails with a sentence saying why when that is unavailable, when the project links no source of that kind, or when the tracker refuses the request. The issue becomes the draft’s spec and is formatted into a full spec when the draft is released, as a picked issue is in the app. An issue that is still blocked by open issues is filed anyway.
keys links tickets to text you wrote, without fetching their content. Use it when one piece covers several tickets, or when your spec carries decisions the ticket does not. Duplicates are dropped case-insensitively and the order is kept. Every key is verified through the project’s linked Stream Source before anything is filed and before any card shows, so the call is all or nothing: a key that cannot be verified fails it with “Couldn’t verify SUP-12: ” followed by the reason, which covers a key that does not exist, Loop Intake being unavailable, no linked source of that kind, and a tracker error. Keys from Linear and GitHub can be mixed; each is checked against a source of its own kind. The first key becomes the draft’s source, so the card shows its pill, the branch carries the key, and the pull request closes it. Every further key is related: it adds its own Closes line to the pull request, one per key, and appears nowhere else on the card. The spec stays the draft’s only content and the title still comes from the spec’s title heading, then title, then the seed. refine applies as usual. A malformed key, an empty array, more than 10 keys, keys without spec or seed, and keys with issue are argument errors that name the problem, the last one starting “create_draft ‘issue’ can’t be combined with ‘keys’:”.
dependsOn makes the new draft wait on other pieces or drafts of its project, as ”+ Add dependency” does in the inspector. Each entry is an id or a tracker key, which matches the piece or draft filed from that issue. Every entry is resolved before anything is filed, so one that names nothing in the project, or names something in another project, fails the call and files nothing. A draft filed from an issue also takes the issue’s own “blocked by” links, as a pick in the app does. A link that would create a loop is skipped rather than failing the call, because the draft is already filed by then; it is listed in skippedDependencies as {blocker, reason}.
Returns {ok, draftId, title, lane}. A draft filed from an issue adds source ({key, url, kind}), and, while the issue is blocked, blockedBy (the open blockers’ keys) and blockedByCount. A draft filed with keys adds source (the first key) and related (the rest, possibly empty), both as {key, url, kind}. A draft filed with dependsOn adds its dependsOn rows. With release set it returns the new piece’s row instead, as list_pieces shows it, with released: true. When the draft’s dependencies hold the release, the call still succeeds and returns {ok, draftId, title, lane, released: false, waiting: true, waitsOn, dependsOn}, where waitsOn lists what it waits on as blockingKeys does; the draft starts on its own once they are met. A resend after the draft was released returns {ok, draftId, pieceId, released: true}.
Running an issue on Auto is one call:
update_draft
Edits a Backlog draft before it runs. Pass at least one field to change; the others keep their value. A spec with a title heading renames the draft from that heading. An empty seed or spec clears it, but a draft has to keep one of the two. Moving a draft to loop while the project’s Loop is playing starts it, as create_draft does. Write, runs on the call.
keys replaces the draft’s linked tickets rather than adding to them, so sending the same list twice changes nothing and sending a shorter one removes the rest. The new keys are verified exactly as create_draft verifies them, before any field in the call is written, so a key that fails leaves the whole draft unchanged. An empty array clears every link. A draft filed from an issue refuses keys with a tool error, because its spec is that issue’s text; file a new draft with spec and keys instead.
Returns the draft’s row as
list_pieces shows it, or the new piece’s row when moving it to loop started it.
cancel_draft
Deletes a Backlog draft and any files staged with it. A released draft is a piece and is not affected. Write, runs on the call.
Returns
{ok, draftId}.
release_draft
Starts a Backlog draft, as “Run spec” or “Play” does on the board. The draft’s lane decides what runs: a manual or loop draft runs its spec step, and an auto draft runs the whole piece unattended up to Review. Write, runs on the call; an auto draft waits for a card when the setting is on.
Returns the new piece’s row as
list_pieces shows it. The piece keeps the draft’s id. When the draft’s dependencies hold the release, the call is not an error: it returns {ok, draftId, title, lane, released: false, waiting: true, waitsOn, dependsOn}, and the draft starts on its own once what it waits on is met. Resending for a draft that was already released returns that piece and starts nothing.
open_piece_workspace
Gets a piece’s worktree ready so you can work on it in your own terminal session, recreating it from the branch if it was removed. It does not move your session: the result tells the client to ask you before changing directory. Write, runs on the call.
Returns the worktree path, branch, phase, pull request URL, the agents, the spec and strategy, and
agentInstruction.
piece_action
Moves a piece through its lifecycle, one action per call. An action that is illegal in the current phase fails and names the actions that are legal now, which get_piece also lists up front. Write.
Returns
{ok, pieceId, phase, gate} after the action, plus prURL for createPR, started for runCodeReview, sessionId for requestChanges, next and message for sendBackToAgents, started and message for resumeTask, next and message for editTaskBrief, messageTask and answerTaskQuestion, and isArchived for archive and unarchive. addDependency and removeDependency return {ok, pieceId, kind, dependsOn, waiting, blockingKeys} after the edit, plus phase and gate for a piece.
The “Legal in” column names the phase the piece must be in, as the rail shows it. Within Agents, “the plan” is the agent graph before it runs, and “finished” is a run whose agents have all ended.
list_cues
Lists what needs you right now, grouped exactly as the menu bar popover groups it. Read, runs on the call.
With
cwd inside a project, inside a worktree under that project’s folder, or inside a git worktree of the project’s repository checked out somewhere else, every field covers only that project: the groups, count, snoozedCount, and running, and multiProject is false. Fermata resolves the directory the same way list_pieces and list_sessions do. When no project contains cwd, the result is empty, so a session in an unrelated repository shows nothing. Without cwd, the result is the same global set the menu bar popover shows. The call never fails over the directory.
Returns {schema, project, count, groups, multiProject, snoozedCount, running}. project is {id, name} when the result is scoped and null when it is global. groups always holds all three groups, in the popover’s order: requiresApproval (Requires approval), readyForReview (Ready for review), and readyToMerge (Ready to merge), each as {key, title, items}, and an empty group is kept. count never includes running work, and snoozed items are left out and counted in snoozedCount. multiProject is true when the items come from more than one project.
Each item carries target ({kind, id}, where kind is piece or session), group, reason, label (the popover’s wording), title, projectName, sourceRef, prNumber, prChecksLine, since, and fingerprint, which snooze_cue takes. Three more fields say what you can do about it:
primaryActionis the onepiece_actionaction that answers the item, or null when the item can only be opened. It is never a merge: an item in Ready to merge always has null here.confirmAvailableis true only for a Ready for review item whose piece has no open Work Review findings, so a piece with findings is opened rather than confirmed.legalActionsis whatget_piecewould list for the piece, and empty for a session.
running is {line, running, advancing, projectNames, pieces}, where each piece is {pieceId, title, projectName, progress} and progress runs from 0 to 1.
snooze_cue
Hides one item in every Fermata surface until its state changes, as Snooze does in the popover. A snooze lasts until Fermata quits. Write, runs on the call.
Returns
{snoozed: true, snoozedCount}. When the item changed since you read it, nothing is snoozed and the call fails with “This cue changed; refresh”, so a snooze can never hide a decision you have not seen.
reveal
Opens a piece or a session in Fermata on the Mac, the way tapping its notification does. Write, runs on the call.
Returns
{revealed: true}.
Opening pull requests for many pieces
There is no bulk tool. A client loops: calllist_pieces for the project, keep the rows whose phase key is complete and whose prURL is null, and call piece_action with action createPR and no title or body for each one. Each call opens one pull request with text Fermata writes from that piece’s diff, and a failure on one piece leaves it as it was and does not stop the others.
What a client cannot do
- Merge a pull request. Merging writes into a shared repository and cannot be undone, so it stays with you, in the app or on a paired phone.
- Rewrite an agent’s prompt on a paused run. A rewrite there would race the brief the agent is about to get, so prompts are edited in the app before the run starts.
- Set a base branch. A piece branches from the project’s base branch.