Skip to main content

Tool Surface & Budgets

What an MCP client can actually call in Daintree: the 25 external tools, why the surface was cut, mcp.surface, payload budgets, forge over MCP, resources and prompts.

Reviewed

This page is the reference half of MCP Server. It covers what a session can call, what shape the results come back in, and the ceilings that keep those results transportable. The authorization model, the connect config and the audit log live on the hub page.

Why the surface was cut

The external tool surface used to carry 100 tools and roughly 128 KB of schema. That is past what real MCP clients tolerate, and the failure mode was the problem: Cursor caps the tool count across every connected server and silently truncates the overflow, while GitHub Copilot's 128-tool cap is a hard blocking error. Either way the client was choosing which of Daintree's tools survived, and neither told anyone which ones it dropped.

v0.30 cut the external surface to a curated allowlist. The count is not the interesting part: the interesting part is that the choice moved back inside Daintree, where it can be reasoned about and tested, instead of being made silently by whichever client happened to connect.

The surface is budgeted in both dimensions, because the failure is measured in bytes as much as in tools. CI asserts a ceiling of 26 external tools and a floor of 15: the floor guards the opposite failure, where a bad merge empties the list and makes every other assertion vacuous. A separate test bounds the summed description bytes across the roster at 16,000 UTF-8 bytes, because two dozen tools carrying novel-length descriptions would reproduce the same truncation with a count that still looks fine.

Note

As of Daintree v0.32.0 the external allowlist holds 25 tools. The v0.30 changelog says "100 tools to 23", which was the count when the cut landed; two have been added deliberately since. The list below is the authority, and the source of truth in the app is MCP_EXTERNAL_TIER_TOOLS: nothing in CI compares a number in prose against it, so treat the table as the shape and the app as the count.

The external tool surface

These are the tools an API-key client sees in tools/list and may dispatch through tools/call. The two sets are identical by design: nothing is withheld from the listing while staying callable.

ToolWhat it does
actions.listEnumerate the calling session's authorized action surface.
actions.searchA compact ranked shortlist of authorized actions, the answer when the tools/list schemas are too large to reason over.
actions.getSchemaOne action's manifest entry plus whatever schemas it publishes.
actions.getContextThe live habitat context: active project, worktree and terminal.
mcp.surfaceThis session's tool surface as versioned data with a stable hash.
agent.launchLaunch an agent (built-in, user-defined or plugin-contributed) in a worktree.
agent.listAvailableThe authoritative effective agent registry and live launchability. Nothing the caller's own shell can answer.
fleet.getRunStatusA read-only supervision snapshot of the fleet broadcast run owned by the dispatching window.
recipe.listList the workspace recipes available to this project.
recipe.runRun a recipe. Confirm-gated.
skills.searchSearch plugin-contributed skills.
skills.loadLoad a skill's markdown into the agent's context.
terminal.listEnumerate the terminal panels in the view.
terminal.newCreate a terminal panel.
terminal.getOutputRead a terminal's scrollback.
terminal.getStatusBatched agent and process state across many terminals, optionally with output tails.
terminal.sendCommandSubmit text to a terminal. Returns once submitted, not when the work finishes.
terminal.injectStage text into a terminal's input without submitting it.
terminal.waitUntilIdleA bounded long-poll until an agent's state machine leaves working.
terminal.waitUntilIdleBatchThe same wait, fanned across several terminals.
worktree.listList the project's worktrees.
worktree.getCurrentThe active worktree.
worktree.createWithRecipeCreate a worktree and apply a recipe to it.
worktree.setActiveSwitch the active worktree.
copyTree.generateAndCopyFileApply the project's CopyTree policy and put a file on the clipboard.

