MCP tools¶
Every agent in the mesh exposes the same set of MCP tools through the repowire server. Tool calls go to the local daemon over HTTP; the agent never sees daemon internals. Names and signatures are stable and used identically across Claude Code, Codex, Gemini CLI, and OpenCode.
Transports¶
The default, stable transport is the per-agent stdio MCP server installed by repowire setup.
An experimental Streamable HTTP MCP endpoint can also be mounted on the local daemon at /mcp with:
Or by editing ~/.repowire/config.yaml:
repowire setup --http-mcp generates daemon.auth_token if one is not already set. This endpoint is opt-in, localhost-only, requires Authorization: Bearer <daemon.auth_token> by default, and is not exposed through the hosted relay. HTTP MCP uses a daemon-owned mcp-http identity instead of tmux/cwd session inference, so it is useful for local MCP clients that cannot run the stdio server in an agent session.
Client registration examples:
{
"mcpServers": {
"repowire": {
"type": "http",
"url": "http://127.0.0.1:8377/mcp",
"headers": {
"Authorization": "Bearer rw_local_..."
}
}
}
}
For Claude Code, the equivalent CLI shape is:
claude mcp add --transport http repowire http://127.0.0.1:8377/mcp \
--header "Authorization: Bearer rw_local_..."
Lifecycle/admin tools such as spawn, kill, and schedule mutation are disabled for HTTP MCP unless explicitly enabled with daemon.mcp_http.allow_dangerous_tools. The stdio server installed by repowire setup remains the stable default and still runs as repowire mcp.
Routing¶
ask¶
ask(peer_name: str, query: str, reply_to: str | None = None, circle: str | None = None, attachments: list[dict] | None = None) -> str
Open a non-blocking ask thread. In normal use, you tell your local agent what you need in natural language, and the agent invokes this MCP tool. Returns a correlation_id immediately. The recipient closes the thread with ack; the daemon routes the close back as a notification framed [ack #cid from @peer].
Use ask for worker checkpoints, review requests, pre-commit handoffs, status checks you intend to track, and delegated work where closure matters. Use a durable job instead when the work needs lifecycle and result state.
Daemon events for asks and acks include nullable repowire_session_id, from_repowire_session_id, and to_repowire_session_id fields when an existing session binding can be resolved. Peer IDs remain the routing authority.
Live delivery is attempted first. If a CLI-fallback/polling peer has no live transport, the ask stays open and a one-shot queued delivery is stored in SQLite for its next Stop-hook or CLI drain. The queued delivery is deleted after drain; the ask itself still appears in /asks/pending until ack.
Peer resolution defaults to the caller's circle. Peers whose role bypasses circles (orchestrator, service, human surfaces) resolve mesh-wide; everything else is scoped to the caller's circle so the daemon's ambiguous-resolve refusal applies. Pass circle="<name>" to target a different circle explicitly.
Pass reply_to to chain a follow-up: the prior thread closes and a new one opens referencing it. See misroute refusal for what happens when names collide within the resolution scope.
ack¶
Close an open ask. Bare ack(cid) signals "seen, no action needed." A reply ack(cid, message) closes the thread and delivers the message back to the original asker. Replies always reach the asker regardless of circle, because the thread was established at ask-time.
When the ask carries a structured question, ack delegates to the typed answer path: bare ack(cid) records an acknowledged answer, while ack(cid, message) records a text answer. Use answer directly when selecting an option.
answer¶
Answer a structured question carried on an ask. Pass option_id to select a choice, or text for a free-text answer. A bare answer(cid) records an acknowledged answer. Tool-permission questions also accept a denied outcome through the dashboard and Telegram renderers; ACP permission prompts deny by default on timeout.
This is the typed counterpart to ack for questions such as tool approvals and future AskUserQuestion-style prompts. Plain asks still use ack; /answer rejects a plain ask so the existing ack retry semantics are not bypassed. If the asking peer is offline after a structured answer is recorded, the readable reply is stashed and redelivered on reconnect.
answer("acpperm-8b9c1f42", option_id="allow")
answer("ask-c1a1c7dd", text="Use the staging database")
notify_peer¶
notify_peer(peer_name: str, message: str, circle: str | None = None, attachments: list[dict] | None = None) -> str
Fire-and-forget. No lifecycle, no expected response. Returns a synthetic notif-XXXXXXXX ID for client-side tracking, not a thread you can close. Use for FYIs, self-wakes, reminders, human phone updates, and nudges where no closure is expected.
Do not use notify_peer for worker checkpoints, review requests, pre-commit handoffs, or delegated work that needs explicit ack; use ask or a durable job instead.
On the HTTP /notify response, hook_delivery may be present when the
recipient is a new enough WebSocket hook. It is a best-effort terminal injection
receipt with statuses such as injected, rejected, or failed; null means
the hook is older, a non-hook transport handled the notify, or no receipt
arrived before the daemon returned. When a session binding is known, /notify
responses and hook receipts may include nullable repowire_session_id,
from_repowire_session_id, and to_repowire_session_id fields for grouping.
If the live transport is unavailable but the daemon can resolve the target peer, /notify may return delivery_state="queued" and reason="queued_delivery". That means the notification was stored in the SQLite queued-delivery table for the target peer/session and will be delivered once through the recipient's Stop hook or repowire peer deliveries, subject to the configured TTL and per-peer cap.
For an ACP-brokered peer (experimental), a fire-and-forget /notify returns delivery_state="delivered" with reason="broker_accepted" rather than transport_delivered. The broker accepted the prompt task, but the ACP reply is discarded for notify, so this is not a runtime receipt — the daemon never learns whether the runtime completed it. Clients that need a real receipt must not treat broker_accepted as one.
Peer resolution mirrors ask: defaults to the caller's circle, except for peers whose role bypasses circles (orchestrator, service, human surfaces) which resolve mesh-wide. Pass circle="<name>" to target a different circle.
The special peer telegram routes to the user's phone. The dashboard already sees agent turns; you do not need to notify it. Both are human-role peers and resolve mesh-wide regardless of your circle.
ask, reply ack, and notify_peer accept optional attachment metadata
objects (id, path, filename, size, content_type). Text-only calls are
unchanged; surfaces should still include a local path in text when targeting an
older transport that may ignore the structured field.
broadcast¶
Fan out to every online peer in your circle. No correlation, no reply. Use sparingly — treat it as a soft interrupt for everyone in scope.
ACP-brokered peers (experimental) are included in the fan-out: each receives the broadcast text as a fire-and-forget prompt through the broker (reply discarded), the same broker-accepted semantics as an ACP notify_peer. They appear in the response's sent_to on broker handoff, not on runtime completion.
ask_many / ask_many_result¶
ask_many(peer_names: list[str], query: str, circle: str | None = None, timeout_seconds: int = 300) -> str
ask_many_result(parent_id: str) -> str
Ask the same question to several peers in parallel under one parent (askm-...). Each recipient gets a normal child ask it closes with ack/ack(msg) — ask_many is a fan-out, not a vote: no quorum, no retry, no aggregation logic beyond collecting replies. Best-effort per peer (a recipient that fails to resolve or deliver is recorded as a failed child and does not abort the rest; the peer list is deduped and bounded).
ask_many returns the parent_id; poll ask_many_result(parent_id) for the current rollup — per-peer status (pending / acked / replied / failed), captured reply bodies, and a state of complete / partial / pending. Timeout is lazy: a parent past its timeout_seconds deadline with open children reports partial / timed_out at read time (no background timer). State is in-memory and does not survive a daemon restart.
parent = ask_many(["reviewer-a", "reviewer-b"], "ready to merge #42?")
# ... later ...
ask_many_result(parent) # shows who replied, who's still pending
Inspection¶
list_peers¶
Returns a TSV with columns: peer_id, name, project, circle, role, status, path, machine, description, backend, last_seen, turn_state.
turn_state is empty when unknown; otherwise idle, working, awaiting_input (peer is mid-turn waiting on user input), or pending_first_turn (spawn-seeded peer whose first prompt never landed — re-send via notify_peer).
By default returns online + busy peers in the caller's circle and hides the caller. Peers whose role bypasses circles (orchestrator, service, and human surfaces like telegram / dashboard / slack) are always visible regardless of the filter. Callers with role=orchestrator default to mesh-wide (circle="*").
Pass circle="*" to widen to the whole mesh, circle="<name>" to scope to a different circle, show_offline=True to include offline peers, or include_self=True to include the caller's own row.
whoami¶
Returns the caller's own TSV row. Useful when an agent needs to know which display name it is registered under (display names get suffixed on collision: repowire, repowire-2).
set_description¶
Update the free-form description visible in list_peers. Call this at the start of a task so peers can see what you are working on without asking.
claim_orchestrator_role¶
Self-repair tool for the orchestrator workspace. Use it after a daemon restart when list_peers(include_self=True) shows the orchestrator session as role=agent. It targets the caller's current peer id, persists role=orchestrator into the session mapping, and refuses non-orchestrator workspace/session names. It can repair stale, offline, or mapping-only holders; a fresh online/busy orchestrator holder is never demoted by this tool.
orchestrator_status¶
Check whether a live orchestrator is present in a circle. Returns a TSV row with columns: circle, present, peer_name, peer_id, last_seen, stale_after_seconds. Defaults to the caller's own circle.
"Live" means a peer with role=orchestrator, status online or busy, and a heartbeat within stale_after_seconds. Use this before dispatching long-running work that assumes an orchestrator will be available to coordinate.
This is a presence check, not a snapshot of mesh state.
Lifecycle¶
job_create¶
job_create(title: str = "", kind: str = "general", assigned_peer_id: str | None = None, owner_peer_id: str | None = None, repowire_session_id: str | None = None, correlation_id: str | None = None, circle: str | None = None, source_kind: str | None = None, source_id: str | None = None, scope: str | None = None, visibility: str = "circle", request: dict | None = None, deadline_at: str | None = None, expires_at: str | None = None, process_scope: str | None = None, continuity: str | None = None, provenance: dict | None = None) -> str
Create a durable tracked work job through the daemon /jobs API. Returns the daemon response as a JSON string with job_id, work_id, and status. The MCP caller's peer ID is sent as created_by_peer_id when available. process_scope="per_fire" requests a short-lived executor for each fire; continuity="resume" uses backend-native runtime resume between recurring fires, while continuity="fresh" starts without resume context.
job_list¶
job_list(state: str | None = None, owner_peer_id: str | None = None, created_by_peer_id: str | None = None, repowire_session_id: str | None = None, circle: str | None = None) -> str
List durable jobs through /jobs. Returns a JSON string shaped like {"work": [status, ...]}. Filters mirror the HTTP API.
job_status / job_show¶
Return one job's current status JSON. job_show is an alias for job_status.
job_update¶
job_update(job_id: str, state: str, state_reason: str | None = None, phase: str | None = None, progress: dict | None = None, progress_note: str | None = None, result_summary: str | None = None, result_data: dict | None = None, error: dict | None = None, artifacts: list | None = None, provenance: dict | None = None, attempt_id: str | None = None) -> str
Update a job lifecycle state through PATCH /jobs/{job_id}. Returns the updated status JSON. Terminal jobs cannot move back to non-terminal states; same-terminal updates may add bounded metadata. Runner-managed updates should include the current attempt_id from the job prompt/status. Workers should immediately mark state="running" with that attempt id before longer work, then send the terminal update with the same attempt id.
job_result¶
Return terminal result JSON for a job, or result_state="not_ready" with the current status while the job is non-terminal.
job_cancel¶
Request cancellation for a tracked work job. Returns status JSON. Queued jobs move directly to cancelled; running, delivered, awaiting-input, or blocked jobs record cancel_requested and remain pending until an executor reports a terminal state. When the daemon already owns a live ACP session for the job's assigned peer, it attempts a bounded protocol session/cancel and reports the result in status.protocol_cancel. If there is no live session/execution link, protocol_cancel reports unavailable rather than claiming runtime cancellation.
spawn_peer¶
spawn_peer(path: str, backend: str, profile: str | None = None, circle: str | None = None, message: str | None = None) -> str
Spawn a new agent session in a project directory. backend must have a launch profile in daemon.spawn.commands in ~/.repowire/config.yaml; spawn is off by default until you configure at least one backend and one allowed path. Pass profile to append args from daemon.spawn.profiles.<backend>.<profile> for model/profile selection. circle maps to the tmux session name and cannot be reassigned after spawn. If circle is omitted, the MCP tool uses the caller's current circle; pass circle="default" explicitly to target the default tmux session. command remains accepted as a deprecated compatibility selector for one release and bypasses profile resolution.
The spawned agent self-registers via its SessionStart hook within a few seconds. The message seeds first-turn context. Codex requires it (or a default) to fire its hook; other backends treat it as an opening prompt.
kill_peer¶
Terminate a peer by name or peer_id. The peer is always deregistered from the mesh. The tmux pane is killed only if the daemon can prove Repowire spawned it, either from current in-memory ownership or durable spawn ownership plus live tmux evidence. Externally attached peers, stale pane records, and mismatched live pane evidence are deregistered without touching tmux. Verify with tmux list-panes and follow up with tmux kill-pane if the pane survives.
Review queue¶
mark_reviewed¶
Record that you've reviewed a GitHub PR. After this call, the PR stops surfacing in your review_queue at the recorded SHA. If last_reviewed_sha is omitted, the daemon best-effort fetches the current HEAD via gh api; future pushes to the PR will then surface as re-review-suggested.
review_queue¶
List PRs awaiting your review (or another peer's). Defaults to the calling peer. Returns TSV with columns: pr_url, last_reviewed_sha, current_head_sha, state, my_action.
my_action values:
none-needed— PR open, head SHA matches what you reviewed.re-review-suggested— PR open, new commits since your review.merged-since-review— PR merged after you last reviewed it.closed-since-review— PR closed (not merged) since your review.unknown— GitHub API unreachable; falls back to cached state.
Scheduling¶
schedule_create¶
schedule_create(to_peer: str, text: str, fire_at: str, kind: str = "notify", circle: str | None = None) -> str
Schedule a one-shot future message to a peer. Use schedule_cron for recurring schedules, or schedule_self when the recipient is the calling peer.
At fire_at, the daemon delivers text to to_peer on your behalf. kind="notify" is fire-and-forget; kind="ask" opens an ask thread (the recipient must ack). Use kind="ask" for scheduled checkpoints, reviews, and handoffs that must be closed. fire_at is ISO-8601; naive datetimes are interpreted as UTC.
Use for self-wake reminders, post-stand-up nudges, or future check-ins that don't need a live caller waiting. Returns a sched-XXXXXXXX ID; pass it to schedule_delete to cancel.
schedule_self¶
schedule_self(text: str, fire_at: str | None = None, cron: str | None = None, kind: str = "notify", circle: str | None = None) -> str
Schedule a future message to yourself. Provide exactly one of fire_at or cron.
For one-shot reminders, pass an ISO-8601 fire_at. For recurring reminders, pass a five-field cron expression or an alias such as @hourly, @daily, @midnight, @weekly, or @monthly. kind="notify" delivers a reminder; kind="ask" opens an ask thread that must be acked. Use kind="ask" when the scheduled delivery gates progress.
schedule_cron¶
schedule_cron(to_peer: str, text: str, cron: str, kind: str = "notify", circle: str | None = None) -> str
Schedule a recurring message to a peer. cron accepts standard five-field cron syntax, including ranges, steps, and comma-separated values, plus aliases such as @hourly, @daily, @midnight, @weekly, and @monthly.
Recurring schedules advance to their next matching fire time after delivery. Cancel them with schedule_delete.
schedule_list¶
List pending scheduled check-ins. Returns TSV with columns: schedule_id, from_peer, to_peer, kind, fire_at, text. Sorted by fire_at ascending. Pass mine_only=False to see all schedules on the daemon. Pass include_cron=True to append a trailing cron column for recurring schedules.
schedule_delete¶
Cancel a pending scheduled check-in by the ID schedule_create returned.
See also¶
- The typed Python client exposes the same routing calls over the daemon's HTTP API for non-MCP callers.
- Message types covers the semantics of
ask,ack,notify_peer, andbroadcastat a higher level. - The orchestrator pattern shows where
orchestrator_status,review_queue, and the scheduling tools fit together.