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.
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.
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.
| Tool | What it does |
|---|---|
actions.list | Enumerate the calling session's authorized action surface. |
actions.search | A compact ranked shortlist of authorized actions, the answer when the tools/list schemas are too large to reason over. |
actions.getSchema | One action's manifest entry plus whatever schemas it publishes. |
actions.getContext | The live habitat context: active project, worktree and terminal. |
mcp.surface | This session's tool surface as versioned data with a stable hash. |
agent.launch | Launch an agent (built-in, user-defined or plugin-contributed) in a worktree. |
agent.listAvailable | The authoritative effective agent registry and live launchability. Nothing the caller's own shell can answer. |
fleet.getRunStatus | A read-only supervision snapshot of the fleet broadcast run owned by the dispatching window. |
recipe.list | List the workspace recipes available to this project. |
recipe.run | Run a recipe. Confirm-gated. |
skills.search | Search plugin-contributed skills. |
skills.load | Load a skill's markdown into the agent's context. |
terminal.list | Enumerate the terminal panels in the view. |
terminal.new | Create a terminal panel. |
terminal.getOutput | Read a terminal's scrollback. |
terminal.getStatus | Batched agent and process state across many terminals, optionally with output tails. |
terminal.sendCommand | Submit text to a terminal. Returns once submitted, not when the work finishes. |
terminal.inject | Stage text into a terminal's input without submitting it. |
terminal.waitUntilIdle | A bounded long-poll until an agent's state machine leaves working. |
terminal.waitUntilIdleBatch | The same wait, fanned across several terminals. |
worktree.list | List the project's worktrees. |
worktree.getCurrent | The active worktree. |
worktree.createWithRecipe | Create a worktree and apply a recipe to it. |
worktree.setActive | Switch the active worktree. |
copyTree.generateAndCopyFile | Apply 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 owngh. All of it stays available to the in-app assistant, which no third-party client cap applies to. terminal.close.terminal.listenumerates 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.getRunStatusis read-only. To fan out, send oneterminal.sendCommandper terminal and watch with a batchedterminal.getStatusor a boundedterminal.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.
| Field | Meaning |
|---|---|
manifestVersion | The 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. |
appVersion | The running Daintree build, so a captured response is diagnosable on its own. |
tier | The tier this call was admitted at, and the tier the rest of the response describes. |
hash | Hex SHA-256 over the canonical form of the surface. |
tools | Sorted 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/listis 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 withactions.searchand thenactions.getSchema.- Resolve worktree and terminal ids before scoped actions.
terminal.sendCommandreturns when the text is submitted, not when the work finishes. Preferterminal.waitUntilIdleorterminal.waitUntilIdleBatchover tight polling, and readidleReason,waitingReasonandexitCodebefore the next step. A terminal with no tracked agent returnsidleReason: "unknown"immediately, which is not proof a shell command finished. - Authorization is tiered, and
TIER_NOT_PERMITTEDmeans what it says. Honor theretriableflag 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.
| Surface | Cap | Behavior at the cap |
|---|---|---|
| Tool result text | 50 KiB | Truncated 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. |
structuredContent | Measured separately | Re-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 text | 50 KiB | A threshold rather than a hard ceiling; appends a [truncated] marker. |
| Project check output | Tail-preserving | Keeps 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 diff | 24 KiB default, 1 MiB maximum | Paged with an explicit offset and maxBytes. |
| Commit metadata | 512 bytes subject, 1 KiB body | Truncated per field, so one pathological commit message cannot crowd out a whole listing. |
| CopyTree content preview | 32 KiB read, 48 KiB serialized | The 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.getCIStatusreturns 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.listIssueCommentsreads 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.
exitCodeis set once the PTY exits and is undefined while it runs, alongsidespawnedAtfrom the panel's creation. An agent or terminal that ended tells you how it ended. - Parsed check results.
lastCheckResultcarries a parsed pass/fail verdict from a test or lint run detected in the terminal. It is best-effort rather than authoritative:project.runCheckis 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.
| URI | Backing action | Subscribable |
|---|---|---|
daintree://worktree/{id}/pulse | git.getProjectPulse | Yes |
daintree://terminal/{id}/scrollback | terminal.getOutput (last 200 lines) | No |
daintree://agent/{id}/state | terminal.list | Yes |
daintree://project/current/issues | forge.listIssues | No |
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.
| Prompt | Arguments | What it renders |
|---|---|---|
start_issue | issue_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_agent | terminal_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_terminals | None | A 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:
| Group | Members |
|---|---|
| Panel and agent spawns | terminal.new, agent.terminal, agent.launch, recipe.run |
| Worktree and workflow creation | worktree.createWithRecipe, workflow.startWorkOnIssue, worktree.resource.provision, worktree.delete |
| Forge writes | forge.createPR, forge.mergePR, forge.commentOnPR, forge.createIssue, forge.addIssueComment, forge.approvePR, forge.requestChanges |
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
worktreeIdorworktreePath, andprojectIdorprojectPath.cwd,rootPathandpathsurvive as legacy aliases. Where a default would be silently wrong, the selector is required rather than inferred. - Pagination.
limitpluscursororoffset, returning items withhasMore,nextCursorand sometimes atotal. 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 aretriableflag; retry afalseonly once the arguments, context or authorization have changed.
Related pages
- 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.generateAndCopyFilebundles. - Fleet: the broadcast runs
fleet.getRunStatusreports on.