Clawboo hosts four MCP servers over the shared SQLite substrate, exposed to consuming runtimes over two transports: a stdio bin per server, and an in-process Streamable HTTP mount at /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. Two servers (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
| Server | tools/list name | Tools | Service core | Bound scope |
|---|
| Tasks | clawboo-tasks | 12 | durable board repository | , |
| Memory | clawboo-memory | 3 | SqliteMemoryStore (facts + procedures) | boundScope (team/agent) |
| Tools | clawboo-tools | 4 builtin (availability-filtered) | tool broker | , |
| TeamChat | clawboo-teamchat | 2 | team_chat room substrate | boundIdentity (author + room) |
| Server | stdio bin | HTTP path |
|---|
| Tasks | clawboo-mcp-tasks | /api/mcp/tasks |
| Memory | clawboo-mcp-memory | /api/mcp/memory |
| Tools | clawboo-mcp-tools | /api/mcp/tools |
| TeamChat | clawboo-mcp-teamchat | /api/mcp/teamchat |
Tasks server
createTasksServer(db) → 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).
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.
list_tasks
List board tasks. Pass ready=true for only claimable (deps satisfied) work.
{
teamId?: string
status?: 'backlog' | 'todo' | 'in_progress' | 'in_review' | 'blocked' | 'done' | 'cancelled'
ready?: boolean
}
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.
{
title: string
description?: string
status?: 'backlog' | 'todo' | 'in_progress' | 'in_review' | 'blocked' | 'done' | 'cancelled'
priority?: number // integer
teamId?: string
parentTaskId?: string
assigneeRuntime?: string
}
create_subtask
Create a subtask under a parent (inherits the parent’s team).
{ parentTaskId: string; title: string; description?: string }
claim_task
Atomically claim a todo task. A conflict error means another agent won; do not retry. Returns the claimed task on success.
{ taskId: string; assigneeAgentId: string; assigneeRuntime?: string }
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.
{ taskId: string; assigneeAgentId: string; assigneeRuntime?: string }
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>.
{
taskId: string
status: 'backlog' | 'todo' | 'in_progress' | 'in_review' | 'blocked' | 'done' | 'cancelled'
}
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 a comment to a task (report-up summaries, system notes). authorType defaults to 'agent'.
{
taskId: string
body: string
authorAgentId?: string
authorType?: 'agent' | 'user' | 'system'
}
link_task
Make taskId depend on dependsOnTaskId; it stays unready until the dependency is done.
{ taskId: string; dependsOnTaskId: string }
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.
When unset (the raw stdio bin / unbound default), the model’s scope args are used. Over HTTP the binding rides query params on the Memory attach URL (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 }.
{
content: string
title?: string // required for a fact (not a procedure)
tags?: string[]
procedureName?: string // set to save a procedure instead of a fact
scopeTeamId?: string // ignored when the connection is boundScope-bound
scopeAgentId?: string // ignored when bound
}
memory_search
Search saved facts. mode is fts (default), vector, or hybrid. Results cite a fact id. limit is 1–100.
{
query: string
mode?: 'fts' | 'vector' | 'hybrid'
limit?: number // integer, 1..100
scopeTeamId?: string // ignored when bound
scopeAgentId?: string // ignored when bound
}
memory_browse
List recent saved facts (scoped). limit is 1–200.
{
limit?: number // integer, 1..200
scopeTeamId?: string // ignored when bound
scopeAgentId?: string // ignored when bound
}
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:
| Tool | Description | Input schema | Risk | Availability |
|---|
echo | Echo a message back. Safe, no side effects. | { message: string } | safe | always |
note | Record a short note. Safe. | { note: string } (min 1) | safe | always |
web_search | Search the web. External side effect → requires approval. | { query: string; limit?: number } | external | hidden until a search provider is configured (TAVILY_API_KEY env or an authed tavily provider) |
delete_path | Delete a path. Destructive → requires approval. (Demo executor, does not actually delete.) | { path: string } (min 1) | destructive | always |
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. 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.
{
text: string
authorAgentId?: string // ignored when the connection is bound
teamId?: string // ignored when bound
roomId?: string // ignored when bound
}
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.
{
sinceSeq?: number // integer ≥ 0, default 0
limit?: number // integer, 1..500
authorAgentId?: string // ignored when bound
teamId?: string // ignored when bound
roomId?: string // ignored when bound
}
The 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.
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 npx clawboo install ships them:
| Bin | Server |
|---|
clawboo-mcp-tasks | tasks |
clawboo-mcp-memory | memory |
clawboo-mcp-tools | tools |
clawboo-mcp-teamchat | teamchat |
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 the mcp-session-id header: a fresh server + transport is created on the initialize request and reused for that session’s subsequent calls.
| Method | Path (× tasks / memory / tools / teamchat) | Role |
|---|
| POST | /api/mcp/<name> | JSON-RPC requests (initialize, tools/list, tools/call) |
| GET | /api/mcp/<name> | SSE stream for an established session |
| DELETE | /api/mcp/<name> | session teardown |
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 / scopeTenantId set the run’s visibility scope (boundScope).
- TeamChat:
roomTeamId / postAuthorAgentId set the room and post author (boundIdentity).
- Tasks / Tools: no scope params; the URL stays bare.
When these params are absent (an external attach, or the stdio bins), the server is unbound and the model supplies scope/identity in args.
See also