Skip to main content

MCP Server

Daintree's local Model Context Protocol server: the three authorization tiers (the Core and Full in-app tool sets and the external tier), per-client connect config, host-side confirmation, time-bounded grants, and the audit log.

Updated
Reviewed

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.

Settings > MCP server, with the Connection section expanded

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.

ClientFormatWhere it goes
Claude CodeJSON.mcp.json in your project, or ~/.claude.json for every project
CodexTOML~/.codex/config.toml
Other clientPlain transport detailsEntered 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>
Note

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 ends all sessions using that bearer. Clients sharing the copied global key are disconnected together. The key itself is unchanged, so they can authenticate again; rotate it to revoke their old credential.

Scoping a client to one workspace

Beside Copy MCP config sits Copy config for this project, which adds one header: Daintree-Workspace-Id. The plain config follows focus, landing every call in whichever Daintree window you focused last. The scoped one binds, so every call routes to that workspace whichever window is in front, and two agents driving two projects cannot retarget each other.

Clients that cannot set an extra header can use the workspaceId query parameter on the MCP URL. When both are supplied they must agree; blank, repeated, or conflicting selectors are rejected.

A binding is fixed at the handshake and immutable for the session's life; to target another workspace, start a new session, and a workspace id arriving later that disagrees with it is refused rather than ignored. Only external API-key sessions may bind at all.

Note

An unreachable workspace is not a handshake failure. If the bound workspace has no window open, or is open in two, the handshake completes on the id alone and each call fails with a retriable SESSION_BINDING_GONE naming it, rather than being routed elsewhere. No client SDK retries a failed initialize, so refusing there would cost the client its Daintree tools for the whole session.

Background workspace limits

A bound session can address a retained background project view without switching your foreground project. Workspace-bound external sessions cannot request host confirmation: confirm-gated operations are withheld from discovery and refused at dispatch, even if you subsequently focus that project. For work that needs approval, use the app directly or a separate unbound client session while the intended project is focused. A quiet bound session does not keep its renderer alive indefinitely; if the view has been evicted, reopen the project and retry.

See the selector validation, dispatch and confirmation policy, and in-flight view protection for the precise contract.

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: Host must be 127.0.0.1:<port> or localhost:<port>. Origin is optional; when present, it must be http://127.0.0.1:<port> or http://localhost:<port>. Other values are refused. These checks supplement authentication; localhost is not isolation from other programs running as your user.

Authorization tiers

Every session resolves to exactly one of three tiers. Two of them are the in-app tool sets used by the Daintree Assistant and by agent panes a project connects to the server; the third is for external API-key clients.

TierWho gets itWhat it grants
coreThe Daintree Assistant's default, a project's agent panes when the project is set to Core, and the fallback baseline for any in-app sessionOrchestration: create worktrees and wait on their setup or pull request, launch agents, send prompts, read and wait on terminals, interrupt, move, rename and close them, delete a worktree the session created (with confirmation), and search actions.
fullOpt-in, per assistant or per projectCore plus recipes and workflows, project checks, forge and git reads, CopyTree, the unscoped worktree delete and resource lifecycle, kill, restart and close-all for terminals, skills, diagnostics and the plugin-authoring loop.
externalAPI-key bearersAn independently curated flat allowlist of orchestration tools. A peer of the tool sets, not a rung above full: it is not a superset of anything.

Git and forge writes, file reads, UI navigation, settings writes, fleet arming and bulk kills are on no tier. Older saved settings read in place: workbench and action as core, system as full.

There is no off tier. Turning the in-app assistant's Daintree control off is a separate switch from choosing its tool set.

How a session lands in a tier:

  • API-key bearer. The Authorization header matches the configured key on a timing-safe comparison, so the session is external.
  • Empty-auth fallback. No key configured and an empty Authorization header also resolves to external. This dies the moment a key is set, and it is only safe because non-loopback Host and Origin values are already refused.
  • Per-pane token. An agent pane's own token maps to core or full from its project's Agent integrations setting. Never external. It prompts, closes and deletes only what it created itself.
  • 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 core 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 assistant's Tool set lives in Settings > Daintree Assistant, and defaults to Core. See Daintree Assistant for what each set means in that context.

