REST surface for two related systems. The tools broker REST routes (/api/tools*) are the UI-facing half of the broker’s dual surface: list the builtin tools with their availability verdict, read the pending tool-approval queue, resolve an approval, and read the forensic audit log. The MCP routes (/api/mcp/*) are the in-process Streamable-HTTP transport for Clawboo’s four MCP servers (tasks, memory, tools, teamchat), the same SQLite-backed servers an external runtime attaches to, plus a per-runtime attach-config snippet endpoint.
The MCP routes are not request/response in the usual sense. They speak the Model Context Protocol over Streamable HTTP: a per-session JSON-RPC channel keyed by the mcp-session-id header. They are documented below with the transport mechanics + status codes, not a JSON response shape. The tools and their input schemas served over these channels are in MCP tools reference.
All POST bodies on these routes are parsed by express.json({ limit: '2mb' }). Error responses on the /api/tools* routes are masked at the rendering boundary by redactValue / redactJsonString (defense-in-depth over the write-time secret scrub).
Routes
| Method | Path | Summary | Stream? |
|---|
| GET | /api/tools | List builtin tools + per-tool availability verdict | No |
| GET | /api/tools/approvals | The pending tool-approval queue | No |
| POST | /api/tools/approvals/:id/resolve | Resolve a pending approval (allow/deny) | No |
| GET | /api/tools/audit | The before/after tool-call audit log | No |
| GET | /api/mcp/config | Emit a per-runtime, per-server attach snippet | No |
| POST | /api/mcp/tasks | MCP JSON-RPC POST (tasks server) | Streamable HTTP |
| GET | /api/mcp/tasks | MCP SSE stream (tasks session) | SSE |
| DELETE | /api/mcp/tasks | MCP session teardown (tasks) | No |
| POST | /api/mcp/memory | MCP JSON-RPC POST (memory server) | Streamable HTTP |
| GET | /api/mcp/memory | MCP SSE stream (memory session) | SSE |
| DELETE | /api/mcp/memory | MCP session teardown (memory) | No |
| POST | /api/mcp/tools | MCP JSON-RPC POST (tools server) | Streamable HTTP |
| GET | /api/mcp/tools | MCP SSE stream (tools session) | SSE |
| DELETE | /api/mcp/tools | MCP session teardown (tools) | No |
| POST | /api/mcp/teamchat | MCP JSON-RPC POST (teamchat server) | Streamable HTTP |
| GET | /api/mcp/teamchat | MCP SSE stream (teamchat session) | SSE |
| DELETE | /api/mcp/teamchat | MCP session teardown (teamchat) | No |
Lists every builtin tool with a server-evaluated availability verdict. The availability is computed against the current process env (defaultAvailabilityContext()), so a tool that requires an unconfigured provider reads available: false with a diagnostics list explaining why; the UI greys it out and the model’s tools/list omits it. The four builtins are echo and note (safe), web_search (external, hidden until a search provider is configured), and delete_path (destructive).
- Path/query params: none.
- Request body: none.
Responses
200 OK: the tool list with availability:
{
ok: true
tools: Array<{
name: string
description: string
owner: 'core' | 'plugin' | 'channel' | 'mcp' // defaults to 'core'
risk: 'safe' | 'destructive' | 'external' // defaults to 'safe'
available: boolean // availability.visible
diagnostics: string[] // why it is hidden, if so
}>
}
500 Internal Server Error: any failure building the registry or evaluating availability:
{ "error": "<masked message>" }
Example
curl http://localhost:18790/api/tools
Returns the pending tool-approval queue, the tool_call_approvals rows with status = 'pending' that have not passed their expiresAt, newest first. Each row’s argsSummary (scrubbed JSON written by the broker) is masked again at this boundary via redactJsonString. This is the queue the Approvals panel and the Governance dashboard render through the shared tool-approval queue UI; the broker (in either the Express process or an MCP stdio bin) polls the same rows for resolution.
- Path/query params: none.
- Request body: none.
Responses
200 OK: the pending approvals:
{
ok: true
approvals: Array<{
id: string
toolName: string
agentId: string | null
argsSummary: string | null // scrubbed + re-masked JSON
reason: string | null
status: 'pending' // only pending rows are returned
taskId: string | null // the board task this approval gates, if known
tenantId: string | null // dormant multi-tenant seam
createdAt: number
expiresAt: number
resolvedAt: number | null
}>
}
500 Internal Server Error:
{ "error": "<masked message>" }
Example
curl http://localhost:18790/api/tools/approvals
POST /api/tools/approvals/:id/resolve
Resolves a still-pending approval. The decision is validated by a Zod schema; an unknown decision returns 400. The UPDATE is guarded status = 'pending', so resolving an already-resolved (or expired-but-still-present) row is a no-op UPDATE; but the handler still returns 200 with the existing row unchanged, because resolveApproval returns getApproval(db, id), which has no status or expiry filter and is truthy for any row that exists. The 404 (approval not found) fires only when no row with that id exists at all. A blocking broker waiter sees the new status on its next poll.
- Path params:
id (approval id).
- Request body: validated by
resolveApprovalBody:
{
decision: 'allow_once' | 'allow_always' | 'deny' // required
}
Responses
400 Bad Request: the body fails Zod validation (missing/invalid decision):
{ "error": "invalid body", "details": { /* zod flatten() */ } }
404 Not Found: no row with that id exists at all (getApproval returns nothing):
{ "error": "approval not found" }
200 OK: the approval was resolved; the updated row is returned:
{
ok: true
approval: {
id: string
toolName: string
agentId: string | null
argsSummary: string | null
reason: string | null
status: 'allow_once' | 'allow_always' | 'deny'
taskId: string | null
tenantId: string | null
createdAt: number
expiresAt: number
resolvedAt: number // set to Date.now() on resolution
}
}
The response approval.argsSummary here is the stored (scrubbed-at-write) value; unlike GET /api/tools/approvals, the resolve response is not re-masked at the boundary.
500 Internal Server Error:
{ "error": "<masked message>" }
Example
curl -X POST http://localhost:18790/api/tools/approvals/<approval-id>/resolve \
-H 'Content-Type: application/json' \
-d '{"decision":"allow_once"}'
Returns the append-only tool-call audit log (before + after phases), newest first. Both argsSummary and resultSummary are masked at this boundary with redactJsonString. The default page size is 100 rows.
| Param | Type | Default | Meaning |
|---|
toolName | string | (all tools) | Filter to one tool’s audit rows |
limit | number | 100 | Max rows returned |
Responses
200 OK: the audit rows:
{
ok: true
audit: Array<{
id: string
toolName: string
agentId: string | null
phase: 'before' | 'after'
decision: string | null // allow | deny | require_approval | rewrite (before phase)
argsSummary: string | null // scrubbed + re-masked JSON
resultSummary: string | null // scrubbed + compacted + re-masked (after phase)
isError: number // 0 | 1
tenantId: string | null
createdAt: number
}>
}
500 Internal Server Error:
{ "error": "<masked message>" }
Example
curl 'http://localhost:18790/api/tools/audit?toolName=web_search&limit=20'
These four route groups are the in-process Streamable HTTP transport for Clawboo’s MCP servers. The API server hosts and supervises the servers in-process; HTTP-capable runtimes (and the live smoke) attach over these paths. They are NOT a Clawboo-defined request/response API; they speak MCP’s JSON-RPC over Streamable HTTP, with a stateful session per connection keyed by the mcp-session-id header.
Each server name maps to a (POST, GET, DELETE) trio:
| Method | Role |
|---|
POST | JSON-RPC request channel. The first POST must be an initialize request (no session id yet); the transport mints a session id and returns it via the mcp-session-id response header. Subsequent POSTs reuse that session id. |
GET | The server-to-client SSE stream for the established session (server notifications + streamed results). |
DELETE | Tears down the session. |
The session lifecycle is enforced by the transport, not by Clawboo handlers:
- A
POST with no session id that is not an initialize request → JSON-RPC error, HTTP 400 ("No valid session; send an initialize request first.").
- A
POST with a session id that has no live transport → same 400.
- A
GET/DELETE with a missing or unknown session id → JSON-RPC error, HTTP 400 ("Invalid or missing MCP session id.").
- A handler-level throw before headers are sent → HTTP 500
{ error: "<message>" }.
A fresh MCP server instance is created per session (on the initialize request) and reused for that session’s subsequent requests; closing the transport drops it from the session map.
Per-server session binding (scoping query params)
The memory and teamchat POST handlers read authoritative binding from the attach URL’s query string at session-initialize time. This is how Clawboo binds a run’s MCP session to its team/agent; the model can neither widen its visibility nor spoof its identity through tool arguments, because the URL is Clawboo-written config. The other two servers (tasks, tools) take no scoping params.
Memory: visibility scope (parseBoundScope):
| Query param | Binds |
|---|
scopeTeamId | The team whose facts/procedures are visible and the team a save is tagged with |
scopeAgentId | The agent scope (team + agent + global are read inclusively) |
scopeTenantId | The dormant multi-tenant scope |
When none are present the memory session is unbound (legacy model-arg behavior, used by the raw stdio bin / external attach).
TeamChat: room + author identity (anti-spoof) (parseTeamChatBinding):
| Query param | Binds |
|---|
roomTeamId | The team room (team:<teamId> via resolveRoomForTeam) |
postAuthorAgentId | The author identity every post is attributed to |
Both roomTeamId and postAuthorAgentId are required for a bound TeamChat session; if either is missing the session is unbound and identity falls back to tool args. A bound TeamChat server also emits a team_chat_post orchestration event into the observability log on every post.
Event stream
The MCP servers’ SSE channel (the GET route) carries Model Context Protocol JSON-RPC notifications and streamed tool results, not Clawboo-shaped events. The tools each server exposes: list_tasks / create_task / claim_task / add_comment (tasks), memory_save / memory_search / memory_browse (memory), the brokered tool set (tools), and team_chat_post / team_chat_subscribe (teamchat), and their Zod input shapes are catalogued in the MCP tools reference.
Example (raw initialize handshake)
# 1. Initialize — the response carries the mcp-session-id header.
curl -i -X POST http://localhost:18790/api/mcp/tasks \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
# 2. Reuse the returned session id on subsequent calls.
curl -X POST http://localhost:18790/api/mcp/tasks \
-H 'Content-Type: application/json' \
-H 'mcp-session-id: <id-from-step-1>' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
In practice you do not hand-roll this; an MCP client library (or the runtime’s own MCP support) speaks the protocol. Use GET /api/mcp/config to get the attach snippet for your runtime.
GET /api/mcp/config
Emits a copy-pasteable attach snippet so the consuming runtime can register one of Clawboo’s MCP servers. Clawboo “hosts” the connection setup; the snippet differs per runtime and transport. The HTTP base URL is built from the server’s own bound loopback port (loopbackMcpBaseUrl(req), reading req.app.locals.apiPort set at boot), never the client Host header, since a forged Host could otherwise redirect a runtime’s MCP traffic. It falls back to http://127.0.0.1:<CLAWBOO_API_PORT or 18790>. For the stdio transport the bin path uses CLAWBOO_MCP_BIN_DIR.
| Param | Type | Default | Values |
|---|
server | string | tasks | tasks | memory | tools | teamchat |
runtime | string | claude-code | claude-code | codex | openclaw |
transport | string | http | http | stdio |
Responses
400 Bad Request: server is not one of the four known MCP server names:
{ "error": "unknown server: <server>" }
200 OK: the attach config for the requested runtime/server/transport:
{
ok: true
config: {
id: string // 'clawboo-<server>'
transport: 'http' | 'stdio'
snippet: string // a copy-pasteable command or config block
structured: Record<string, unknown> // programmatic form (e.g. Claude Code inline mcpServers)
}
}
The snippet/structured shape branches on runtime and transport; for example claude-code + http yields claude mcp add --transport http clawboo-<server> <url>, while codex + stdio yields a [mcp_servers.clawboo-<server>] TOML block. For openclaw, the snippet targets the top-level mcp.servers key with transport: "streamable-http".
500 Internal Server Error:
Example
curl 'http://localhost:18790/api/mcp/config?runtime=claude-code&server=tasks&transport=http'
Error envelope
Every error response on these routes is the standard envelope { error: string }. On the /api/tools* routes the error string (and, where present, the response body’s JSON-string summaries) is masked through redactValue / redactJsonString so credential-shaped values never leak into a response. The MCP transport’s protocol-level errors (bad/missing session) are JSON-RPC error objects ({ jsonrpc: "2.0", error: { code: -32000, message } }) at HTTP 400; a handler throw before headers are sent is the plain { error } envelope at 500.
See also
- MCP tools reference, the 4 servers, their tools, and Zod input shapes
- Operating: MCP servers as teammates, attach config, transports, scoping in practice
- Memory API, the UI-facing memory REST surface over the same store
- Governance API, budgets, delegation-approval, and the shared approval queue
- Capabilities API, the unified capability inventory that surfaces broker tools
- Runtimes API, a
:id/run attaches to these /api/mcp/* endpoints
- REST API overview