Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MCP Server

The --mcp flag runs Intendant as a Model Context Protocol server over stdio JSON-RPC (src/bin/caller/mcp/). It lets an external agent (Claude Code, Codex, Kimi Code, etc.) observe and control Intendant through a broad operational tool surface: session actions, display/CU/frame tools, shared-view collaboration, live audio, managed context, and controller orchestration. Presentation-only dashboard affordances are not necessarily one-for-one tools.

Architecturally the MCP server is a frontend peer of the dashboard: it subscribes to the same EventBus, and user intents are ControlMsg values everywhere — the web dashboard and the Unix control socket dispatch them to the centralized control_plane.rs (see Autonomy & Approvals for why frontends are display-only), and the MCP server’s approval/input tools apply the same state helpers as its own ControlMsg arms (resolve_pending_approval & co. in mcp/mod.rs; the former MCP-only UserAction enum is retired). --mcp is its own run mode and is not implied by --web.

Running

# MCP server on stdio
./target/release/intendant --mcp "Deploy the application"

# With provider/model overrides
./target/release/intendant --mcp --provider anthropic --model claude-sonnet-4-6-20250929 "Fix the tests"

# With an autonomy preset
./target/release/intendant --mcp --autonomy high "Refactor the auth module"

In MCP mode, stdin/stdout are reserved for JSON-RPC, so the initial task is taken from the command line (or the server starts idle and accepts start_task).

Client Configuration

Add Intendant to your MCP client config (Claude Code ~/.claude/claude_desktop_config.json):

{
  "mcpServers": {
    "intendant": {
      "command": "intendant",
      "args": ["--mcp", "Your task description here"]
    }
  }
}

Tools

The full MCP tool surface (dispatched in call_tool_by_name) is broad. For model clients that front-load tool schemas into every request, prefer the HTTP transport’s tool_profile=core query parameter and the intendant ctl CLI for lazy discovery. tool_profile=core advertises get_status; whoami (the caller’s own identity, for provenance in memory/agenda writes); the agent-to-user collaboration primitives (post_session_note, ask_user, notify_user); Agenda and Memory list/read/propose tools; the shared-view tools; and the minimal real-display/CU set (list_displays, grant_user_display, request_user_display, revoke_user_display, take_screenshot, read_screen, execute_cu_actions, display_readiness) — managed and vanilla alike. Managed context additionally advertises rewind/backout and fission tools. Omitting tool_profile keeps the historical full tool list. Profile filtering applies to tools/list only — hidden HTTP tools remain callable (the lazy ctl tools call path). Authorization is separate: see the next section.

The tables below describe the full daemon HTTP MCP surface as well as the stdio-backed tools. Bare --mcp stdio mode does not carry the daemon’s Agenda or Memory service handles and does not advertise those HTTP-only definitions; the wired daemon /mcp surface is the shape that exposes them.

/mcp authorization