Warning

Core already permits deleting a worktree the session created, and Full permits deleting any eligible worktree and tearing down its resources. Both run project-defined teardown commands. Tier membership and confirmation are separate gates: read the settings preview before widening access.

For the tool-by-tool reference (the external tool roster, why the surface was cut, the payload budgets and what forge over MCP can reach), see Tool surface and budgets.

Listing and dispatch authorization

Tier checks govern both tools/list and tools/call. Restricted or hidden actions remain unavailable. Discovery through actions.list, actions.search and actions.getSchema is filtered to the caller's effective surface, including eligible live grants. Workspace-bound external sessions have the additional confirmation ceiling described above. Discovery is not a way around these checks.

Widening a session, and its limits

When an in-app assistant calls a tool outside its tool set, the call is denied and a Tool not permitted banner names the tool. Two ways to widen, both time-bounded:

  • Allow this tool 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.
  • Set project default saves the required tool set as the project's default for agent panes, and elevates this help session, returning it to its original tier after 30 minutes of awake time. Sleep and wake do not extend it.

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.

Host confirmation and automation grants

Tools annotated danger: "confirm" are exposed and dispatchable when the tier permits, and normally dispatch unconfirmed so a human approves them 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>, to reduce accidental disclosure. Redaction is not a guarantee that every possible secret is recognized.

Warning

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 authorization must come from Daintree. A live host-issued native automation grant can pre-authorize an eligible tool within its approved scope; the ordinary per-tool tier grant does not itself bypass confirmation. 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.

Host-side worktree deletion confirmation. Current external clients use worktree.deleteOwned for worktrees created by their session.

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.getSettings no 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.search and actions.getSchema describe the caller's own surface, not the registry.
  • Confirm-gated calls require host approval, through a dialog or an eligible native automation grant, and never trust client elicitation. See above.
  • Routing is bound or observable, never silent. A workspace-scoped session fails closed rather than falling through to another window; an unscoped one follows focus and can retarget mid-session. Either way every dispatch reports the workspace it resolved to — kind, id and path.
  • Terminal dispatch requires an explicit terminal id. An agent or MCP caller sending input to "the terminal" is no longer resolved implicitly.
  • There is no fullToolSurface opt-in. External API keys remain on the curated external allowlist.
  • The github.* action aliases were removed in v0.18. github.listIssues, github.listPullRequests, github.getIssueByNumber, github.checkCli, github.getRepoStats and github.openPR do not exist at any tier. Everything forge-shaped is forge.* 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.

After at least 50 records, the viewer flags latency drift, failure clusters, cross-tool p95 outliers and first-seen tool/tier combinations. Treat these as diagnostic signals, not proof that an action was unsafe.

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.

The Assistant tab’s copy of the viewer, which excludes external-tier records

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 Authorization header 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, switch the Tool set to Full in Settings > Daintree Assistant or allow the tool 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. /sse is 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.
  • SESSION_BINDING_GONE. A scoped client whose workspace has no reachable renderer, or an ambiguous route. Reopen it, or close the duplicate, and retry; nothing client-side changes.
  • Loopback rejected. The transport requires an allowed loopback Host and validates Origin when present. Proxies and container setups that rewrite them will be refused.
  • Tool surface and budgets: the external tool roster, the payload caps, forge over MCP, resources and prompts.
  • Daintree Assistant: the in-app client that uses the Core and Full tool sets.
  • Security & Privacy: the process-level model this server sits inside.
  • Code Forge: provider setup for the forge.* tools.
  • Recipes: what recipe.run and worktree.createWithRecipe execute.
  • Plugins: plugin-contributed MCP servers, which are a separate surface from this one.