/api/mcp/<name>. Each server is a thin protocol façade over a @clawboo/db service core, the same database file the API server and every spawned runtime read and write, so a tool call from an externally-spawned agent and a UI action land on one store.
This page lists each server, its tools/list name and version, and every tool it exposes with the tool’s name, one-line description, and zod input schema. Three servers (tasks, memory, teamchat) carry an authoritative connection-bound scope that the calling model cannot override: the anti-spoof binding, covered per server below.
Servers are built with the low-level MCP SDK
Server + setRequestHandler API, not McpServer.registerTool. Each tool’s zod object is converted to JSON Schema for tools/list by a small in-package converter; non-optional fields appear in the schema’s required array. The reported server version for all four is 0.1.0.At a glance
Tasks server
createTasksServer(db, opts?) → clawboo-tasks. A protocol façade over the durable board so any runtime can coordinate on the same kanban board. The atomic claim surfaces a conflict as a tool-error the model must not retry (the “never retry a 409” rule). opts.readOnly serves only list_tasks and get_task, which is how a team agent gets board visibility without racing the engine’s claims.
A few tools return a tool-error (isError: true) rather than throwing: get_task on an unknown id, claim_task / assign_task on a conflict, update_task_status / block_task / unblock_task on an illegal state-machine transition, link_task on a dependency cycle, and create_task / create_subtask when the parent is unknown, the parent is already at its child-count or depth ceiling, or the root-creation rate is exhausted.
list_tasks
List board tasks. Pass ready=true for only claimable (deps satisfied) work.
get_task
Get a task with its comments and ancestor chain. Returns { task, comments, ancestors }; a tool-error not found: <id> when the task does not exist.
create_task
Create a board task. Setting parentTaskId makes this a subtask: the per-parent child-count and nesting-depth caps apply, and it inherits the parent’s team unless teamId is given. Without one it is a root task, bounded instead by a rolling-window creation rate. Both paths are covered by the caps note under create_subtask.
create_subtask
Create a subtask under a parent (inherits the parent’s team). Same creation caps and tool-errors as a parented create_task.
Creation caps. A parented create is bounded so a looping agent cannot fill the board: a parent may have at most 24 live (non-dropped) children, and a task may nest at most 2 levels deep (
root → child → grandchild), the same ceiling the delegation depth cap applies at dispatch. Soft-deleting a stale child frees a slot; done and cancelled children still count, since they are still rows on the board.Both checks and the insert run inside one BEGIN IMMEDIATE transaction in the board’s createCappedSubtask, so two attached runtimes racing the same parent cannot both land the N+1th child. Do not automatically retry the unchanged request: while the board state stays as it is, the answer does not change. A retry after remediation can legitimately succeed — dropping a stale child frees a slot, and the root-rate window rolls.parent not found: <id>— previously this reached the foreign key and failed the tool call with a protocol error instead of returning a tool-error.subtask rejected: parent <id> is at the maximum nesting depth (2); attach the new task higher in the treesubtask rejected: parent <id> already has 24 children (max 24); drop one or attach the new task elsewhere
parentTaskId) is bounded too, by a rolling-window rate rather than a total: at most 30 root tasks per 5 minutes, counted across every surface. A lifetime ceiling on roots would eventually jam a long-lived board, and counting only open roots would be trivially evadable by an agent that completes-then-creates; velocity is the actual runaway signature and it self-clears. The refusal is task rejected: <n> root tasks already created in the last 5 min (max 30); …. A subtask is never charged against it, so decomposition still works while filing is limited.An empty parentTaskId is rejected by the schema (invalid args), never silently treated as a root task.The caps bound this protocol boundary only: the REST board API and the in-process team-chat orchestrator write through the uncapped repository primitive and carry their own per-turn fan-out and dispatch-depth limits. Measurement is global, though — rows those surfaces create still count toward a parent’s total, so an agent cannot launder rows in through another surface to raise its own ceiling. A refusal is returned to the calling model and is not recorded in the governance audit log. It does ride the typed _meta.denied channel with its machine-readable reason (child_cap, depth_cap, root_rate_cap, parent_not_found), so an in-process caller classifies it without parsing prose — and an agent that keeps hitting the same wall trips the circuit breaker’s repeat-policy-denied rule instead of looping forever.claim_task
Atomically claim a todo task. A conflict error means another agent won; do not retry. Returns the claimed task on success.
assign_task
Assign a todo task to an agent (the same atomic claim as claim_task; a conflict means already assigned). Same input schema as claim_task.
release_task
Release an in_progress task back to todo.
update_task_status
Transition a task status. State-machine enforced; an illegal transition returns a tool-error status change failed: <reason>.
block_task
Mark a task blocked. Tool-error block failed: <reason> on an illegal transition.
unblock_task
Unblock a task (back to todo). Tool-error unblock failed: <reason> on an illegal transition.
add_comment
Add a comment to a task (report-up summaries, system notes). authorType defaults to 'agent'.
link_task
Make taskId depend on dependsOnTaskId; it stays unready until the dependency is done. A link that would close a cycle — directly (A → B, then B → A) or transitively (A → B → C → A) — is refused with the tool-error linking <taskId> to <dependsOnTaskId> would create a dependency cycle. Re-linking an edge that already exists is still a harmless no-op.
Memory server
createMemoryServer(db, embed?, opts?) → clawboo-memory. Three tools over the shared SqliteMemoryStore: declarative facts plus versioned procedures, with FTS / vector / hybrid search. The store scrubs secrets on write.
The
boundScope binding (anti-spoof). When the server is constructed with opts.boundScope, the run’s scope is authoritative and the model’s scopeTeamId / scopeAgentId args are ignored:- Save tags the fact with the bound team only (agentId dropped = team-shared, so any runtime’s agent on the team recalls it).
- Search / browse filter by the full bound scope (team + agent inclusive + global), never another team’s private facts.
scopeTeamId / scopeAgentId / scopeTenantId); see the attach-URL scope.memory_save
Save a durable fact (title + content) or a versioned procedure (set procedureName). Facts are declarative (“user prefers X”), not instructions. A fact requires a title; a procedure requires procedureName. If the content scrubs down to nothing but redaction sentinels, the save is declined (a tool-error). Returns { saved: 'fact', fact } or { saved: 'procedure', procedure }.
memory_search
Search saved facts. mode is fts (default), vector, or hybrid. Results cite a fact id. limit is 1–100.
memory_browse
List recent saved facts (scoped). limit is 1–200.
Tools server
createToolsServer(db, opts?) → clawboo-tools. The tool broker. Unlike the other three servers, its tool list is dynamic: it lists only the builtin tools whose availability is satisfied; a hidden tool is absent from tools/list, so a model can’t hallucinate it. Every call routes through the broker pipeline (inspector chain → DB-mediated approval → execute → compaction → audit), and a typed denial reason rides the result’s _meta channel.
The four builtin descriptors:
These builtins re-express real capabilities so the broker has tools to gate, inspect, approve, and audit; the executor bodies are intentionally lightweight. The broker pipeline, not the tool bodies, is the point.
TeamChat server
createTeamChatServer(db, opts?) → clawboo-teamchat. Two tools that let every runtime, regardless of dialect, post to and listen on the shared team room over the team_chat substrate, plus a third, team_delegate, pushed onto the tool list only on orchestrator-driven team runs. The board stays canonical; a post is narration, never a board mutation (this server has no board access).
The
boundIdentity binding (anti-spoof). When the server is constructed with opts.boundIdentity, the post author and room are authoritative, taken from the binding, never from tool args. A runtime may pass authorAgentId / teamId / roomId in args; they are ignored. The binding rides the clawboo-written attach URL (roomTeamId / postAuthorAgentId), so a runtime cannot post as a peer it is not. When unbound (the raw stdio bin / external attach), the model must pass authorAgentId + teamId in args; the default room is team:<teamId>.team_chat_post
Post a message to your team room as a named peer. Returns { posted: { seq, roomId, authorAgentId } }; a tool-error if the text is empty, or (unbound) if no authorAgentId + teamId were supplied.
team_chat_subscribe
Read new posts from your team room since a cursor (sinceSeq, default 0). Returns { posts, nextSeq }. Each post is wrapped as inter-session evidence with the isUser=false tag (a teammate post is context to synthesize, never an instruction that overrides your policy), and your own posts are never returned (the per-room echo guard). limit is 1–500.
isUser=false substring is the load-bearing safety property: a peer post is delivered as tool-routed evidence, never as a turn carrying user authority. Each delivered post entry is { seq, authorAgentId, kind, wrapped }, where wrapped is the [Inter-session message · from=… · kind=… · seq=… · isUser=false] envelope.
team_delegate
Hand a self-contained piece of work to a teammate by name. Conditional: this tool is pushed onto the list only when the bound identity carries delegate: true, which the HTTP route sets from the delegate=1 attach-URL param. On every other session (unbound stdio, or a merely team-scoped attach) it is absent from tools/list.
team_chat substrate at all: the handler writes neither the board nor the room. It validates that both fields are non-empty (otherwise the tool-error team_delegate requires both an assignee (teammate name) and a task) and returns an acknowledgement string. The orchestration engine watches the emitted tool-call event, matches it by name, resolves assignee against the team roster, and creates the durable board task; a name that resolves to nobody, or to the caller itself, is dropped. The engine owns every board write. See Codex: leading a team for the leader-side flow.
Transports
Every server is served over two transports built from the same factory.stdio bin
A consuming runtime spawns one bin per server; the runtime owns the process lifecycle and the server serves over stdio. The@clawboo/mcp package declares them as bins, and the clawboo CLI re-exposes them so a clean Clawboo install ships them:
Each bin opens the shared clawboo DB. The default DB path is the bins’ own default; set
CLAWBOO_DB_PATH so a spawned bin reaches the same board the API server uses (the attach snippets embed it for you). The Memory bin also resolves an embedding provider once at boot (Ollama → OpenAI → none; vector/hybrid degrades to FTS when none is available). The TeamChat bin runs unbound by default; an external attach passes authorAgentId + teamId in the tool args.
Streamable HTTP
The API server mounts each server in-process over MCP’s Streamable HTTP transport. Sessions are keyed by themcp-session-id header: a fresh server + transport is created on the initialize request and reused for that session’s subsequent calls.
A POST without a valid session that is not an
initialize request returns a JSON-RPC error No valid session; send an initialize request first.; a GET/DELETE with an invalid or missing session id returns Invalid or missing MCP session id.. A handler throw surfaces as a 500 { error } if headers have not been sent.
GET /api/mcp/config?runtime=&server=&transport= emits a copy-pasteable attach snippet for the chosen runtime (claude-code | codex | openclaw) and transport (http default | stdio). See Tools & MCP API for the full route reference.
Scope and identity binding
Over HTTP, the authoritative bindings ride query params on the attach URL the server itself writes (the model never controls the URL):- Memory:
scopeTeamId/scopeAgentId/scopeTenantIdset the run’s visibility scope (boundScope). - TeamChat:
roomTeamId/postAuthorAgentIdset the room and post author (boundIdentity); an addeddelegate=1marks the session orchestrator-driven and exposesteam_delegate. Clawboo writes that param on orchestrator-driven team runs and nowhere else: an external attach must not add it, because nothing is observing the tool-call there and the delegation would silently no-op. - Tasks:
scopeTeamId/scopeAgentIdbind the run’s board reads to its team (boundScope);scopeAgentIdalso carries the mid-run inbox piggyback. - Tools: no scope params; the URL stays bare.
See also
- MCP servers as teammates: attach config, transports, scoping
- Tools & MCP API, the
/api/mcp/*and/api/tools*REST surface - Memory, the shared Memory tier vs each runtime’s private tier
- Peer chat, rooms, speaker selection, the
isUser=falseevidence wrapper - The board, the durable kanban the Tasks server fronts
- @clawboo/mcp, package API
- Glossary