MCP Server
Daintree's local Model Context Protocol server: the four authorization tiers, per-client connect config, host-side confirmation, time-bounded grants, and the audit log.
What the MCP server is
Daintree runs a local Model Context Protocol server, and it inverts the usual relationship. Most MCP servers are tools your agent connects to. Here Daintree is the server. An external client (Claude Code, Codex, or anything that speaks Streamable HTTP) connects to it on 127.0.0.1 and drives the running habitat: launching agents into worktrees, creating worktrees from recipes, polling terminal state, waiting for an agent to go idle. Daintree's own in-app assistant connects to the same server through internal sessions.
The selection rule for the external surface follows from that inversion: keep what only Daintree can do, and drop what the caller can already do for itself. An external agent driving Daintree sits in a terminal with its own shell and its own gh, so git plumbing, forge reads and writes, file reads and project queries are its job, not Daintree's. Worktree lifecycle, recipe execution, agent orchestration and live habitat context have no shell equivalent, so those are what the server exposes.
The server is loopback-only, opt-in, and disabled by default. The default port is 45454. The primary transport is Streamable HTTP at /mcp. A legacy SSE endpoint at /sse still answers for in-app and older callers, but it was deprecated by the MCP spec in revision 2025-03-26 and clients vary in whether they attach the auth header to its separate POST leg: no external client should be pointed at it.
Enable the server
Open Settings (Command-, Control-, Control-, ) and switch to the MCP server tab. Flip the master toggle. The tab is organized as Connection, Port, Authentication, Audit log, and Turn outcome diagnostics.
The server reports one of four runtime states: disabled, starting, ready, or failed. When it is ready, the Connection section shows the bound port and the exact URL a client should use.
If 45454 is taken, Daintree tries the next ten ports in sequence, up to 45464. The URL shown in Connection always reflects the port that was actually bound, so re-copying the config after a fallback is enough to fix a stale client.
Connect a client
The Connection section has a Client picker with three options. Pick the client you are connecting and press Copy MCP config: the snippet arrives in that client's own format. The URL displayed on the tab and the URL inside the copied snippet come from one computation, so the two cannot drift.
| Client | Format | Where it goes |
|---|---|---|
| Claude Code | JSON | .mcp.json in your project, or ~/.claude.json for every project |
| Codex | TOML | ~/.codex/config.toml |
| Other client | Plain transport details | Entered by hand in any Streamable HTTP client |
The Claude Code snippet is the shape claude mcp add --transport http … --header writes:
{
"mcpServers": {
"daintree": {
"type": "http",
"url": "http://127.0.0.1:45454/mcp",
"headers": {
"Authorization": "Bearer daintree_<your-key>"
}
}
}
} The Codex snippet uses a literal http_headers table rather than an environment variable, because the key is copy-pasted rather than injected: Daintree cannot reach the environment of an external Codex process, and the key rotates from this same tab, which would strand an exported variable:
[mcp_servers.daintree]
url = "http://127.0.0.1:45454/mcp"
http_headers = { Authorization = "Bearer daintree_<your-key>" } Other client copies the transport-level truth instead of a config file, for clients whose config schema Daintree has not verified:
Transport: Streamable HTTP
URL: http://127.0.0.1:45454/mcp
Header: Authorization: Bearer daintree_<your-key> Cursor's and VS Code's config schemas are deliberately not claimed as verified here. If you are connecting one of those, use the Other client values and enter them in whatever form that client expects.
Connected external clients are listed under the Connection section, keyed by a hash of the bearer token and labelled with the client's user agent and the last four characters of its key. Each row has a Disconnect button that revokes that client's sessions without rotating the key for everyone else.
API key and the trust boundary
Daintree generates an API key on the first server start, in the format daintree_<32-hex>. It is stored in the app's electron-store and survives restarts. The server validates incoming bearer tokens by comparing precomputed SHA-256 hashes with crypto.timingSafeEqual. A missing or wrong token returns 401 Unauthorized with WWW-Authenticate: Bearer realm="Daintree MCP", writes an auth401 audit record, and ticks the abuse counter.
To rotate, use Rotate API key in the Authentication section. Rotation invalidates every external client holding the previous key at once, so re-copy the config into each one afterwards.
On top of the loopback bind, the transport enforces DNS-rebinding protection: the Host and Origin headers must be 127.0.0.1:<port> or localhost:<port>. Anything else is refused. That second check is what makes the empty-auth fallback below safe.
Authorization tiers
Every session resolves to exactly one of four tiers. Three of them form the in-app ladder used by the Daintree Assistant; the fourth is for external API-key clients.
| Tier | Who gets it | What it grants | Tools |
|---|---|---|---|
workbench | The in-app assistant's read-only setting, and the fallback baseline for any in-app session | Read-only introspection: project and worktree reads, file search and file view, terminal listing and scrollback, git and forge reads, agent state, skills. | 60 |
action | The in-app assistant's default | Workbench plus in-app orchestration: create worktrees from recipes, spawn and restart terminals, launch agents, send and inject prompts, wait for idle, run a project check, drive the browser and dev preview, pick themes. | 110 cumulative |
system | Per-assistant opt-in | Action plus operations that touch disk or external services: delete worktrees, stage and commit and push, arm and disarm terminals for fleet broadcast, write the system clipboard, and the full set of forge writes. | 149 cumulative |
external | API-key bearers | An independently curated flat allowlist of orchestration tools. A peer of the ladder, not a rung above system: it is not a superset of anything. | 25 |
There is no off tier. Turning the in-app assistant's Daintree control off is a separate switch from choosing its tier.
How a session lands in a tier:
- API-key bearer. The
Authorizationheader matches the configured key on a timing-safe comparison, so the session isexternal. - Empty-auth fallback. No key configured and an empty
Authorizationheader also resolves toexternal. This dies the moment a key is set, and it is only safe because non-loopbackHostandOriginvalues are already refused. - Per-pane token. An in-app assistant pane's own token maps to
workbench,actionorsystemfrom that pane's configuration. Neverexternal. - Help-session token. Pinned to the WebContents that minted it and to the action context captured at provision time, so calls dispatch against the worktree you had focused when you launched the assistant.
Anything else falls to the workbench baseline. If getTier finds no live transport at all the call fails closed with SESSION_GONE rather than defaulting to a tier: a revoked bearer must not quietly widen onto the in-app baseline.
The in-app tier picker lives in Settings > Daintree Assistant, and defaults to Action. See Daintree Assistant for what each tier means in that context.
Setting the assistant to System grants destructive and externally visible operations: deleting worktrees, committing and pushing, and creating, merging and commenting on pull requests. Some of those are irreversible and some are visible to your team. The settings tab pins git.push, git.commit and worktree.delete to the top of the blast-radius preview for exactly that reason.
For the tool-by-tool reference (the 25 external tools, why the surface was cut, the payload budgets and what forge over MCP can reach), see Tool surface and budgets.
Two gates, and no third state
Authorization is enforced twice, on purpose. isTierPermitted owns tier membership and runs at tools/call. shouldExposeTool runs at tools/list and layers two hard ceilings on top (a tool marked danger: "restricted" or mcpVisibility: "hidden" is never advertised), then defers to the same membership check, so listing and dispatch can never drift apart.
A third gate narrows introspection. actions.list, actions.search and actions.getSchema are filtered in the main process against the calling session's own tier, so discovery describes the session's surface rather than reaching past it.
There is deliberately no state where a tool is withheld from tools/list but stays dispatchable. An earlier "discoverable" visibility tried that and it does not work: shipped clients build their tool registry from tools/list and reject unlisted names before they ever become requests, so withholding a name is indistinguishable from revoking it. The advertised set and the callable set are the same boundary.
Widening a session, and its limits
When an in-app assistant calls a tool above its tier, the call is denied and a banner names the tool. Two ways to widen, both time-bounded:
- Approve once mints a per-tool grant keyed on
(sessionId, toolId)with a 15-minute sliding TTL, refreshed on each successful dispatch, under a hard 30-minute wall-clock ceiling that the sliding window cannot extend. - Always allow elevates the whole session, and decays back to
workbenchafter 30 minutes of awake time. Sleep and wake do not extend it.
Both target the narrowest sufficient tier rather than blanket-elevating to system. Both TTLs sit at or under the 30-minute idle session timeout, so neither can outlive the session that holds it. Grant lifecycle events (grant.issued, grant.expired, grant.revoked) interleave with tool-call records in the audit log.
If the same (sessionId, toolId) pair is denied twice in a row the banner is suppressed for the rest of the session, but every denial still writes an audit record carrying bannerSuppressed: true. The counter resets when a grant is issued or the session ends.
Confirmation is host-side, always
Tools annotated danger: "confirm" are exposed and dispatchable when the tier permits, but the call is dispatched unconfirmed so a human approves it in Daintree's own native confirmation dialog. The dialog shows the action title, its description, and a redacted argument summary: long strings collapse to <string: N chars> and nested objects to <object>, so a secret in an argument does not land in the prompt.
A client's self-declared elicitation.form capability is never treated as authorization. A headless client could answer its own elicitation and self-approve a destructive call, so confirmation always routes to the host. With no Daintree window open, a confirm-gated call is refused up front with CONFIRMATION_REQUIRED and confirmationChannel: "unavailable" rather than running unattended.
The renderer cancels a pending confirmation after 28 seconds, two seconds inside the 30-second main-process dispatch deadline, so the audit log records a clean timeout rather than a torn-down dispatch. Concurrent confirm calls queue behind the visible dialog. While it is open the audit record sits at confirmation-pending and then resolves to approved, rejected or timeout.
Security posture over MCP
The MCP surface is the one place where a non-human caller reaches into a running habitat, so several boundaries exist only here.
project.getSettingsno longer returns decrypted secrets. It projects the full settings payload (which carries decrypted secure environment variables and a large icon blob) down to an agent-visible field set before returning, and dispatch parses that result against the action's declared schema as a second boundary. Its own description still warns that run-command strings come back verbatim and may have a credential inlined by whoever wrote them.- Introspection is scoped to the calling session's tier.
actions.list,actions.searchandactions.getSchemadescribe the caller's own surface, not the registry. - Destructive calls require host confirmation and never trust client elicitation. See above.
- An unpinned tool call routes by focus order, and reports where it landed. An external session follows window focus on every call, so a long-running agent's calls can retarget mid-session when you switch workspace. Every dispatch reports the workspace it actually resolved to: its kind (project or scratch), id and path, so that drift is observable to the caller instead of silent.
- Terminal dispatch requires an explicit terminal id. An agent or MCP caller sending input to "the terminal" is no longer resolved implicitly.
- The external API-key
fullToolSurfaceopt-in is gone. It short-circuited both tier gates and treated the author-setdangerandmcpVisibilityfields as the ceiling, which exposed 335 of 426 actions to any API-key caller. It was removed outright rather than left dormant: the MCP spec is explicit that tool annotations are untrusted UX hints, not an access-control boundary, and the server-side allowlist is the enforceable one. Nothing widens the external allowlist. - The
github.*action aliases were removed in v0.18.github.listIssues,github.listPullRequests,github.getIssueByNumber,github.checkCli,github.getRepoStatsandgithub.openPRdo not exist at any tier. Everything forge-shaped isforge.*and routes through the provider abstraction described in Code Forge.
For the process-level picture (sandboxing, IPC hardening, secret scrubbing, git hardening), see Security & Privacy.
Audit log
Every dispatch writes a record to a ring buffer, capped at 500 entries by default and configurable between 50 and 10,000 in the Audit log section. Records carry an id, timestamp, tool id, session id, tier, a redacted argument summary, the result, a severity, and optional errorCode, durationMs, confirmationDecision, tierHint, bannerSuppressed, turnId and repeatCount fields. Grant lifecycle events share the buffer and carry a discriminator so they can be filtered apart.
Filters narrow the view by time range and by result. The viewer also renders a per-tool latency table — n, p50 and p95 columns, with a separate row per outcome, success and failed. The SLO band is tagged against the p95 alone: Instant under 200 ms, Fast under 1000 ms, Standard up to 5000 ms, Slow beyond that. A p95 of zero carries no band.
Once the buffer holds at least 50 records, four anomaly detectors run against new dispatches:
- Latency drift: a per-tool modified z-score using the median absolute deviation; 3 or higher flags the record.
- Failure cluster: three or more failures inside any 10-record sliding window.
- p95 outlier: a tool's p95 measured against the median p95 across all tools, with a five-tool minimum.
- First-seen combination: the first appearance of a given tool-and-tier pair.
Export the buffer as newline-delimited JSON, or copy it as pretty-printed JSON. Clear wipes it behind a confirmation.
The same viewer and latency table also sit on Settings > Daintree Assistant, under Advanced diagnostics. That copy deliberately drops external-tier records: it is there to account for what the in-app assistant did, so it never shows external client traffic. Read the MCP server tab's own Audit log section for that.
Turn outcome diagnostics
When an in-app assistant moves from active to passive, Daintree classifies the turn against a priority waterfall of eleven outcomes: answered, hedged, refused, no docs found, tier rejected, MCP not ready, agent stuck, tool error, resume stale, reasoning loop, and unknown. Every tool call inside the turn carries the same turn id, so the audit log can be filtered or aggregated per turn. The Turn outcome diagnostics section shows the per-tool rollups.
The two worth acting on are tier rejected, which means a tier or grant denial ended the turn and the assistant probably needs a wider tier, and reasoning loop, which fires when the same tool and argument summary repeat three or more times inside one turn.
Runtime state, restart and the abuse cap
If the HTTP server closes unexpectedly, a supervisor restarts it with exponential backoff: a 500 ms base, a 2× multiplier, capped at 15 seconds, with ±250 ms of jitter per attempt. Thirty seconds of stable uptime resets the counter. After five consecutive failures the supervisor parks the server in failed and shows the last error next to the toggle.
Sessions that go 30 minutes without activity are reaped, which revokes their grants with reason session-ended. The next call re-authenticates and mints a fresh session; most clients reconnect without you noticing.
There is also an abuse policy, off by default. Turn it on and the server counts 401s and tier-mismatch denials in a 60-second sliding window. Five denials revokes the session and notifies the renderer. Reconnecting mints a fresh one: it is a brake on a misconfigured or looping client, not a lockout.
Troubleshooting
- 401 with a config you just copied. Some clients drop or misplace the
Authorizationheader on Streamable HTTP. Re-copy the snippet for your specific client from the picker rather than adapting another client's. Check the key has not been rotated since you pasted it. TIER_NOT_PERMITTED. The tool exists but this session's tier does not reach it. For an in-app assistant, raise the tier in Settings > Daintree Assistant or approve the call from the banner. For an external client, the tool is off the curated allowlist and nothing widens it. Do that work in the caller's own shell instead.- Connection refused, or a 404. Daintree may have bound a fallback port. Read the URL from the Connection section and re-copy the config.
- Wrong transport. Point external clients at
/mcp./sseis legacy and its header handling across clients is unreliable. - A confirm-gated call refused with
CONFIRMATION_REQUIRED. No Daintree window was open to show the dialog. Open the app and retry. - Idle expiry. A client idle for 30 minutes loses its session and reconnects on the next call.
- Loopback rejected. The transport requires
HostandOriginto be loopback. Proxies and container setups that rewrite them will be refused.
Related pages
- Tool surface and budgets: the 25 external tools, the payload caps, forge over MCP, resources and prompts.
- Daintree Assistant: the in-app client that uses the workbench, action and system tiers.
- Security & Privacy: the process-level model this server sits inside.
- Code Forge: provider setup for the
forge.*tools. - Recipes: what
recipe.runandworktree.createWithRecipeexecute. - Plugins: plugin-contributed MCP servers, which are a separate surface from this one.