Every POST /mcp request binds to a principal in the same local IAM system that gates the dashboard and federation surfaces (Trust Architecture), and every tools/call is evaluated at call time against that principal’s permissions via a per-tool operation map (mcp_tool_operation in mcp/tool_gate.rs; e.g. execute_cu_actions and grant_user_display require display.input, start_task requires task.run, unclassified tools require runtime.control). tools/list is filtered to what the principal may actually call. The display tools carry a second, separate gate: a user_session target needs the standing user-display grant unless the bound principal is an owner/root caller (the trusted dashboard, an independently enrolled direct-mTLS role:root user client, or bare local loopback — derived by ToolCallerTrust without widening AccessPrincipal::is_owner_surface); the stdio transport, being wired up by the owner’s own client config, always counts as an owner surface. See Computer Use. Binding order:

  1. Peer daemons (mTLS peer identity) use their peer-profile principal.
  2. Supervised backends receive a bearer only in their cleared child environment and send it in the Authorization header. Their argv-visible MCP config contains the environment-variable name, never the value. INTENDANT_MCP_URL remains the private environment bootstrap for ctl. The bearer is session-scoped — derived from the daemon’s per-process token and the session_id — so it authenticates exactly that agent session (principal:agent-session:<id>). Possession of the raw per-process token remains root-equivalent. Explicit-but-wrong tokens are refused with 401. A session whose binding is known but whose grant has lapsed (expired or revoked) binds the scoped principal and is denied with the real reason — it does not fall back to default trust; only sessions with no binding at all do.
  3. Browser pages may only call /mcp from this daemon’s own origin (or the macOS app scheme) and then bind like any dashboard HTTP request (an enrolled mTLS certificate principal or trusted-local root). Foreign origins get 403 — same posture as the rest of /api/*.
  4. Tokenless loopback processes bind to principal:local-process:loopback. Tokenless non-loopback requests are refused. Once any agent_session binding exists — even one whose grant has since expired or been revoked — this path fails closed (401) until an explicit local_process grant states what bare loopback callers may do; otherwise a scoped agent could shed its injected token and re-enter as the root-compatible local default, making its grant decorative. A lapsed local_process grant likewise denies rather than restoring the open default.

The rule across all of these: once a principal is named, its authority comes only from grants, and a lapsed grant means “no” — never “back to defaults”. Security posture only relaxes when a person explicitly relaxes it: re-grant role:root (to the "*" agent principal or to local_process) to restore the implicit-trust behavior visibly and auditable, rather than by timer or revocation side effect.

By default the supervised-agent, token-holder, and local-loopback principals are root-compatible, so bare intendant ctl on the daemon host and existing supervised backends keep working with zero ceremony. Root-compatible does not mean unconstrained, though: independent of IAM, agent-session provenance is contained on the approval/settings surface. A caller bound as an agent session (session-scoped token, or the process token naming a session) is refused set_autonomy and approve_all outright — both rewrite the daemon-global shared autonomy, which would let a supervised agent widen its own approval policy — and approve/deny/skip resolve only approvals raised by the caller’s own session (cross-session resolution is denied; unknown ownership fails closed). The same containment covers daemon lifecycle: quit, schedule_controller_restart, and cancel_controller_restart are refused to agent-session callers (a supervised agent must not stop or bounce its supervisor), while controller_turn_complete (the session’s own completion signal inside an owner-scheduled restart) and the controller-loop halt tools (the autonomous loop’s self-stop verbs) stay open. This mirrors grant_user_display’s owner-surface rule: owner surfaces (dashboard, intendant ctl on loopback, enrolled root mTLS clients, the stdio transport) and deliberately granted non-agent principals keep the full surface. The point of the binding is that the owner can now scope them: an agent_session grant (exact session_id, or "*" for every supervised agent) or a local_process grant against POST /api/access/iam/user-client-grants pins that principal to a role, and call-time enforcement + tools/list follow it. Example — cap every supervised agent at operator (no runtime control, no settings/access administration):

curl -X POST http://localhost:8765/api/access/iam/user-client-grants \
  -H 'Content-Type: application/json' \
  -d '{"kind": "agent_session", "session_id": "*", "role_id": "role:operator"}'

Scoping any agent session flips the tokenless loopback default to fail-closed, so pair it with an explicit statement of what your own bare intendant ctl gets (root keeps it exactly as before, now as a visible, revocable grant):

curl -X POST http://localhost:8765/api/access/iam/user-client-grants \
  -H 'Content-Type: application/json' \
  -d '{"kind": "local_process", "role_id": "role:root"}'

The shared per-process token still exists as the transport-layer fallback (and is what the strict-TLS loopback-cleartext exception checks), but possession of it is no longer the authorization story — grants and the evaluator are.

CORS on /mcp matches the gate: responses echo Access-Control-Allow-Origin only for the daemon’s own origin or the app-bundle scheme (which genuinely needs it); foreign origins and non-browser clients get no CORS grant at all. With the patched managed Codex binary, rewind_backout mode="fork" creates a new Codex thread while inheriting the lineage prompt-cache key from the saved rollout; same-thread restore remains available when the current thread should be rewritten in place.

The CLI mirrors the broad surface without loading every schema into model context:

"${INTENDANT:-intendant}" ctl --help
"${INTENDANT:-intendant}" ctl tools list
"${INTENDANT:-intendant}" ctl tools schema take_screenshot
"${INTENDANT:-intendant}" ctl tools call grant_user_display --args '{}'
"${INTENDANT:-intendant}" ctl display grant-user
"${INTENDANT:-intendant}" ctl display screenshot --target user_session --output screen.png

Full MCP tool groups:

Status & logs (observation)

ToolDescriptionParams
get_statusProvider, model, turn, budget %, phase, autonomy, verbosity, tokens.
whoamiThe caller’s own gate-resolved identity, for provenance in memory/agenda writes: supervised callers get their daemon session id, backend harness (claude-code/codex/kimi/native) with its harness session id, wrapper aliases (restart/resume rotations of the same conversation), project root, and log dir; unsupervised callers get supervised:false plus their principal id. Claims a session only when the call authenticated with that session’s token — never from request fields. Also intendant ctl whoami.
get_logsLog entries, cursor-paginated and level-filterable. Without session_id, HTTP/ctl reads the daemon’s currently observed session.session_id?, since_id?, level_filter?, limit?
get_pending_approvalThe current pending approval request (or null).
get_pending_inputThe current pending askHuman question (or null).

Interactive actions

ToolDescriptionParams
approveApprove a pending command. Owner surfaces only; HTTP/ctl routes the exact prompt id and owning session through the daemon control plane.id
denyDeny and stop. Owner surfaces only; HTTP/ctl routes through the daemon control plane.id
skipDecline this command and let the agent continue. Owner surfaces only; HTTP/ctl routes through the daemon control plane.id
approve_allApprove and set autonomy to Full. Owner surfaces only (daemon-global autonomy escalation).id
respondAnswer an askHuman question.text
post_session_notePost a display-only note into the session transcript — rendered live in the dashboard and persisted for replay, never added to any model’s context. Optional base64 images are committed to the session upload store and rendered as clickable thumbnails. Caps: 16 KB text, 6 images, 4 MB per image, 8 MB total; raster types only (image/png, image/jpeg, image/gif, image/webp, image/bmp). Session-scoped callers post into their own session by default.text, images? ([{media_type, data, name?}]), session_id?, source?
ask_userAsk the user one structured question on the dashboard question rail and block until answered or the wait expires. A question requests input, never permission: it is never auto-approved and answering it never widens autonomy. 0–4 options; free-text answers are always accepted (zero options = free-text only). Up to 4 preview cards render above the options (show, then ask — prototype variants, before/after states): html must be one self-contained document, rendered only inside a sandboxed opaque-origin iframe (scripts run; external fetches and daemon APIs do not resolve); image is base64 raster (session-note MIME allowlist); text renders preformatted. Blob kinds commit to the session upload store and travel as references. Caps: 2 MB/html, 4 MB/image, 4 KB/text, 8 MB total. Returns {status, answer, answers}answered carries the choice(s); timeout/dismissed/pass carry best-judgment guidance; shapes with no answerable frontend auto-answer immediately with the same guidance. Session-scoped callers ask as their own session. Also intendant ctl ask (whose --preview-html/-image LABEL=FILE flags read the files ctl-side, under the caller’s own sandbox).question, options? ([{label, description?}]), previews? ([{label, html | image+media_type | text}]), header?, multi_select?, wait_seconds? (default 300, max 900), session_id?
notify_userFire-and-forget notification to the user; returns immediately, renders as a dashboard toast plus a transcript row (persisted for replay), never enters model context. urgency escalates delivery: info (default) dashboard-only; attention + tab badge and hidden-tab browser notification; urgent + an immediate content-free push nudge to the owner’s opted-in browsers. Cap: 4 KB text. Also intendant ctl notify.text, title?, urgency? (info/attention/urgent), session_id?
set_autonomySet autonomy. Denied to agent-session callers — the setting is daemon-global.level: low/medium/high/full
set_verbositySet log verbosity.level: quiet/normal/verbose/debug
start_taskStart work, route a follow-up to session_id, or resume a persisted external-agent wrapper. A non-empty reference_frame_ids list is the supervisor’s CU-routing gate; display_target selects the display for that grounded request but is not sufficient by itself.task, session_id?, orchestrate?, reference_frame_ids?, display_target?
quitShut down the agent.

start_task currently has two routing caveats. A new-session CU request needs at least one frame id that resolves in the frame registry: a display_target alone, or only stale/unknown frame ids, is accepted and acknowledged as “CU task dispatched” by the MCP edge but falls through to an ordinary task in the supervisor. With session_id, any supplied frame ids and display_target are discarded and only the text is routed as a follow-up. Treat those acknowledgments as enqueue acknowledgments, not proof that grounded CU started; both behaviors are tracked implementation defects.

Agenda & Memory

Agenda is the durable, append-only parking ledger; Memory is a bounded provenance-labeled claim plane. Both render their text as quoted data, never instructions. Agenda effect approval/revocation is owner-only even though agents and peers may propose an effect. Memory proposals enter the candidate lane; this slice exposes no judgment command.

ToolDescriptionParams
agenda_listList oldest-first items plus Open / Done / Retired counts; optionally filter by status.status?
agenda_opApply one tagged operation: add, answer, patch, complete, reopen, retire, propose_effect, approve_effect, or revoke_effect.operation-specific op shape
memory_searchBounded claim search (default 10, maximum 50); candidates are excluded unless explicitly requested. Responses report effective durability.query?, limit?, include_candidates?
memory_readRead one claim by an id prefix of at least eight hex characters.id
memory_proposePropose a typed, sensitivity-labeled candidate. Authorship comes from the gate-bound caller, not writer-supplied context fields.kind, statement, sensitivity?, session?, project?, model?, labels?

Display, computer use & frames

ToolDescriptionParams
list_displaysEnumerate displays with their session state.
take_displayOptional dashboard signal that an agent is using a display; it neither grants input authority nor is required before screenshot/CU calls.display_id
release_displayRelease control of a display.display_id, note?
grant_user_displayGrant access to the user’s real display session (owner surfaces only — this call is the opt-in); on Wayland, enable Allow Remote Interaction in the GNOME portal before clicking Share so CU input works.display_id?
request_user_displayAsk the user for their display: raises the dashboard doorbell popup with your reason and blocks for their click — the only thing that can grant it (never auto-approved; see Autonomy — the display request rail). access="view" shares the stream without CU input; "view_and_control" requests the full grant.reason, access?, wait_seconds?, session_id?
revoke_user_displayRevoke access to the user’s real display session.display_id?, note?
take_screenshotCapture a screenshot (returns image content).display params
read_screenUser session’s frontmost-app accessibility tree — macOS AX, Linux AT-SPI, or Windows UIA.display_target?, format?, full_values?
display_readinessProbe display authority, capture/accessibility permission, target availability, and input backend live; names each missing layer.display_target?
execute_cu_actionsRun a batch of computer-use actions.CU action params
list_framesList captured video frames.filter params
read_frameRead a specific frame.frame_id

Shared-view collaboration

These tools control the agent-owned display presentation the user sees. They do not silently grant keyboard/mouse authority: in particular, request_shared_view_input only raises an advisory request and the user must click the dashboard control.

ToolDescriptionParams
show_shared_viewOpen/foreground a shared display, optionally with an initial highlighted region.display_target?, display_id?, reason?, focus_region?
hide_shared_viewDismiss the shared-view banner and focus overlay.reason?
focus_shared_viewHighlight a normalized region and optional note.region, display_target?, display_id?, note?
clear_shared_view_focusClear only the focus annotation; safe when none exists.reason?
request_shared_view_inputAsk the dashboard user to take input authority; never grants it.display_target?, display_id?, reason?
capture_shared_view_frameForeground the shared view and return its current frame as an MCP image.display_target?, display_id?, reason?

Managed context & fission

These definitions are advertised only for sessions whose Codex managed-context mode is enabled; calls against a vanilla session fail with an explicit disabled-mode error. Rewind tools operate on exact Codex item ids. Fission forks the completed-turn context into real sibling sessions and records their group/canonical state in the lineage ledger.

ToolDescriptionParams
list_rewind_anchorsReturn bounded exact rewind anchors, with optional paging/search and density/recovery estimates.session_id?, paging/filter/density flags
inspect_rewind_anchorInspect a compact window around one exact anchor.item_id, session_id?, radius?
rewind_contextSchedule rollback to anchor.position (before/after) and inject a required carry-forward primer.anchor, reason, primer, session_id?, preserve?, discard?, artifacts?, next_steps?
rewind_backoutInspect, restore, or fork/back out a prior rewind record.record_id, session_id?, mode?, name?
fission_spawnFork 1–4 full-context sibling branches; write-scoped branches use isolated worktrees by default.branches, session_id?, use_worktree?
fission_controlwait, import, cancel, or detach one fission branch/group.group_id, op, session_id?, branch_session_id?, timeout_s?
claim_fission_canonicalClaim or compare-and-swap the group’s canonical continuation.group_id, branch_session_id, expected_canonical_session_id?

Browser workspaces

Browser workspaces are addressable browser-control surfaces for agent/human collaboration and headed UI testing. The first executable backend launches a managed local Chromium-family browser with an isolated profile and Chrome DevTools Protocol metadata. On macOS, Intendant does not launch the user’s installed /Applications/Google Chrome.app by default; use provider=system_cdp or INTENDANT_BROWSER_WORKSPACE_ALLOW_SYSTEM_BROWSER=1 to opt into system Chrome/Chromium, and use INTENDANT_BROWSER_WORKSPACE_EXECUTABLE for an explicit browser binary. Run intendant setup browsers to install Chrome for Testing into Intendant’s managed cache. The wire contract already carries provider and peer_id fields so Playwright/Agent Browser adapters and federated peer-hosted browsers can slot in later. Each workspace has a lease, so concurrent agents must explicitly acquire it and use force to take over an active holder.

ToolDescriptionParams
browser_workspace_providersReport available workspace providers.
list_browser_workspacesList active browser workspaces and leases.
create_browser_workspaceLaunch/register a workspace.url?, label?, provider?, peer_id?, owner_session_id?, profile_dir?
acquire_browser_workspaceAcquire a workspace lease.workspace_id, holder_id, holder_kind?, note?, force?
release_browser_workspaceRelease a workspace lease.workspace_id, holder_id?, note?
close_browser_workspaceClose a workspace and terminate its local browser process when owned here.workspace_id, reason?

Live audio

ToolDescriptionParams
spawn_live_audioSpawn an untrusted live-audio voice session.id, provider, playbook, response_schema, …

Peer federation

The agent-facing surface for peer federation: inspect the peer roster, delegate work to sibling daemons, and do direct computer use on peer displays. list_peers is gated as peer.inspect (same classification as GET /api/peers); every other tool here is gated as peer.use — acting through a peer delegates this daemon’s peer identity, and the receiving peer authorizes the request against its own grants for this daemon. A delegated task runs on the peer’s machine under the peer’s own autonomy/approval policy. The direct-CU trio is one stateless tools/call POST to the peer’s /mcp over the transport’s mTLS identity; the peer’s gate then requires display view for peer_list_displays / peer_take_screenshot (profile read-only-display or better) and display input for peer_execute_cu_actions (peer-operator / peer-root).

ToolDescriptionParams
list_peersPeer snapshot list — id, label, connection state, capabilities, sessions, displays (same payload as GET /api/peers).
peer_send_messageSend a message to a peer’s agent.peer_id, message, session?
peer_delegate_taskDelegate a task executed by the peer’s own agent; returns task_id.peer_id, instructions, context?
peer_list_displaysList a peer’s displays (ids, names, resolutions) over its /mcp.peer_id
peer_take_screenshotScreenshot a peer display; returns an MCP image content block.peer_id, display_target?
peer_execute_cu_actionsRun CU actions on a peer display; returns per-action status + the peer’s post-action observation (clean screenshot by default).peer_id, actions, display_target?, coordinate_space?, observe?, annotate?

Controller Orchestration

ToolDescriptionParams
schedule_controller_restartSchedule a controller restart / autonomous re-init workflow.controller_id, north_star_goal, reason?, restart_after?, restart_command?, auto_start_task?, max_attempts?, cooldown_sec?
controller_turn_completeFinal handshake; validates token and executes the scheduled restart.restart_id, turn_complete_token, status?, handoff_summary?
get_restart_statusCurrent restart state (or null).
cancel_controller_restartCancel a scheduled restart.restart_id?
request_controller_loop_haltRequest loop halt.persistent?
clear_controller_loop_haltClear loop-halt flags so restarts can resume.
intervene_controller_loopIntervene in the active loop process and visible Codex app-server descendants.mode: stop/abort
get_controller_loop_statusUnified loop-health snapshot.

schedule_controller_restart, controller_turn_complete, and cancel_controller_restart return JSON payloads with an ok boolean and status fields; rejections come back as JSON (ok: false) with an error message rather than plain text.

Resources

Resources provide push-based observation via subscriptions. The server emits notifications/resources/updated when state changes so clients re-fetch.

URIDescription
intendant://statusProvider, model, turn, budget %, phase, autonomy, session ID, task.
intendant://usagePer-model token usage (main + optional presence).
intendant://logsLast 100 chronological log entries (same as the dashboard’s activity log).
intendant://pending-approvalThe current pending approval, if any.
intendant://pending-inputThe current pending askHuman question, if any.
intendant://controller-restartCurrent controller-restart workflow state, if any.
intendant://controller-loopLoop-health snapshot (intervention flags, singleton lock owner, active wrapper/codex PIDs, latest run pointers).

Controller Restart Workflow

Use this when you want Intendant to trigger a controller re-init cycle safely (e.g. an external Codex/Claude/Kimi controller relaunching itself).

  1. Call schedule_controller_restart; capture restart_id + turn_complete_token.
  2. Before ending the controlling agent’s turn, call controller_turn_complete with both values.
  3. Intendant executes the restart actions:
    • spawn restart_command (if provided), and/or
    • start a fresh Intendant task from north_star_goal (auto_start_task=false by default; opt in only for E2E testing).
  4. Inspect via get_restart_status or intendant://controller-restart.

Notes & guarantees

  • Restart state persists to the session dir as controller_restart.json.
  • restart_after defaults to "turn_end"; only "turn_end" or "now" are accepted (others rejected). String inputs are trimmed before validation.
  • restart_command, when provided, must be non-empty/non-whitespace.
  • At least one restart action is required: restart_command and/or auto_start_task=true.
  • max_attempts must be >= 1 (0 rejected). Optional status, handoff_summary, and the cancel restart_id guard treat whitespace-only as unset.
  • If restart_after="now" and execution fails after validation, schedule_controller_restart reports "ok": false with execution_error, and the persisted phase becomes "failed" with last_error populated.
  • controller_turn_complete only accepts restarts in "awaiting_turn_complete"; duplicate/late handshakes (e.g. "phase": "ready") are rejected to prevent double execution.
  • get_restart_status and intendant://controller-restart redact turn_complete_token as "[redacted]"; only schedule_controller_restart returns the raw token (for the final handshake).
  • request_controller_loop_halt, clear_controller_loop_halt, intervene_controller_loop, and get_controller_loop_status return/emit normalized loop-health data (flags, lock owner PID + liveness, latest run pointers, active PID counts). The control socket’s command_result.data mirrors the same structured payloads.

Controller recursion profile

Recommended for Codex/Claude/Kimi-style controllers:

  • Set auto_start_task=false (or omit it — false is the default).
  • Use restart_command to relaunch the external controller process.
  • Treat start_task as optional E2E testing, not the default recursion path.

Controller Loop Monitoring

For restart_command wrapper scripts, loop artifacts live under .intendant/controller-loop/:

  • Stable pointers: latest (symlink), latest.pid, latest.status.json, latest.jsonl, and the singleton active.lock/ (pid, run_id, acquired_at).
  • Inspect: tail -f .intendant/controller-loop/latest/codex.jsonl, cat .intendant/controller-loop/latest.status.json.
  • Intervention markers: touch .intendant/controller-loop/request_halt (persistent), request_halt_after_cycle (one-shot legacy), request_stop (graceful), request_abort (immediate). History: .intendant/controller-loop/latest/intervention.log.
  • Per-run PIDs: .intendant/controller-loop/<run_id>/wrapper.pid and codex.pid. The Codex wrapper applies stop/abort to the recorded Codex process and its visible descendants so nested app-server children are not orphaned.

Typical Agent Workflow

  1. get_status for the current phase and budget.
  2. Poll get_logs with since_id to stream new events (or subscribe to intendant://logs).
  3. On an approval, get_pending_approval gives the command preview → approve, deny, or skip.
  4. On an askHuman, get_pending_input gives the question → respond.
  5. quit when done.

MCP Client

Intendant can also be an MCP client, connecting to external MCP servers configured in intendant.toml so the agent can use their tools alongside Intendant’s native ones (mcp_client.rs).

Configuration

[[mcp_servers]]
name = "filesystem"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]

[[mcp_servers]]
name = "github"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]

[mcp_servers.env]
GITHUB_TOKEN = "ghp_..."

At startup, McpClientManager::connect_all() spawns each server, discovers its tools, and registers them as mcp__<server>_<tool> (e.g. a filesystem server’s read_filemcp__filesystem_read_file). Tool calls with the mcp__ prefix are routed to the right server. If a server fails to connect, it is skipped with a warning; other servers and native tools keep working.

Outbound calls pass the controller’s tool_call approval gate before dispatch. That category defaults to ask, so Medium autonomy prompts unless the owner explicitly configures tool_call = "auto".

Returned text is not inserted raw. Intendant preserves is_error, quotes and labels the content as untrusted external data, normalizes or exposes control and invisible formatting, and caps the complete rendered result at 64 KiB. Transport error text uses the same boundary; non-text and structured payloads are omitted from this text bridge. This reduces prompt-injection and context exhaustion risk but cannot make the server’s data or claims trustworthy.

Trust model — read this before adding a server

Each [[mcp_servers]] entry is launched as a child process with the user’s full privileges:

#![allow(unused)]
fn main() {
let mut cmd = Command::new(&config.command);
cmd.args(&config.args);
let transport = TokioChildProcess::new(cmd)?;   // mcp_client.rs
}

Intendant performs no checksum verification, no signature check, and no sandboxing of MCP server binaries. Adding an MCP server is equivalent to adding a line to your ~/.zshrc that runs a binary.

Mitigating defaults: mcp_servers = [] by default, and intendant.toml is git-ignored, so the repo ships no MCP servers. Treat copying an intendant.toml between machines like copying shell rc files — read it before you source it.

See Also