What is deliberately excluded

  • Every forge.*, git.*, file.*, project.* and browser id. An external agent driving Daintree sits in a terminal with its own shell and its own gh. All of it stays available to the in-app assistant, which no third-party client cap applies to.
  • terminal.close. terminal.list enumerates every panel in the view (your own shells and other agents' terminals included), and nothing binds a panel to the session that created it, so "close the ones it opened" is not an invariant that exists. Recovery is worse than the mistake: trash is purged after 20 seconds, which kills the PTY, and no restore action is on this surface.
  • Fleet broadcast. fleet.getRunStatus is read-only. To fan out, send one terminal.sendCommand per terminal and watch with a batched terminal.getStatus or a bounded terminal.waitUntilIdleBatch.
  • The github.* aliases, which were removed in v0.18 and do not exist at any tier.

Reading your own surface with mcp.surface

A client written against an older surface has no way to notice it is talking to a newer one, until a call fails for a reason it cannot interpret. mcp.surface answers that at startup: it returns the session's tool surface as versioned data with a stable content hash, so a client can compare one string instead of diffing a tool list, and fail loudly rather than silently.

FieldMeaning
manifestVersionThe shape version of the payload, currently 1. Bumped by hand when a field is added, removed or given new meaning. A client reads this to know whether it can still parse the response at all.
appVersionThe running Daintree build, so a captured response is diagnosable on its own.
tierThe tier this call was admitted at, and the tier the rest of the response describes.
hashHex SHA-256 over the canonical form of the surface.
toolsSorted by id. Each entry carries id, tier, kind, readOnlyHint, idempotentHint, and an optional deprecated object with a reason and a replacement.

Two decisions are worth knowing. Descriptions and schemas are deliberately omitted: they are already on tools/list, and re-sending them would make the cheap startup check as expensive as the thing it replaces. And prose is stripped before hashing: descriptions, titles and examples are removed from the schema preimage, so rewording a description never reads as a compatibility break, while an argument-schema edit always does.

The manifest describes the static tier allowlist, not the transient per-tool approvals that can widen dispatch for a few minutes. Folding those in would make the hash flap as approvals come and go, and would describe a surface tools/list never showed. When a tier changes, the server fires notifications/tools/list_changed; a client that re-reads on that notification converges.

Workflow instructions at initialize

The MCP initialize result carries a static instructions string that clients fold into the model's system prompt at session start. It is the only place to establish cross-cutting rules before the model has picked a tool, so it deliberately does not restate any tool's own description. Four points:

  • Use Daintree for the coordination it owns: worktrees, recipes, agent terminals. An external client should use its own shell and tooling for the repository, file, git and forge work that is absent from tools/list; that is not license for an in-app session to route around its own tier.
  • tools/list is the advertised baseline and there is no deferred catalog behind it. Do not invent tool names. When the schemas are too large to reason over, narrow attention with actions.search and then actions.getSchema.
  • Resolve worktree and terminal ids before scoped actions. terminal.sendCommand returns when the text is submitted, not when the work finishes. Prefer terminal.waitUntilIdle or terminal.waitUntilIdleBatch over tight polling, and read idleReason, waitingReason and exitCode before the next step. A terminal with no tracked agent returns idleReason: "unknown" immediately, which is not proof a shell command finished.
  • Authorization is tiered, and TIER_NOT_PERMITTED means what it says. Honor the retriable flag on errors.

The string is budgeted by test rather than truncated at runtime: Claude Code hard-cuts instructions at 2 KiB and the authorization paragraph is the tail, so silently losing it would leave a model guessing at why a call was denied. The suite fails an author's edit instead.

Payload budgets

Before v0.30 a single tool call could return 31 MB. Every result path is now bounded, and the bounds are byte-based rather than token-based on purpose: tokenization is a client concern, and pinning the server to one vendor's tokenizer would be worse than a conservative fixed cap.

SurfaceCapBehavior at the cap
Tool result text50 KiBTruncated at a UTF-8 character boundary, with the notice prefixed rather than appended: a model reading top-down learns the body is incomplete before it starts parsing it.
structuredContentMeasured separatelyRe-derived by parsing the already-bounded text, so the two halves can never disagree. Dropped when the text was truncated, when nesting exceeds 100 levels, or when it re-serializes over the cap. Any dropped structured half flags the envelope isError: true.
Resource text50 KiBA threshold rather than a hard ceiling; appends a [truncated] marker.
Project check outputTail-preservingKeeps the end of the run, where the failure is. Secret-scrubbed across an 8 KiB overlap so a credential straddling the cut is still redacted.
Git file diff24 KiB default, 1 MiB maximumPaged with an explicit offset and maxBytes.
Commit metadata512 bytes subject, 1 KiB bodyTruncated per field, so one pathological commit message cannot crowd out a whole listing.
CopyTree content preview32 KiB read, 48 KiB serializedThe serialized budget is the one that decides: JSON escaping doubles newlines and quotes and expands control characters sixfold, so a 32 KiB head of the wrong file serializes to far more.

Session history reads, bookmark reads and the action manifest carry their own budgets on the same model. Two paths bypass the helper entirely: non-text content such as screenshots, and JSON-RPC protocol errors.

Alongside the budgets, every action result is parsed through its declared schema before it is returned, so the published shape is the delivered one. That parse is also a security boundary in its own right. See project.getSettings on the hub page.

Forge over MCP

Forge tools are not on the external surface: an external caller has its own gh. They matter for the in-app assistant, which reaches forge reads at workbench and forge writes at system. Together they are enough for an agent to run a whole work loop without leaving Daintree.

Writes

Twelve write actions cover the loop: pull requests: create, merge, comment, edit; issues: create, close, label; and PR reviews: approve and request changes. Each of them returns the state it changed rather than void, so a caller does not have to follow every write with a read to find out what happened.

Reads

  • forge.getCIStatus returns the roll-up CI verdict for one pull request. Read the overall state for the answer: the accompanying counts cover required checks only, and a zero total also appears when the required-check list could not be read in full, so it is never evidence that nothing gates the merge. Values are provider-cached and can lag by around a minute.
  • forge.getChecks (v0.32) is the per-check follow-up: every check's name, whether it is still running, how it finished, and a link to its log where the provider reports one. Reach for it once the roll-up shows trouble and the question becomes which check broke. Three outcomes must not be conflated: { checks: [] } means no checks, { checks: null } means no such pull request, and a provider without the capability rejects outright. It costs one request per 100 checks.
  • forge.listIssueComments reads an issue's comment thread, cursor-paged.

getCIStatus and getChecks are the only forge reads that advertise an MCP outputSchema, so a model can be held to the result shape rather than parsing prose.

Listing

Issue and PR lists take perPage (1–100, default 20; 100 is the tightest page ceiling across the provider roster), sort (created or updated), direction, an opaque cursor from the previous response's nextCursor, and a view that selects a summary or full projection. There is also a bypassCache escape hatch, because without it an out-of-band change (you running a forge CLI in a terminal, another agent closing an issue) stays invisible until the cached page ages out.

The schemas are strict: an unknown filter key is rejected rather than dropped. A silently ignored filter returned a plausible-looking but wrong set, and an agent then acted on it. A validation error naming the bad key is strictly more useful than a wrong answer.

Running a project's checks

project.runCheck (v0.30) runs one of a project's detected runners (a test command, a lint command) as a real child process and returns its real exit code, instead of the best-effort scrollback parse that preceded it. It executes in the main process rather than round-tripping to a renderer, because renderer dispatch is capped at 30 seconds, well under a real test suite, and an MCP abort signal does not cross IPC so a renderer-hosted run could never be cancelled.

It sits at the action tier, not workbench: a read-only session may detect a project's runners with project.detectRunners, but it must not execute them. It is not on the external surface: an external caller runs its own test command in its own shell.

CopyTree returns a file handle

copyTree.generateAndCopyFile is the one deliberate exception to "drop what the caller can do itself", and it is worth stating why. A caller holding terminal.sendCommand could pipe files through pbcopy itself. What it cannot reproduce is the deliverable: one call that applies the project's own CopyTree policy (ignore rules, budgets, format) and puts a file on the clipboard identically across macOS, Linux and Windows. The alternative is a brittle shell-and-pipe reconstruction per platform.

It returns a file handle rather than the whole bundle, so a multi-megabyte context bundle never has to cross the transport. Its blast radius is a clipboard overwrite, which the risk banding already classifies as destructive-local and the tool advertises through destructiveHint. See CopyTree.

Terminal and agent metadata

terminal.getStatus is the batched polling path. Prefer it over listing terminals and then reading each one's output in turn. Per entry it returns the terminal id, the detected or launched agent id, the agent state, the reason it is waiting, the timestamp of the last transition, and:

  • Exit metadata. exitCode is set once the PTY exits and is undefined while it runs, alongside spawnedAt from the panel's creation. An agent or terminal that ended tells you how it ended.
  • Parsed check results. lastCheckResult carries a parsed pass/fail verdict from a test or lint run detected in the terminal. It is best-effort rather than authoritative: project.runCheck is the path that returns a real exit code.
  • Armed state. Whether the terminal is in the fleet arming set for a broadcast.

It never blocks and never fails as a whole: an unknown id returns a per-entry error instead of aborting the call.

Also over MCP, at the tiers noted: per-terminal arm and disarm plus a fleet-wide disarm (system), terminal.restart (action), and browser.captureScreenshot (action), which returns real image bytes to the caller. A screenshot dispatched by an agent deliberately does not write to the system clipboard: only a keybinding or toolbar dispatch does that, so an agent cannot clobber what you copied.

Resources

Four resource kinds are exposed, under the URI scheme daintree://<host>/<id>/<verb>. Two support live subscriptions, which emit notifications/resources/updated with no payload: the client re-reads on each notification.

URIBacking actionSubscribable
daintree://worktree/{id}/pulsegit.getProjectPulseYes
daintree://terminal/{id}/scrollbackterminal.getOutput (last 200 lines)No
daintree://agent/{id}/stateterminal.listYes
daintree://project/current/issuesforge.listIssuesNo

Resources authorize through their backing action, which has a consequence worth stating: because no forge.* or git.* id is on the external allowlist, an external session can no longer read the pulse or issues resources. Scrollback and agent state are unaffected.

Prompts

The server registers three MCP prompts. Most clients surface them as slash commands in the input autocomplete.

PromptArgumentsWhat it renders
start_issueissue_number (required)A structured prompt to start work on an issue, with the active worktree and branch filled in, asking the model to summarize the goal, confirm the worktree is the right place, and outline the first two or three steps for sign-off before editing.
triage_failed_agentterminal_id (optional)A diagnosis prompt for a stuck or failed agent. Pass a terminal id and recent output is embedded; omit it to triage the current worktree without specific output.
triage_terminalsNoneA static polling recipe: how to batch terminal.getStatus, when to set includeOutput, and how to pace the next round instead of holding a blocking wait open.

Idempotency dedup

Models replay tool calls during multi-step planning, especially across reconnects. A per-session dedup cache absorbs that for creation tools: within a 120-second TTL a duplicate call returns the original result instead of redispatching, with a 256-entry FIFO cap per session.

A caller can pass an explicit requestKey alongside _meta; without one, the server hashes the action id and the argument object. An in-flight duplicate joins the original promise; a completed one returns the cached result and audits as dedup. A repeat with the same requestKey but different arguments is rejected as a collision rather than silently answered.

A tool earns a place on that allowlist only when all three hold: a retry after a transient error is realistic, the repeat would leave a durable or immediately visible artifact, and an intentional same-argument repeat inside the window is not a normal use case. That last condition is what bounds the list:

GroupMembers
Panel and agent spawnsterminal.new, agent.terminal, agent.launch, recipe.run
Worktree and workflow creationworktree.createWithRecipe, workflow.startWorkOnIssue, worktree.resource.provision, worktree.delete
Forge writesforge.createPR, forge.mergePR, forge.commentOnPR, forge.createIssue, forge.addIssueComment, forge.approvePR, forge.requestChanges
Note

git.commit and git.push were deliberately removed from this list. Neither takes an argument a legitimate repeat varies: git.commit commits whatever the index holds, and git.push gets only a working directory and an upstream flag. So a second push after a new commit (or a repeated wip message) is a same-argument call that would be swallowed as a cached success for work that never happened. The replay they would have absorbed is cheap by comparison: a redundant push prints "Everything up-to-date" and a redundant commit errors visibly on an empty index.

Navigation actions, repeatable commands and idempotent state-sets are excluded for the mirror-image reason: they create no duplicate to suppress, so caching buys nothing and costs correctness. In a set → unset → set sequence, the third call matches the first, the cached success returns, and the state stays unset.

Argument and error conventions

  • Selectors. Scoped actions take worktreeId or worktreePath, and projectId or projectPath. cwd, rootPath and path survive as legacy aliases. Where a default would be silently wrong, the selector is required rather than inferred.
  • Pagination. limit plus cursor or offset, returning items with hasMore, nextCursor and sometimes a total. An empty cursor string is rejected rather than treated as page one.
  • Errors throw. A returned { ok: false } serializes as a successful tool result, so failures must throw to reach the caller as failures. Errors carry a retriable flag; retry a false only once the arguments, context or authorization have changed.
  • MCP Server: enabling the server, connecting a client, the tier model and the audit log.
  • Daintree Assistant: the in-app client that reaches the workbench, action and system tiers.
  • Code Forge: provider setup behind the forge.* tools.
  • CopyTree: what copyTree.generateAndCopyFile bundles.
  • Fleet: the broadcast runs fleet.getRunStatus reports on.