MCP agent control
An agent running in a pane that tongs-agents launched can start, prompt and
watch other agents through four MCP tools on the tongs-agents server. They
are off by default and exist only when you opt in. The read-only fleet_*
tools are always available and unchanged.
| Tool | Arguments | Does |
|---|---|---|
agents_spawn |
profile |
Starts a named profile in a new pane of the running workspace session and returns its launch id |
agents_send |
launch_id, text |
Pastes the text and presses Enter, like tongs-agents send |
agents_status |
launch_id |
The pane’s state, like tongs-agents agent-status |
agents_wait |
launch_id, until (default done), timeout_s (at most 600) |
Waits like tongs-agents wait |
No tool takes a command, arguments, environment or working directory: an agent can only start a profile you configured, as configured.
That limits what a caller can start, not what it can do once a pane runs.
agents_send types the text into the target as if you typed it, so it has
your full authority there: shell escapes such as ! in Claude Code and Pi,
slash commands such as /permissions, and anything else the harness accepts
from its user. The target’s permission mode and approval settings do not
stop it, and neither session asks you first. A caller that drives a
worker profile therefore holds every permission that profile could be
given, whatever mode the caller itself runs in. Set mcp_control = true only
on profiles whose full access you would grant the calling agent anyway.
Turning it on
Section titled “Turning it on”Control needs both the server switch and a per-profile opt-in:
[control]mcp_enabled = true # register the agents_* tools (default false)mcp_any_target = false # also act on opted-in panes another caller spawned (default false)mcp_nested_spawn = false # let an agent started through MCP start more agents (default false)mcp_rate_per_minute = 6 # sends, and separately spawns, per caller per minute (1 to 60)
[profiles.lead]command = ["claude"]mcp_control = true # may use the tools
[profiles.worker]command = ["codex"]mcp_control = true # may be spawned and drivenThe server reads mcp_enabled when it starts: with it off (or with an invalid
config file) the agents_* tools are not listed at all. Every call reads the
file again, so turning it off refuses calls from servers already running.
Restart the agent (or its MCP server) after turning it on.
Who may do what
Section titled “Who may do what”The caller is the agent whose MCP server makes the call, identified by the
server’s TONGS_AGENTS_LAUNCH_ID. Each call is refused (exit code 5 in the
reply) unless:
- That launch passes the same verification as
tongs-agents send(claimed, not replaced, its pane listed and its process alive with the same start time), and the server process runs under that launch’s process. A launch id copied into another process names nobody. - The caller’s profile, the profile to spawn and the target launch’s profile
are named profiles with
mcp_control = true. - The target is not the caller itself: not its launch, and not its pane (the same multiplexer, session, pane id and socket as the caller’s verified launch record; the server’s own environment plays no part).
- The target was spawned by this caller through
agents_spawn(its launch record names the caller inspawned_by). Withmcp_any_target = trueany opted-in pane qualifies. Panes started bytongs-agents workspaceor the sidebar have no spawning caller. - The caller has not used up
mcp_rate_per_minutesends (or spawns) in the last 60 seconds. The window is kept in$XDG_RUNTIME_DIR/tongs-agents/mcp-rate/(or the state dir’srun/mcp-rate/), so restarting the server does not reset it. A call reserves its slot under a short per-caller lock and runs without holding it, so parallel calls of one caller do not wait for each other. Only a call that succeeded counts: one refused by these checks or by the target’s verification, or one that failed in the multiplexer, gives its slot back. - For
agents_spawn, the session is running: the multiplexer, session and socket of the caller’s own launch, whatever[workspace]says. A spawn never creates a session.
The target pane then goes through the full verification of
driving agent panes. agents_spawn waits up to
5 seconds for the new pane to claim its launch and reports state as
running or still pending; status and wait refuse a pending launch until
its pane claims it.
agents_wait also ends early, with an error reply, when the call is
cancelled, which is what happens when the caller exits and its MCP server
shuts down (outcome cancelled), or when the caller stops passing check 1
(outcome refused). Cancellation is checked every poll, about every half
second, so a pending wait never keeps the server running after its agent is
gone. The caller is re-verified about every 5 seconds and the wait ends only
after two failed verifications in a row, so one transient multiplexer error
does not end it.
When the Zellij
home pane is gone and the new pane covers
part of the sidebar, the reply also has notices, a list of messages saying
so.
Replies
Section titled “Replies”Every tool returns JSON. A successful call has "ok": true; a refused or
failed one has "ok": false with outcome (refused, rate-limited,
error, exited, unsupported, cancelled), the exit_code of the matching CLI verb
and an error message.
Every call, including each refusal made by these checks, appends one line to
the control audit log described in driving agent panes
(audit/control.jsonl in the state dir, 0600). The actor is
mcp:<caller launch id> (mcp when the server has no launch id), with the
caller’s and the target’s launch ids, the action (spawn, send, status,
wait), the byte count and SHA-256 of sent text, the outcome and the exit
code. Text is never logged.
Validation
Section titled “Validation”validation/mcp_stdio.py starts the server over stdio twice in a temporary
state and config dir: with control off it lists only the five fleet_*
tools; with control on it lists all nine and refuses (and audits) an
agents_send from outside a launched pane. It starts no agent and no
multiplexer.
python validation/mcp_stdio.py