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:
- Peer daemons (mTLS peer identity) use their peer-profile principal.
- Supervised backends receive a bearer only in their cleared child
environment and send it in the
Authorizationheader. Their argv-visible MCP config contains the environment-variable name, never the value.INTENDANT_MCP_URLremains the private environment bootstrap forctl. The bearer is session-scoped — derived from the daemon’s per-process token and thesession_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. - Browser pages may only call
/mcpfrom 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/*. - Tokenless loopback processes bind to
principal:local-process:loopback. Tokenless non-loopback requests are refused. Once anyagent_sessionbinding exists — even one whose grant has since expired or been revoked — this path fails closed (401) until an explicitlocal_processgrant 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 lapsedlocal_processgrant 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)
| Tool | Description | Params |
|---|---|---|
get_status | Provider, model, turn, budget %, phase, autonomy, verbosity, tokens. | — |
whoami | The 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_logs | Log 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_approval | The current pending approval request (or null). | — |
get_pending_input | The current pending askHuman question (or null). | — |
Interactive actions
| Tool | Description | Params |
|---|---|---|
approve | Approve a pending command. Owner surfaces only; HTTP/ctl routes the exact prompt id and owning session through the daemon control plane. | id |
deny | Deny and stop. Owner surfaces only; HTTP/ctl routes through the daemon control plane. | id |
skip | Decline this command and let the agent continue. Owner surfaces only; HTTP/ctl routes through the daemon control plane. | id |
approve_all | Approve and set autonomy to Full. Owner surfaces only (daemon-global autonomy escalation). | id |
respond | Answer an askHuman question. | text |
post_session_note | Post 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_user | Ask 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_user | Fire-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_autonomy | Set autonomy. Denied to agent-session callers — the setting is daemon-global. | level: low/medium/high/full |
set_verbosity | Set log verbosity. | level: quiet/normal/verbose/debug |
start_task | Start 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? |
quit | Shut 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.
| Tool | Description | Params |
|---|---|---|
agenda_list | List oldest-first items plus Open / Done / Retired counts; optionally filter by status. | status? |
agenda_op | Apply one tagged operation: add, answer, patch, complete, reopen, retire, propose_effect, approve_effect, or revoke_effect. | operation-specific op shape |
memory_search | Bounded claim search (default 10, maximum 50); candidates are excluded unless explicitly requested. Responses report effective durability. | query?, limit?, include_candidates? |
memory_read | Read one claim by an id prefix of at least eight hex characters. | id |
memory_propose | Propose 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
| Tool | Description | Params |
|---|---|---|
list_displays | Enumerate displays with their session state. | — |
take_display | Optional dashboard signal that an agent is using a display; it neither grants input authority nor is required before screenshot/CU calls. | display_id |
release_display | Release control of a display. | display_id, note? |
grant_user_display | Grant 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_display | Ask 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_display | Revoke access to the user’s real display session. | display_id?, note? |
take_screenshot | Capture a screenshot (returns image content). | display params |
read_screen | User session’s frontmost-app accessibility tree — macOS AX, Linux AT-SPI, or Windows UIA. | display_target?, format?, full_values? |
display_readiness | Probe display authority, capture/accessibility permission, target availability, and input backend live; names each missing layer. | display_target? |
execute_cu_actions | Run a batch of computer-use actions. | CU action params |
list_frames | List captured video frames. | filter params |
read_frame | Read 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.
| Tool | Description | Params |
|---|---|---|
show_shared_view | Open/foreground a shared display, optionally with an initial highlighted region. | display_target?, display_id?, reason?, focus_region? |
hide_shared_view | Dismiss the shared-view banner and focus overlay. | reason? |
focus_shared_view | Highlight a normalized region and optional note. | region, display_target?, display_id?, note? |
clear_shared_view_focus | Clear only the focus annotation; safe when none exists. | reason? |
request_shared_view_input | Ask the dashboard user to take input authority; never grants it. | display_target?, display_id?, reason? |
capture_shared_view_frame | Foreground 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.
| Tool | Description | Params |
|---|---|---|
list_rewind_anchors | Return bounded exact rewind anchors, with optional paging/search and density/recovery estimates. | session_id?, paging/filter/density flags |
inspect_rewind_anchor | Inspect a compact window around one exact anchor. | item_id, session_id?, radius? |
rewind_context | Schedule rollback to anchor.position (before/after) and inject a required carry-forward primer. | anchor, reason, primer, session_id?, preserve?, discard?, artifacts?, next_steps? |
rewind_backout | Inspect, restore, or fork/back out a prior rewind record. | record_id, session_id?, mode?, name? |
fission_spawn | Fork 1–4 full-context sibling branches; write-scoped branches use isolated worktrees by default. | branches, session_id?, use_worktree? |
fission_control | wait, import, cancel, or detach one fission branch/group. | group_id, op, session_id?, branch_session_id?, timeout_s? |
claim_fission_canonical | Claim 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.
| Tool | Description | Params |
|---|---|---|
browser_workspace_providers | Report available workspace providers. | — |
list_browser_workspaces | List active browser workspaces and leases. | — |
create_browser_workspace | Launch/register a workspace. | url?, label?, provider?, peer_id?, owner_session_id?, profile_dir? |
acquire_browser_workspace | Acquire a workspace lease. | workspace_id, holder_id, holder_kind?, note?, force? |
release_browser_workspace | Release a workspace lease. | workspace_id, holder_id?, note? |
close_browser_workspace | Close a workspace and terminate its local browser process when owned here. | workspace_id, reason? |
Live audio
| Tool | Description | Params |
|---|---|---|
spawn_live_audio | Spawn 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).
| Tool | Description | Params |
|---|---|---|
list_peers | Peer snapshot list — id, label, connection state, capabilities, sessions, displays (same payload as GET /api/peers). | — |
peer_send_message | Send a message to a peer’s agent. | peer_id, message, session? |
peer_delegate_task | Delegate a task executed by the peer’s own agent; returns task_id. | peer_id, instructions, context? |
peer_list_displays | List a peer’s displays (ids, names, resolutions) over its /mcp. | peer_id |
peer_take_screenshot | Screenshot a peer display; returns an MCP image content block. | peer_id, display_target? |
peer_execute_cu_actions | Run 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
| Tool | Description | Params |
|---|---|---|
schedule_controller_restart | Schedule 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_complete | Final handshake; validates token and executes the scheduled restart. | restart_id, turn_complete_token, status?, handoff_summary? |
get_restart_status | Current restart state (or null). | — |
cancel_controller_restart | Cancel a scheduled restart. | restart_id? |
request_controller_loop_halt | Request loop halt. | persistent? |
clear_controller_loop_halt | Clear loop-halt flags so restarts can resume. | — |
intervene_controller_loop | Intervene in the active loop process and visible Codex app-server descendants. | mode: stop/abort |
get_controller_loop_status | Unified 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.
| URI | Description |
|---|---|
intendant://status | Provider, model, turn, budget %, phase, autonomy, session ID, task. |
intendant://usage | Per-model token usage (main + optional presence). |
intendant://logs | Last 100 chronological log entries (same as the dashboard’s activity log). |
intendant://pending-approval | The current pending approval, if any. |
intendant://pending-input | The current pending askHuman question, if any. |
intendant://controller-restart | Current controller-restart workflow state, if any. |
intendant://controller-loop | Loop-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).
- Call
schedule_controller_restart; capturerestart_id+turn_complete_token. - Before ending the controlling agent’s turn, call
controller_turn_completewith both values. - Intendant executes the restart actions:
- spawn
restart_command(if provided), and/or - start a fresh Intendant task from
north_star_goal(auto_start_task=falseby default; opt in only for E2E testing).
- spawn
- Inspect via
get_restart_statusorintendant://controller-restart.
Notes & guarantees
- Restart state persists to the session dir as
controller_restart.json. restart_afterdefaults 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_commandand/orauto_start_task=true. max_attemptsmust be>= 1(0rejected). Optionalstatus,handoff_summary, and the cancelrestart_idguard treat whitespace-only as unset.- If
restart_after="now"and execution fails after validation,schedule_controller_restartreports"ok": falsewithexecution_error, and the persisted phase becomes"failed"withlast_errorpopulated. controller_turn_completeonly accepts restarts in"awaiting_turn_complete"; duplicate/late handshakes (e.g."phase": "ready") are rejected to prevent double execution.get_restart_statusandintendant://controller-restartredactturn_complete_tokenas"[redacted]"; onlyschedule_controller_restartreturns the raw token (for the final handshake).request_controller_loop_halt,clear_controller_loop_halt,intervene_controller_loop, andget_controller_loop_statusreturn/emit normalized loop-health data (flags, lock owner PID + liveness, latest run pointers, active PID counts). The control socket’scommand_result.datamirrors the same structured payloads.
Controller recursion profile
Recommended for Codex/Claude/Kimi-style controllers:
- Set
auto_start_task=false(or omit it —falseis the default). - Use
restart_commandto relaunch the external controller process. - Treat
start_taskas 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 singletonactive.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.pidandcodex.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
get_statusfor the current phase and budget.- Poll
get_logswithsince_idto stream new events (or subscribe tointendant://logs). - On an approval,
get_pending_approvalgives the command preview →approve,deny, orskip. - On an
askHuman,get_pending_inputgives the question →respond. quitwhen 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_file → mcp__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
- Autonomy & Approvals — the autonomy model that gates approvals.
- Integrations —
ControlMsg, the control socket, and the web gateway WebSocket protocol.