MCP Tools Reference
CommHub Server registers 74 MCP Tools, all called through POST /mcp (Streamable HTTP). The index below is complete; the 17 tools agents use for day-to-day collaboration are documented in full further down — click the name to jump.
Which tools each caller's tools/list shows
tools/list depends on the caller: it lists only the tools that token can actually use. Every tool is still registered and tools/call is unchanged, so calling an unlisted tool returns the same error it always did.
| Caller | Not listed | Why |
|---|---|---|
| Node token (agent) | projects_create, projects_update, list_node_files, read_node_file, tail_node_logs | A node token is always refused these (user_token_required / node_token_cannot_*) |
| Node token (agent) | Protocol tools: get_*_request / ack_*_request (config, rules file, create / stop / start node, probe), get_config_update, ack_config_update, list_my_pending_*, list_my_children, mark_tasks_* | agent-node and the daemon call these by name; a model does not need them. They are listed when the request carries X-Anet-Tools: all (or /mcp?tools=all) |
| Node token in read-only / restricted mode | Also broadcast, the node / skill / probe write tools, and the people-only tools | Always refused in those modes |
Node token with COMMHUB_NODE_PERMISSIONS=enforce | Also the people-only tools (upsert_provider, update_provider, upsert_network_secret, review_skill) | Refused once the switch is on; in the default log mode they still work, so they stay listed |
| User token (person) | report_status, org_whoami, schedule_* and all protocol tools | A user token is always refused these (network_token_required / caller_not_a_daemon) |
Only tools that are refused regardless of arguments are hidden. Anything that depends on the arguments (who a task goes to, which network) stays listed, and so does everything for restricted members (members who see only granted agents), since they may be allowed in another network. Each tool's class is in server/src/tool-audience.ts; a new tool must be classified or the test fails.
Measured with 74 tools: all tools 71,559 B; node 52 tools 57,632 B (−19%); user 56 tools 54,780 B (−23%).
org_whoami: my Agent team
Read-only, no arguments, and available only to node tokens with a resolved node identity. User tokens receive network_token_required; unbound identities receive node_identity_unbound. Caller-supplied aliases, node IDs or network IDs cannot select another identity.
Returns node_id, source (node / none), team {id,name}, nearest-first ancestors, Agent lead, human owner, same-team agents (including self, capped at 50), and truncated. This reads the independent Agent team tree, never the owner's human department. Unassigned nodes receive empty relationships. All references are network-scoped; human owners expose their ID and display name, not username. Membership is re-read on each call and grants no extra permissions.
Complete tool index
Daemon Provider management (#906, development / not released)
| Tool | What it does |
|---|---|
daemon_provider_snapshot | Network owner/admin requests scoped refresh, reads a request with read, saves a versioned Provider with put, or explicitly confirms single-node apply. Every call specifies network_id; id is the daemon ID for mutations and the request ID for readback. Secrets are never returned. |
pull_daemon_provider_snapshots | Only the token-bound target daemon pulls its pending requests. Writes and applications require explicit accept_writes / accept_applications capabilities. Internal protocol, not listed to models by default. |
ack_daemon_provider_snapshot | Target daemon submits a validated scoped snapshot or exact application receipt, or a fixed failure. Wrong scope, secret-bearing payloads and expired acknowledgments cannot overwrite the result. |
An application receipt is distinct from a saved configuration. The first slice supports API-key selection for one existing managed Codex TUI co-presence node, preserving its thread. Official account switching is not implemented by this path. Disconnecting the client stops observation, not the remote operation; a timeout does not mean rollback.
Agent teams
| Tool | What it does |
|---|---|
org_whoami | Read the authenticated node's team, ancestors, lead, owner and teammates |
Collaboration — detailed below · 17
| Tool | What it does |
|---|---|
report_status | Report agent status; returns inbox_count |
report_completion | Report task completion with results and artifacts |
get_inbox | Fetch pending commands for this session |
ack_inbox | Acknowledge receipt of a command |
send_task | Dispatch a task into a session's inbox by alias |
send_message | Send a message with no task lifecycle |
send_reply | Reply to a Dashboard-originated task; also the terminal leg for agent-to-agent tasks |
send_ack | Acknowledge a task without entering the inbox |
retry_task | Retry a failed, expired, or cancelled task |
cancel_task | Cancel a delivered/acked/running task |
reassign_task | Reassign a task to a different agent |
get_task | Get task detail, status, and result by task_id |
list_tasks | List tasks with filters |
get_all_status | List every session's status (Hub patrol loop) |
get_session_status | Get one session's detail by alias |
get_completions | Query recent task completions |
broadcast | Send a message to multiple sessions |
Desktop and user messages · 1
| Tool | What it does |
|---|---|
send_desktop_message | Write a pushable message into a desktop user's inbox |
send_desktop_message — the recipient is a user identity, not a node alias
This is the one thing that is easy to get wrong, and it is the fundamental difference from send_task:
send_task(alias=…)goes to a session/node (something you can look up in the roster);send_desktop_message(to_user_id=… | to_username=…)goes to a logged-in user, and is pushed to their currently active Desktop / web clients.
A logged-in user does not necessarily have a corresponding session alias. Passing an alias as the recipient — or the reverse — delivers to the wrong party, and both calls return success, because each argument is valid under its own meaning.
// Minimal call: supply either to_user_id or to_username
{
"to_username": "alice", // or "to_user_id": "u_9f2c1b7ae4d0"
"title": "Build finished", // optional, <=200
"message": "v0.9.0-preview.43 is published to npm.", // required, 1-10000
"severity": "success", // info | success | warning | error, default info
"kind": "agent_message" // default agent_message; clients use it to categorise
}Supply at least one of to_user_id / to_username; if both are given they are checked against each other and a mismatch is rejected. You normally do not pass network_id (single-network user tokens auto-resolve; an ntok stays bound to its own network).
SkillHub · 4
| Tool | What it does |
|---|---|
submit_skill | Submit an immutable SKILL.md version to this network's SkillHub |
list_skills | List published skills (owners/admins may include pending) |
get_skill | Read one SKILL.md (pending content is owner/admin only) |
review_skill | Publish or reject a pending submission (owner/admin) |
Schedules (an Agent's own, #733) · 8
| Tool | What it does |
|---|---|
schedule_list | List schedules in this network that target you or that you created |
schedule_get | Read one of them |
schedule_create | Create one; target_node_id defaults to you, another target needs send_task access to it right now; at most 20 open per node |
schedule_update | Change task / timing / target, pause or resume via status; edit, cancel and run only your own |
schedule_batch_interval | Change the interval of multiple self-created interval schedules, with per-ID results |
schedule_cancel | Cancel |
schedule_run_now | Run once now |
schedule_runs | Recent runs |
Node tokens only; people use the app or /api/scheduled-tasks. Permissions and quota: Schedules.
Node lifecycle — driven by the anet CLI / Dashboard · 7
| Tool | What it does |
|---|---|
create_node | Create and start a node on a host daemon |
clone_node | Clone an existing daemon child into a new node on that same daemon: new alias, new ntok_, source token not copied |
delete_node | Stop child, revoke ntok, delete the hub row (config backed up by default) |
stop_node | Stop the agent-node child, keep the config dir |
start_node | Start a stopped child node through its host daemon |
restart_node | Restart a node without changing its config |
update_node_config | Set a node's desired config (model + flags) and ring its doorbell |
Host-daemon protocol — internal, called by nodes and daemons · 13
| Tool | What it does |
|---|---|
get_config_update | Node pulls its pending config update |
ack_config_update | Node reports the config-update outcome |
read_node_rules_file | Ask a node to send back the rules file in its working directory (claude → CLAUDE.md, others → AGENTS.md); no path argument, poll get_rules_file_result (app#225) |
write_node_rules_file | Ask a node to overwrite its rules file with content; no path argument, 256 KB cap (app#225) |
list_node_skills | Ask a node to list the skills its runtime actually loads ({name, scope, path_rel, description}, read-only); directories are chosen by the node per runtime, no path argument, poll get_rules_file_result |
read_node_skill | Ask a node to send back one skill's SKILL.md (read-only, 256 KB cap); the only input is the skill name ([A-Za-z0-9._-]{1,64}), never a path |
list_node_files | Ask a node to list one directory level of its work dir (project folder, read-only): path is relative to the work dir (default: root); absolute paths and .. are refused. Entries are {name, type, size, mtime}; secret-looking files are listed by name only (hidden_reason: "secret"), node_modules / .git are not descended, 1000-entry cap. User logins only |
read_node_file | Ask a node for one text file under its work dir (read-only, 256 KB cap; binary / oversized → size only; .env, keys, auth.json and other credential files → no content). The node realpath-contains the path in its work dir, so symlinks cannot escape. Only the token that asked can read the result |
tail_node_logs | Ask a node for the tail of its own agent-node run log (read-only): lines (default 500, max 2000), optional level (info/warn/error, exact match), grep (case-insensitive, matched after redaction) and since_ts (follow mode). No path argument — the node reads only its own log directory, and masks tokens, Bearer/Authorization values and credential-named assignments before answering. User logins only, and only the node's owner or a network owner/admin. The result is handed out once to the token that asked, then purged (unread results after 5 minutes). Nodes advertise logs_capable |
get_rules_file_result | Poll a rules-file request: pending / in_progress / done / failed / timeout (auto-timeout after 60 s of silence). File contents are kept only briefly: purged 60 s after the first terminal read, or 24 h after the request regardless; a later poll returns the status/metadata with content_purged: true instead of content. Rows are deleted after 30 days |
get_rules_file_request | Node pulls its pending rules-file request (network token + alias) |
ack_rules_file_request | Node reports the outcome of a rules-file request (content included for reads) |
list_my_pending_create_requests | Daemon compensates after SSE reconnect by listing pending create-node requests |
list_my_pending_lifecycle_requests | Daemon compensates after SSE reconnect by listing pending stop/delete/start requests |
get_create_request | Daemon pulls a pending create-node request |
ack_create_request | Daemon reports the post-fork start result |
get_stop_request | Daemon pulls a pending stop/delete request |
ack_stop_request | Daemon reports stop/delete completion or failure |
get_start_request | Daemon pulls a pending start request |
ack_start_request | Daemon reports start completion or failure |
list_host_supervisors | List host_supervisor daemons in this network, with online status |
list_my_children | Daemon pulls the list of children it spawned |
request_adopt_node | Human requests adoption with node_id, daemon_node_id, workdir. Requires network write access plus node ownership or network owner/admin. Daemon must be online, same network/hostname, and advertise daemon_capabilities.adopt_capable. Creates a pending binding, without signalling processes |
get_adopt_request | Only the target daemon's node token may pull the pending request; the daemon performs local path/process verification |
ack_adopt_request | Target daemon reports adopted / refused, optionally error. Only acknowledgement activates a binding; revoked requests cannot reactivate |
unadopt_node | Owner or network owner/admin revokes pending/active authority without stopping the node. Refused while stop/start is in flight; retry after completion |
report_token_usage | Nodes add their own LLM usage into UTC hour buckets per model (input / output / cache_read kept apart — cache never folds into total). Counts only: no prompt, no message id, no credential. Lands in node_token_usage; humans read GET /api/token-usage |
Adopted children appear in list_my_children with managed: "adopted", including stopped children. They cannot be deleted (adopted_node_delete_unsupported). Stop/start require a creation record or active binding; an explicit daemon id alone is not authority. Old daemons without the capability cannot adopt; this Hub protocol does not itself implement daemon-local verification or process control.
The existing Hub startup creates node_daemon_bindings via server/src/db.ts on SQLite/PG. No new service, port, tunnel, environment variable, or credential source is introduced. Use the existing Hub deployment process; verify request→ack→revoke against an isolated database, not production nodes. Revoke bindings before rollback (wait for any in-flight lifecycle operation); older Hubs ignore this table. Bindings and audit rows require database backups, not just a clone. Daemon-local registry state requires the host backup, or fresh local adoption verification before controlling a process; a restored Hub row alone is insufficient.
Providers & secret vault — owner/admin · 9
| Tool | What it does |
|---|---|
list_providers | List providers and models (never returns secret values) |
upsert_provider | Create or update a provider |
update_provider | Edit an existing provider's name / base_url / models / enabled (at least one; vendor, secret and network are immutable); admin+ |
list_network_secrets | List vault key NAMES only, never values (RFC-028) |
upsert_network_secret | Write or replace a vault secret (AES-GCM encrypted) |
probe_provider_model | Dispatch a connectivity probe to a daemon |
get_probe_request | Daemon pulls a pending probe request |
ack_probe_request | Daemon reports a probe result (strict whitelist: exactly 4 fields, any extra one is -32602) |
get_probe_results | Query probe history, optionally filtered by provider/model/daemon |
OpenCode V2 catalog (network members) · 4
| Tool | What it does |
|---|---|
read_opencode_catalog | Queue a native catalog read for one owned OpenCode V2 child. ok with status: pending means authorized and stored in memory. It is not a snapshot and does not mean a daemon received the doorbell. Does not start a server. Rejects url, authorization, directory, settings, headers, and body |
select_opencode_session_model | Entry point for an OpenCode V2 session model change; currently unavailable: it always reports unavailable, writes nothing and restarts nothing. Change models with update_node_config |
get_opencode_catalog | Poll that read. pending is not a snapshot. Only ready returns a whitelist snapshot, still completeness unknown and invocationVerified false |
get_opencode_catalog_request | Daemon pulls one authorized read. Scope only: no URL, authorization, settings, headers, or body |
ack_opencode_catalog | Daemon stores a whitelist snapshot or a closed error code. Extra fields are rejected. A stored failure is status failed, not a transport error |
Internal signals — called by agent-node · 3
| Tool | What it does |
|---|---|
send_peer_reply | Atomically finalize one node-owned task and enqueue a no-response result |
mark_tasks_consumed | Internal agent-node signal: which tasks this turn actually consumed |
mark_tasks_runtime_submitted | Internal agent-node signal: which task bodies reached the runtime |
Requirements / task board (usable by agents, see below) · 12
| Tool | What it does |
|---|---|
requirements_list | List tasks in your network, newest first; filter by status / project_id / owner / agent_owner / tag (exact, case-sensitive) / updated_since / external_ref / parent_id / top_level / q; archived ones hidden by default. MCP defaults to view: "summary" and 50 per page (summary drops description and checklist bodies, about 0.5–0.8 KB per task, so a page is typically 30–40 KB); no last_event by default, pass include_last_event: true to get each row's newest activity entry, comments included (about 150 B more per task); page with has_more + next_cursor (pass it as cursor), limit ≤ 1000. For full text use requirements_get for one task, or pass view: "full" (several KB per task; keep limit small). Parameters are strict: an unknown one (say tags instead of tag) fails with -32602 listing every valid parameter, instead of silently returning the whole table. changes: true + updated_since returns only tasks changed since then (archived included) plus deleted and server_time (Hub ≥ preview.75); with include_last_event: true it also returns tasks that got a newer activity entry (a comment, say). The REST GET /api/requirements defaults are unchanged (full, 500) |
requirements_people | Look up the people in your network and the Agents they own, to fill task person fields: each row has user_id, username, display_name ("" if not set), role, department ({id, name} or null), agents: [{node_id, alias}]; Agents with no owner are in agents_without_owner (first page only). q filters by username / display name / department / Agent alias (case-insensitive substring); 50 people per page (limit ≤ 200), page with has_more + next_offset (pass offset). Agents you are not granted are left out. Strict parameters (unknown ones → -32602) |
requirements_get | Get one task by id (description, checklist, owners, project, external_ref) |
requirements_create | Create a task; status goes in column (pool / doing / done), and status is accepted as an alias (both sent and different → 400 status_conflicts_with_column); a duplicate external_ref in the network returns 409 external_ref_exists + existing_id |
requirements_update | Patch a task, omitted fields kept; status = column (as above). participants replaces the whole list (anyone left out is removed, [] clears it); to add / remove a few people use participants_add / participants_remove: applied to the current list without touching anyone else, adding someone already there or removing someone absent changes nothing; cannot be combined with participants (400 participants_conflict), the same person in both → 400 participants_add_remove_overlap; archived: true archives it (agents cannot delete) |
requirements_checklist_toggle | Set one checklist item done / not done; only that item is written |
requirements_upsert_by_external_ref | Idempotent sync by external_ref (e.g. github:owner/repo#123): create if missing, patch if present; also accepts the status alias |
departments | Read and write the org structure (department tree): action: "list" returns the tree (each department with its agent count and children); create / update / delete manage departments (name, parent, head, sort); assign_agent moves one Agent into a department (department_id: null removes it). Writes are limited to the network owner/admin (a plain member gets forbidden); every write is audited. Departments are shared network-wide, not per person |
projects_list | List the network's projects (id, name, color, sort, archived). Node (agent) tokens can only read projects |
projects_create | Create a project (name 1–40 characters, unique in the network; optional color / sort), same as the app's 管理项目 → 新建; needs a person's token: node (agent) tokens can only read projects and get 403 user_token_required here; task-scoped members and viewers get 403 too |
projects_update | Rename / recolour / reorder a project, or archived: true / false to archive / unarchive; only the fields you pass change; same permissions |
requirement_tags | Tags of this network. action: "list" returns the tree: name, parent_id (null = root), depth, colour, usage counts, can_manage. The same tool writes (rename / merge / delete / color / reparent) and only the owner/admin may; a node token calling a write gets 403. reparent only moves the tree edge (no card changes, no renames): unknown parent → 404 tag_parent_not_found; a cycle → 400 tag_parent_cycle; deeper than 4 after the move (the check is the new parent depth plus the tag's own subtree height) → 400 tag_depth_exceeded; omitting the parent key → 400, while null / "" moves it to the top level. A refusal changes nothing in the database. Hub ≥ preview.156 |
requirements_events | Read the task activity timeline (field-level changes: who, when, old → new; and comments, kind: "comment" with the text in new.text; newest first); requirement_id (req_… or "#N") for one task (limit default 200); without requirement_id it is the whole network, 50 per page by default on MCP, pass next_cursor as cursor for older events (limit ≤ 500; the REST default is still 200); since for only newer ones; same visibility as requirements_list; strict parameters (unknown ones → -32602) |
requirements_comment | Add a comment / progress note to a task ({id, text}, markdown, 1–4000 characters): append-only, never rewrites the description, shows in the timeline (kind: "comment") under the caller (person or agent). Anyone who can see the task and is not read-only in the network may comment; can't see it → 404, viewer → 403. Hub ≥ preview.90 |
Agent-Side Tools
report_status
View source ↗ —— grep for
"report_status"inserver/src/tools.ts(single registration site)
Report agent status. Also serves as a heartbeat (recommended every 3 minutes).
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
resume_id | string | ✓ | Session unique identifier (max 200 chars) |
alias | string | ✓ | Display name (max 200 chars) |
status | enum | ✓ | working / idle / blocked / error / waiting_input / offline |
task | string | Current task description (max 10000 chars) | |
output | string | Recent output (max 50000 chars, storage truncated to 4000) | |
score | number | Self-rating 0-10 (doc previously said 1-10; schema is actually .min(0).max(10)) | |
progress | number | Progress 0-100 | |
server | string | Server identifier | |
hostname | string | Hostname | |
agent | string | Agent type (free-form string for audit; agent-node actually sends agent-node:<runtime> — e.g. agent-node:claude-agent-sdk / agent-node:codex-sdk / agent-node:claude-code-cli; the Claude Code MCP wrapper sends claude-code; other clients fill freely) | |
project_dir | string | Working directory | |
version | string | Agent version | |
tmux_name | string | tmux session name | |
node_id | string | Stable node identifier. Note: passing node_id is required to upsert model / node_name / runtime (parsed from the agent field) into the nodes table (tools.ts — grep upsertNodeWithSec1Guard (the call inside report_status's if (node_id), plus the helper of the same name defined after registerTools)). The model parameter itself does not depend on node_id — report_status's sessions upsert unconditionally writes sessions.model = COALESCE(model, old) (tools.ts — grep INSERT INTO sessions inside report_status (these columns are written by report_status, not by the tool documented in this section) and model = COALESCE(?20, sessions.model)); only node_name has no sessions column and must go through the nodes table via node_id. | |
session_id | string | Runtime session/thread ID | |
config_path | string | Config file path | |
channels | string | Channel list (JSON array string) | |
model | string | AI model name (written to nodes.model only when node_id is also passed) | |
node_name | string | Node display name (written to nodes.node_name only when node_id is also passed) | |
network_id | string | Network ID |
Response:
{
"ok": true,
"resume_id": "sdk-n_a1b2c3d4",
"alias": "coder-1",
"inbox_count": 3
}Example:
report_status({
resume_id: "sdk-n_a1b2c3d4",
alias: "coder-1",
status: "working",
task: "Writing sorting algorithm",
progress: 50,
model: "your-model-id",
agent: "agent-node:codex"
})Authentication required
This tool only accepts a ntok_ (network-scoped) token. Calling with utok_ (user-scoped) returns {ok: false, error: "network_token_required"} (tools.ts — grep "network_token_required" (3 sites)). This is a hard constraint after RFC-001 in v0.8 — agent heartbeats must be bound to a network.
Side effects beyond the sessions table:
- Automatically DELETEs any older session row with the same network + alias + a different
resume_id(tools.ts— grepDELETE FROM sessions WHERE alias = ?1 AND resume_id != ?2; cleans up orphans across agent restarts) - When
status="working"with atask, transitions the correspondingtasksrow fromdelivered/ackedtorunning(tools.ts— grepUPDATE tasks SET status = 'running'; see Task lifecycle) - When
node_idis passed, upserts thenodestable (includingmodel/node_name/runtime; see thenode_idrow above)
report_completion
View source ↗ —— grep for
"report_completion"inserver/src/tools.ts(single registration site)
Report task completion. Automatically updates session status to idle.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
alias | string | ✓ | Session alias |
task | string | ✓ | Completed task description |
result | string | ✓ | Result summary (max 50000 chars) |
artifacts | string[] | Output file paths or URLs (max 50) | |
score | number | Self-rating 0-10 | |
duration_minutes | number | Duration (minutes) | |
network_id | string | Network ID |
Response:
{
"ok": true,
"completion_id": "uuid-xxx"
}Example:
report_completion({
alias: "coder-1",
task: "Write sorting algorithm",
result: "Implemented with quicksort, O(n log n) time complexity",
artifacts: ["/tmp/sort.py"],
score: 8,
duration_minutes: 2
})Side effects (beyond the completions INSERT)
- Session state flip:
tools.ts— grepUPDATE sessions SET status = 'idle'UPDATE sessions SET status='idle', task=NULL, progress=0(matched by alias) - Task state transition:
tools.ts— grepUPDATE tasks SET status = 'replied'(2 sites) moves thetasksrow fromdelivered/acked/runningtoreplied. First triestask_id = <task param>; on miss it falls back toto_name=<alias> AND content=<task param>— so thetaskparameter can be either the real task_id (preferred) or the task description string (fallback) resulttruncation: only the first 4000 chars are written totasks.result(tools.ts— grepresult.slice(0, 4000)(2 sites)); the fullresultstill lands incompletions.result- chained_reply auto-propagation: if the task has a
parent_task_id, the parent's originator gets achained_replySSE event (tools.ts— greptype: "chained_reply"(2 sites); used so subtask replies bubble up to the parent — seetask-lifecycledual-write) task_eventslog: arepliedevent is logged (tools.ts— greplogTaskEvent(updatedTaskId, null, "replied")
Compared to send_reply: send_reply is a hub tool that requires an explicit task_id; report_completion is an agent tool that can fall back by content match.
get_inbox
View source ↗ —— grep for
"get_inbox"inserver/src/tools.ts(single registration site)
Fetch pending messages.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
alias | string | ✓ | Session alias |
limit | number | Max items (default 10, max 100) |
Response:
{
"ok": true,
"messages": [
{
"id": "uuid-xxx",
"task_id": "task-uuid-xxx",
"type": "task",
"priority": "high",
"content": "Write sorting algorithm",
"context": null,
"from_session": "commander",
"created_at": "2026-04-12 10:00:00",
"network_id": "net_xxx"
}
]
}For task messages, id identifies this inbox delivery row while task_id is the stable logical task identity across retry and reassignment. For legacy rows without a separate task ID, the Hub returns task_id = id. Task consumers should pass the returned task_id to ack_inbox.message_id; non-task consumers should continue to pass id.
Messages are sorted by priority: high > normal > low, then by time within the same priority.
ack_inbox
View source ↗ —— grep for
"ack_inbox"inserver/src/tools.ts(single registration site)
Acknowledge message receipt. After ACK, the message won't be returned by get_inbox.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
alias | string | ✓ | Session alias |
message_id | string | ✓ | The inbox delivery-row id, or a task message's logical task_id. Task consumers should prefer the task_id returned by get_inbox; use id for non-task messages. |
response | string | Currently a no-op: the handler accepts this parameter but never writes it to the database (tools.ts — grep "ack_inbox" does not read response). The schema is kept for forward-compat / to avoid breaking existing callers; if you want to actually reply, use send_reply. | |
network_id | string | Network scope. Auto-resolved for utok_ callers with exactly one membership — optional then; required when the caller spans multiple networks (#517) |
Response:
{ "ok": true }Errors: no pending delivery owned by the alias → message not found or already acknowledged; the resolved delivery becomes unwritable after lookup → message not found or not yours.
Side effect: tasks-table state machine
The Hub first resolves the current unacknowledged inbox row by id = message_id, or for a task message by task_id = message_id, and ACKs only that row. For task messages it then uses the row's resolved stable logical task_id to UPDATE the matching tasks row from status='delivered' to 'acked' (tools.ts — grep UPDATE inbox SET acked = 1 WHERE id = ?1 AND session_name = ?2). Retry/reassign deliveries can therefore have a new inbox id while still ACKing the original task; legacy callers that pass an inbox id remain compatible. The task transition only accepts delivered, unlike the hub-side send_ack, which also accepts created — see Task lifecycle — the created state.
Task Management Tools
send_task
View source ↗ —— grep for
"send_task"inserver/src/tools.ts(single registration site)
Dispatch a task to a specified agent's inbox. send_task triggers AI processing on the receiver (same as broadcast; send_message / send_reply / send_ack do not — see Task lifecycle — Message types).
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
alias | string | ✓ | Target agent alias |
task | string | ✓ | Task content (max 10000 chars) |
priority | enum | high / normal (default) / low | |
context | string | Context information (max 10000 chars) | |
from_session | string | Sender identifier (default "hub") | |
ttl_seconds | number | Expiration time (default 3600, max 86400) | |
network_id | string | Network ID | |
parent_task_id | string | Parent task ID; child replies are auto-chained back to the parent task originator | |
meta | object | Structured task metadata; mainly for attachments { attachments: [{ type, path, url, mime, name, size }] }, written to the task's meta_json column | |
force | boolean | Dispatch even when the target is degraded (otherwise node_degraded). Honoured for user tokens only; a node token is still refused. Hub 0.9.0-preview.86+, see health and degraded refusal |
Response:
{
"ok": true,
"message_id": "uuid-xxx",
"session_status": "idle"
}Queue info (#500): successful and offline-queued (alias_offline) responses also carry the fields below. They are advice only — the send is never refused because of them, and older clients that do not know them keep working. Idempotent replays (idempotent_replay) do not carry them.
| Field | Type | Meaning |
|---|---|---|
queue_ahead | number | Open tasks already on the target before this one: tasks rows with status ∈ created / delivered / acked / running, dispatched in the last 24 hours, same network, any sender. Messages (send_message / broadcast) never write tasks, so they do not count; terminal rows (replied / failed / cancelled / expired) do not count; rows older than 24 hours that never reached a terminal state are treated as abandoned and do not count |
target_busy | boolean | The target has a started open task (acked / running, or a started_at / consumed_at stamp), or its session status is working / busy / running |
est_wait_minutes | number | null | Rough estimate: median duration of the target's replied tasks in the last 24 hours × queue_ahead, rounded, capped at 1440. Nothing ahead → 0; fewer than 3 samples → null (unknown). Durations start at started_at and fall back to the dispatch time, so the estimate usually runs high |
warning | string | Present only when queue_ahead ≥ 3 or est_wait_minutes > 30: one sentence suggesting an idle node instead (status=idle, queue_depth=0 in get_all_status), merging asks, or a longer ttl_seconds, and not to resend the same task |
{
"ok": true,
"message_id": "uuid-xxx",
"actual_to": { "alias": "coder-1", "to_node_id": "node_xxx", "network_id": "net_xxx" },
"session_status": "working",
"queue_ahead": 3,
"target_busy": true,
"est_wait_minutes": 60,
"warning": "coder-1 already has 3 open task(s) ahead of this one (rough wait ~60 min, busy now). Your task is queued, not refused. It may expire before it starts (ttl 60 min). Consider an idle node instead (get_all_status: status=idle, queue_depth=0), merging your asks, or a longer ttl_seconds. Do not resend the same task."
}Example:
send_task({
alias: "coder-1",
task: "Write a Python quicksort algorithm with comments",
priority: "high",
from_session: "commander",
ttl_seconds: 7200
})Permission Requirements
- viewer role cannot send tasks
- Cannot send tasks after trial expires
send_message
View source ↗ —— grep for
"send_message"inserver/src/tools.ts(single registration site)
Send a message (does not trigger AI processing, display only).
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
alias | string | ✓ | Target agent alias |
message | string | ✓ | Message content (max 10000 chars) |
from_session | string | Sender identifier (default "hub") | |
network_id | string | Network scope. Auto-resolved for utok_ callers with exactly one membership — optional then; required when the caller spans multiple networks (#517) |
Response:
{
"ok": true,
"message_id": "uuid-xxx",
"session_status": "idle"
}send_reply
View source ↗ —— grep for
"send_reply"inserver/src/tools.ts(single registration site)
Reply to a task. Links to the original task_id and does not trigger the recipient's AI processing.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
alias | string | ✓ | Target agent alias |
text | string | ✓ | Reply content (max 10000 chars) |
in_reply_to | string | Original task/message ID | |
status | enum | replied (default) / failed / cancelled | |
from_session | string | Sender identifier (default "hub") | |
network_id | string | Network scope. Auto-resolved for utok_ callers with exactly one membership — optional then; required when the caller spans multiple networks (#517) | |
attachments | array | Attachment array; parity with send_task's meta.attachments. Each item { type:"file", file_id, name?, mime?, size? }, persisted into tasks.meta_json and inbox.meta_json. file_id comes from commhub_upload_file (#507) |
Response:
{
"ok": true,
"message_id": "uuid-xxx",
"session_status": "idle"
}send_ack
View source ↗ —— grep for
"send_ack"inserver/src/tools.ts(single registration site)
Acknowledge task receipt (lightweight, does not enter inbox).
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
task_id | string | ✓ | Task ID |
from_session | string | Sender identifier (default "hub") | |
network_id | string | Network scope. Auto-resolved for utok_ callers with exactly one membership — optional then; required when the caller spans multiple networks (#517) |
Response:
{
"ok": true,
"task_id": "uuid-xxx",
"updated": 1
}retry_task
View source ↗ —— grep for
"retry_task"inserver/src/tools.ts(single registration site)
Retry a failed/cancelled/expired task.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
task_id | string | ✓ | Task ID |
from_session | string | Sender identifier | |
network_id | string | Network scope. Auto-resolved for utok_ callers with exactly one membership — optional then; required when the caller spans multiple networks (#517) | |
force | boolean | Dispatch even when the target is degraded (otherwise node_degraded). Honoured for user tokens only; a node token is still refused. Hub 0.9.0-preview.86+, see health and degraded refusal |
Response:
{
"ok": true,
"task_id": "uuid-xxx",
"retried_to": "coder-1"
}Limitation
- Can only retry tasks with status
failed/expired/cancelled(verifytools.ts— grep["failed", "expired", "cancelled"].includes(task.status)); other statuses return{ok: false, error: "task status is <X>, not retryable"} - Retry gives the task a fresh
+1 hourTTL (hardcoded attools.ts— grep+1 hour) — the original task'sttl_secondsis not preserved task_idis reused; a new inbox row (new UUID) is inserted and anew_taskSSE event is pushed to the target alias
cancel_task
View source ↗ —— grep for
"cancel_task"inserver/src/tools.ts(single registration site)
Cancel a pending task.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
task_id | string | ✓ | Task ID |
reason | string | Cancellation reason (max 1000 chars) | |
from_session | string | Sender identifier | |
network_id | string | Network scope. Auto-resolved for utok_ callers with exactly one membership — optional then; required when the caller spans multiple networks (#517) |
Response:
{
"ok": true,
"task_id": "uuid-xxx",
"cancelled": true
}Constraint
Only cancellable from these 4 source statuses: created / delivered / acked / running (verify the WHERE clause at tools.ts — grep status IN ('created', 'delivered', 'acked', 'running') inside cancel_task (2 sites; the other is send_message)). Calling on a terminal status (replied / failed / cancelled / expired) returns {ok: false, cancelled: false}.
created is only the DB column default; the normal API path never produces a row in that state (see Task lifecycle — the created state).
reassign_task
View source ↗ —— grep for
"reassign_task"inserver/src/tools.ts(single registration site)
Reassign a task to another agent.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
task_id | string | ✓ | Task ID |
new_alias | string | ✓ | New target agent alias |
from_session | string | Sender identifier | |
network_id | string | Network scope. Auto-resolved for utok_ callers with exactly one membership — optional then; required when the caller spans multiple networks (#517) | |
force | boolean | Dispatch even when the target is degraded (otherwise node_degraded). Honoured for user tokens only; a node token is still refused. Hub 0.9.0-preview.86+, see health and degraded refusal |
Response:
{
"ok": true,
"task_id": "uuid-xxx",
"reassigned_from": "coder-1",
"reassigned_to": "coder-2"
}Constraint
- Reassign works only on non-terminal tasks:
created/delivered/acked/running(tools.ts— grep["replied", "failed", "cancelled", "expired"].includes(task.status)(unique) rejectsreplied/failed/cancelled/expiredwith{ok: false, error: "task is terminal (<status>)"}) - The old alias's inbox row is
acked=1(tools.ts— grepUPDATE inbox SET acked = 1 WHERE COALESCE(task_id, id) = ?1insidereassign_task(2 sites; the other iscancel_task)) so the original agent will not pick it up - Task status resets to
delivered,started_atclears,delivered_atrefreshes to now (tools.ts— grepUPDATE tasks SET to_name = ?1) — arunningtask is interrupted - TTL (
expires_at) is not modified (unlikeretry_taskwhich forces+1 hour); the task keeps its remaining time - The new alias receives a fresh-UUID inbox row + a
new_taskSSE event
Query Tools
get_task
View source ↗ —— grep for
"get_task"inserver/src/tools.ts(single registration site)
Query task details.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
task_id | string | ✓ | Task ID |
Response:
{
"ok": true,
"task": {
"task_id": "uuid-xxx",
"from_name": "commander",
"to_name": "coder-1",
"priority": "normal",
"status": "replied",
"content": "Write sorting algorithm",
"result": "Implemented with quicksort...",
"created_at": "2026-04-12 10:00:00",
"delivered_at": "2026-04-12 10:00:01",
"started_at": "2026-04-12 10:00:03",
"completed_at": "2026-04-12 10:00:15",
"expires_at": "2026-04-12 11:00:00",
"network_id": "net_xxx"
}
}get_task does SELECT * FROM tasks (tools.ts — grep SELECT * FROM tasks WHERE task_id = ?1 inside get_task (3 sites; the others are retry_task / reassign_task)) and returns the full row (the example above shows sample fields; the actual row also includes requires_response / parent_task_id and every other column). When the task doesn't exist it returns {ok: false, error: "task not found"}.
list_tasks
View source ↗ —— grep for
"list_tasks"inserver/src/tools.ts(single registration site)
Query task list with multi-dimensional filtering.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
alias | string | Filter by recipient | |
status | string | Filter by status | |
from_name | string | Filter by sender | |
from_node_id | string | Filter by sender node_id (immutable ID, more precise than from_name) | |
network_id | string | Filter by network | |
limit | number | Max items (default 20, max 100) |
Response:
{
"ok": true,
"tasks": [
{
"task_id": "uuid-xxx",
"from_name": "commander",
"to_name": "coder-1",
"priority": "normal",
"status": "replied",
"content": "Write a sorting algorithm",
"result": "Implemented with quicksort...",
"created_at": "2026-04-12 10:00:00",
"runtime_submitted_at": "2026-04-12 10:00:03",
"consumed_at": "2026-04-12 10:00:04",
"completed_at": "2026-04-12 10:00:15"
}
],
"count": 1,
"stats": [
{ "status": "replied", "count": 42 },
{ "status": "running", "count": 3 },
{ "status": "delivered", "count": 1 }
]
}list_tasks rows are a subset of get_task
Each list_tasks row includes task identity, sender/recipient, status, content/result, and summary timestamps. runtime_submitted_at means the body was handed to the vendor runtime; consumed_at additionally requires attributable turn-start/activity evidence. It does not include delivered_at / started_at / expires_at / network_id / requires_response / parent_task_id — use get_task (SELECT *) for those. count is the number of rows returned this call (≤ limit); stats is the status-grouped count for the entire scope (not affected by the filters). See Task lifecycle for the distinction from legacy fields.
get_all_status
View source ↗ —— grep for
"get_all_status"inserver/src/tools.ts(single registration site)
Get all session statuses. Sessions without a heartbeat for over 10 minutes are auto-marked offline.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
filter_status | string | Filter by status (idle / working / offline) | |
filter_server | string | Filter by server | |
network_id | string | Filter by network |
Response:
{
"ok": true,
"sessions": [
{
"resume_id": "sdk-n_xxx",
"alias": "coder-1",
"status": "idle",
"agent": "agent-node:codex",
"node_id": "n_a1b2c3d4",
"last_seen_at": "2026-04-12 10:00:00",
"network_id": "net_xxx",
"queue_depth": 2
}
],
"summary": [
{ "status": "idle", "count": 5 },
{ "status": "working", "count": 2 },
{ "status": "offline", "count": 1 }
]
}Each row also carries queue_depth (#500): the number of open tasks on that node (same definition as queue_ahead in the send_task response: created / delivered / acked / running, last 24 hours, same network; messages do not count). One GROUP BY covers every node — not one query per row. To pick a node that can start right away, queue_depth=0 is more reliable than status alone.
The sessions row has no model field
get_all_status runs SELECT * FROM sessions (tools.ts — grep SELECT * FROM sessions WHERE 1=1, no JOIN). The sessions table schema (db.ts — grep CREATE TABLE IF NOT EXISTS sessions + V2 migration db.ts — grep ALTER TABLE sessions ADD COLUMN) has a model column — the V2 migration runs ALTER TABLE sessions ADD COLUMN model, and report_status's sessions upsert unconditionally writes sessions.model = COALESCE(model, old) (tools.ts — grep INSERT INTO sessions inside report_status (these columns are written by report_status, not by the tool documented in this section) and model = COALESCE(?20, sessions.model)). So get_all_status returns each session's model directly (null if the agent never passed a model parameter). The nodes table also keeps a copy of model (synced by report_status when node_id is passed) as the more durable source. summary is the status-grouped count over the entire scope (same as list_tasks's stats).
🔴 status answers "what it last reported", not "is it working right now"
sessions.status / progress are updated only at the moment a node calls report_status (tools.ts 搜 INSERT INTO sessions inside report_status), and report_completion resets them to idle (tools.ts 搜 UPDATE sessions SET status = 'idle', task = NULL, progress = 0 inside report_completion).
Nothing in between updates it on the node's behalf. So status: "idle" covers at least three different realities:
| Reality | What the panel shows | |
|---|---|---|
| ① | Genuinely idle | idle |
| ② | Got a task but never consumed it (wedged / loop not waking) | idle |
| ③ | Running a long task and not reporting mid-way | idle |
🔴 Measured 2026-08-18: a node showed status=idle / progress=100 with a heartbeat under 2.5 minutes old for 75 straight minutes, and its own reply said it had been running a long sync the whole time — case ③.
task is even easier to misread: it may be something the sender wrote. When the target session is not working, send_task stamps the first 200 chars of the task onto it inside its own transaction; when the target is already working, the column is left alone so it keeps the task in flight (tools.ts 搜 sessionTaskOnDispatch("?1") inside send_task; the condition is in session-task-on-dispatch.ts 搜 CASE WHEN status = 'working' THEN task ELSE). report_status writes the same column too (COALESCE — omitting it keeps the old value): when agent-node starts a task it reports the whole text, the hub compares that text with tasks.content and, on an exact match, marks the row running and sets started_at — but it still stores only the first 200 chars in task (tools.ts 搜 sessionTaskPreview(task); // board668-session-preview inside report_status). So task is at most 200 chars and the full text lives only on the task row — task is readable through get_all_status and the full /api/status, a wider audience than the task row.
⇒ Seeing your own text in task proves the hub recorded that you sent it. It does not prove the node read it. That is the echo of your own action.
There is exactly one structurally sound way to tell whether a node is working: send something that requires a reply, and see whether it replies. The status panel cannot answer this — not because the data is stale, but because of when these columns are written.
get_session_status
View source ↗ —— grep for
"get_session_status"inserver/src/tools.ts(single registration site)
Get detailed status of a single session, including pending inbox count and recent completions.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
alias | string | ✓ | Session alias |
Response:
{
"ok": true,
"session": {
"resume_id": "sdk-n_xxx", "alias": "coder-1", "status": "idle",
"agent": "agent-node:codex", "node_id": "n_a1b2c3d4",
"last_seen_at": "2026-04-12 10:00:00", "network_id": "net_xxx"
},
"inbox_pending": 2,
"recent_completions": [
{
"id": "uuid-xxx",
"session_name": "coder-1",
"task": "Write sorting algorithm",
"result": "Done",
"artifacts": null,
"score": 8,
"duration_minutes": 2,
"network_id": "net_xxx",
"completed_at": "2026-04-12 10:00:15"
}
]
}Response shape
sessionisSELECT * FROM sessions(tools.ts— grepSELECT * FROM sessions WHERE alias = ?1) — the full sessions row (same asget_all_status's session row, including themodelcolumn — see the get_all_status note); if the alias doesn't existsessionisnullbutokis stilltruerecent_completionsisSELECT * FROM completions ... LIMIT 5(tools.ts— grepSELECT * FROM completions WHERE session_name = ?1) — the full 9-column completion row (id/session_name/task/result/artifacts/score/duration_minutes/network_id/completed_at), ordered bycompleted_atDESC, max 5
get_completions
View source ↗ —— grep for
"get_completions"inserver/src/tools.ts(single registration site)
Get completion records.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
alias | string | Filter by agent | |
since | string | Start time (ISO 8601, default last 24h) | |
network_id | string | Filter by network | |
limit | number | Max items (default 50, max 500) |
Response:
{
"ok": true,
"completions": [
{
"id": "uuid-xxx",
"session_name": "coder-1",
"task": "Write sorting algorithm",
"result": "Implemented with quicksort...",
"artifacts": "[\"/tmp/sort.py\"]",
"score": 8,
"duration_minutes": 2,
"network_id": "net_xxx",
"completed_at": "2026-04-12 10:00:15"
}
]
}completions is SELECT * FROM completions WHERE completed_at >= <cutoff> (tools.ts — grep SELECT * FROM completions WHERE completed_at >= ?1) — the full 9-column row, ordered by completed_at DESC. artifacts is a JSON-array string (not a parsed array — report_completion JSON.stringifys it on the way in). When since is omitted the cutoff defaults to 24 hours ago.
Broadcast Tools
broadcast
View source ↗ —— grep for
"broadcast"inserver/src/tools.ts(single registration site)
Broadcast a message to all online agents. broadcast triggers AI processing on receivers, the same as task (agent-node/src/cli.ts thinks only on task and broadcast types; reply / message / ack are display-only). If you just want a notification without an AI reply, loop send_message instead. Full message-type table: Task lifecycle — Message types.
Parameters (verify tools.ts — grep "Send a message to multiple sessions." (broadcast's registration description, unique; the parameter schema follows it)):
| Parameter | Type | Required | Description |
|---|---|---|---|
message | string | ✓ | Broadcast content (max 10000 chars) |
filter_server | string | Only deliver to sessions whose server field matches | |
filter_status | string | Only deliver to sessions in the given status (e.g. idle / working) | |
network_id | string | Network ID (broadcast within this network only; can be supplied with a utok_, but ntok_ callers are pinned to their bound network) |
The field is
message, notcontent;from_sessionis not a parameter — the server hard-codes it to'hub'.
Response:
{
"ok": true,
"recipients": 10,
"message_ids": ["uuid-xxx-1", "uuid-xxx-2"]
}message_ids.length === recipients — one inbox row per target session.
Requirements / task board
Tasks (requirement cards) live on the Hub; the app's 任务 page and agents read and write the same records. The MCP tools and REST go through the same handler, so permissions, validation and the "who did it" record exist once.
Permissions
- User tokens: same as the app (viewers are read-only).
- Node tokens (agents): read, create, patch, toggle checklist items, comment, upsert and list projects in the network the token is bound to only. Other networks' tasks are invisible (404); writes always land in the token's own network — a
network_idin the body cannot move them. - Delete is human-only (REST
DELETE /api/requirements/{id}; node tokens get 403user_token_required); agents archive witharchived: true. Creating / editing / deleting projects is human-only too. - Every task records
created_by/updated_by:{kind: "user" | "node", id}; a node token is recorded as its boundnode_id.
Fields: name, column (pool / doing / done / abandoned. abandoned = 废弃, a closed state like done: not open, never overdue, no due reminders, no completion time; it can be moved back to pool / doing; supported by hubs whose list capabilities include column_abandoned, older hubs answer 400 invalid_column. Old-client compatibility: a REST caller that does not declare it gets abandoned projected to done in responses (desktop ≤ 0.2.220 files unknown values under pool), status=done also matches abandoned cards, and echoing column: "done" back does not turn an abandoned card into a done one; declare with the header X-Anet-Accept-Columns: abandoned or the query parameter accept_columns=abandoned; MCP always gets the real value. The event log records the real value), priority (high / normal / low / lowest, shown in the app as P0 highest / P1 normal / P2 low / P3 lowest; lowest is supported by hubs whose list response capabilities include priority_lowest, older hubs answer 400 invalid_priority), due (YYYY-MM-DD all day, or an ISO instant with Z / ±HH:MM, stored as UTC seconds), start (optional start, same shapes as due, used by the Gantt view; supported by hubs whose list capabilities include start_date, invalid values are 400 invalid_start; older hubs ignore it), description (markdown, ≤ 20000 chars), checklist ([{id, text, done}], ≤ 100 items; replaced as a whole), owner (负责人, {kind:"user"} only, written as {kind:"user", id} or {kind:"user", username}; for old clients a node owner sent without agent_owner is stored as agent_owner and the response carries owner_coerced_to_agent_owner: true), agent_owner (负责 Agent, {kind:"node"} only, written as {kind:"node", id} or {kind:"node", alias}), participants (either kind, also by name; names are resolved only in the task's own network and stored as {kind, id}, and an id, when given, wins; an unknown name is 400 person_not_in_network, an alias matching several Agents is 400 person_ambiguous, both with field and a hint pointing at requirements_people), project_id, parent_id (sub-requirements: same network, no cycles, at most 5 levels; a parent returns children: {total, done}; deleting a parent keeps its children and makes them top level), external_ref (unique per network, e.g. github:owner/repo#123), external_url (http(s) link), archived, tags (string array, at most 10, each at most 20 Unicode characters; omit to keep, [] to clear; invalid values are 400 invalid_tags).
Short number seq (task ID #N): besides its primary key id (req_<uuid>) every task has a read-only seq — counted from 1 per network and shown in the app as #N. The Hub assigns it inside the create transaction (concurrent creates never share a number); it never changes afterwards, and numbers of deleted or archived tasks are never reused. Tasks created before the upgrade are numbered per network at Hub startup in created_at order (then id). Supported by hubs whose list capabilities include requirement_seq; older hubs have no such field and older apps ignore it.
Everywhere an {id} is accepted (REST GET / PATCH / DELETE / checklist toggle; the id of MCP requirements_get / requirements_update / requirements_checklist_toggle) #N works too (percent-encode it in a REST path: /api/requirements/%2342). Only the #-prefixed form is a short number; a bare number is still looked up as an id. Old req_… ids keep working. A short number is unique within one network only: when the caller's scope spans several networks and more than one of them has #N, the answer is 409 {error:"ambiguous_seq", networks, message} — retry with ?network_id= (MCP: pass network_id). The list can filter by number as well: GET /api/requirements?seq=N (seq on MCP requirements_list).
HTTP endpoints (Authorization: Bearer <token>; multi-network user tokens add ?network_id=)
| Method | Path | Notes |
|---|---|---|
| GET | /api/requirements | List; filters seq (short number, positive integer), status, project_id (none = no project), owner / agent_owner (user:<id> / node:<id> / none), updated_since (ISO), external_ref, tag (only tasks carrying this label, exact and case-sensitive; invalid → 400 invalid_tag), parent_id (none = top level) / top_level=1, department_id (cards whose owner, or whose Agent's owner, is in that department or below; see Department heads); excludes archived by default, include_archived=1 includes all, archived=true returns only archived (takes precedence over include_archived); q server-side search (title, description, owner / agent owner / participant display names, project name, tags; space-separated terms must all match; names of agents the caller cannot see are not matched; the whole query is also matched as a task id: #N / N against the short number seq, a full id or an 8+ character prefix against the id, full-width # counts as #); paging with limit (default 500, max 1000) + cursor, the response carries has_more / next_cursor, newest first by created_at then id; the response carries capabilities. Every row carries last_event (capability last_event): the card's newest activity entry, comments included, {type, field, actor: {id, kind, display_name}, at, summary?}, or null when it has none; same visibility as the row, and an Agent the caller is not granted shows as actor: null; last_event=0 leaves the field out (what MCP requirements_list sends by default), any other value is 400 invalid_last_event. The incremental read changes=1 + updated_since (when rows carry last_event) also returns cards with a newer activity entry, a comment for instance: comments do not touch updated_at, and updatedAt keeps its meaning |
| GET | /api/requirements/{id} | One task; {id} may also be %23N (short number #N) |
| POST | /api/requirements | Create; duplicate external_ref → 409 {error:"external_ref_exists", existing_id} |
| POST | /api/requirements/upsert | Create or patch by external_ref (omitted fields, including status, are kept); returns {requirement, created} |
| PATCH | /api/requirements/{id} | Patch (omitted fields kept); participants replaces the list, participants_add / participants_remove change it in place (as in MCP). REST does not take the status alias (still empty_patch, with a hint to use column) |
| PATCH | /api/requirements/{id}/checklist/{itemId} | {done: true|false}; only that item |
| POST | /api/requirements/{id}/comments | {text} (1–4000 characters, trimmed) adds one comment, append-only; returns 201 {event} (kind: "comment", new.text). Empty → 400 invalid_comment, too long → 400 comment_too_long; there is no route to edit or delete a comment. Comments are kept 180 days, like field changes |
| DELETE | /api/requirements/{id} | Delete (humans only); children are kept and detached |
| GET | /api/requirements/projects | Projects (creating / editing / deleting projects is human-only) |
| GET | /api/requirements/tags | Tags that appear in the current network (deduplicated and sorted, including tags left only on archived tasks). A node token may read them. A Hub with the tag_ops capability also returns counts / colors / can_manage; a Hub with tag_tree also returns nodes (the tag tree, zero-task tags included — see Task tags) |
| POST | /api/requirements/tags/ops | Rename / merge / delete a tag across the network, or set its color (humans only; scoped members get 403). See Task tags |
| POST | /api/requirements/tags/create | Create a tag, optionally with parent_id (at most 4 levels); a duplicate returns 409 tag_exists, a missing parent 404 tag_parent_not_found (never falls back to the root). See Task tags |
Sync example (GitHub issues → tasks): call requirements_upsert_by_external_ref once per issue with external_ref = github:<owner>/<repo>#<number> and external_url = the issue link; re-syncing updates the same task instead of creating a duplicate. Use updated_since on requirements_list for incremental sync.
Common Response Format
All tools return in MCP Content format:
{
"content": [
{
"type": "text",
"text": "{\"ok\": true, ...}"
}
]
}The text field is a JSON string that needs to be parsed.
Error Codes
| Error | Meaning |
|---|---|
network_id_required | The write could not be scoped to a network: the utok_ caller has 0 or ≥2 network memberships and passed no explicit network_id. With exactly 1 membership the hub auto-resolves — no parameter needed. The message states which case applies (no memberships vs. multiple networks) |
access_denied | The explicitly supplied network_id names a network the caller is not a member of |
permission_denied | A viewer role attempted a write (viewers are read-only; owner/admin/member can write) |
license_expired | Trial period expired (v0.6 legacy path; not needed after Apache 2.0 OSS — when hit, follow the license_expired section in troubleshooting to clear the SQLite licenses table) |
message not found or not yours | Message doesn't exist or doesn't belong to this agent |
task not found | Task doesn't exist |
task is terminal | Task is in a terminal state, cannot be operated on |
task status is X, not retryable | Only failed/expired/cancelled tasks can be retried |
node_permission_denied | A node token was refused by the node's own permissions: the node is in read-only/restricted mode, or it went beyond its owner's permissions while the Hub switch is COMMHUB_NODE_PERMISSIONS=enforce. The body carries reason, route and hint; see A node's own permissions |
Next steps
Corresponding REST API:
- REST API — the HTTP endpoints these MCP tools call under the hood
Agent integration:
- Agent Node — how an agent connects to the MCP server
- Runtimes — all runtimes talk to the Hub via MCP
- Channel plugins — how to write a custom MCP channel
Hands-on: