Skip to main content
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

MethodPathSummaryStream?
GET/api/toolsList builtin tools + per-tool availability verdictNo
GET/api/tools/approvalsThe pending tool-approval queueNo
POST/api/tools/approvals/:id/resolveResolve a pending approval (allow/deny)No
GET/api/tools/auditThe before/after tool-call audit logNo
GET/api/mcp/configEmit a per-runtime, per-server attach snippetNo
POST/api/mcp/tasksMCP JSON-RPC POST (tasks server)Streamable HTTP
GET/api/mcp/tasksMCP SSE stream (tasks session)SSE
DELETE/api/mcp/tasksMCP session teardown (tasks)No
POST/api/mcp/memoryMCP JSON-RPC POST (memory server)Streamable HTTP
GET/api/mcp/memoryMCP SSE stream (memory session)SSE
DELETE/api/mcp/memoryMCP session teardown (memory)No
POST/api/mcp/toolsMCP JSON-RPC POST (tools server)Streamable HTTP
GET/api/mcp/toolsMCP SSE stream (tools session)SSE
DELETE/api/mcp/toolsMCP session teardown (tools)No
POST/api/mcp/teamchatMCP JSON-RPC POST (teamchat server)Streamable HTTP
GET/api/mcp/teamchatMCP SSE stream (teamchat session)SSE
DELETE/api/mcp/teamchatMCP session teardown (teamchat)No

GET /api/tools

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

GET /api/tools/approvals

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"}'

GET /api/tools/audit

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.
  • Query params:
ParamTypeDefaultMeaning
toolNamestring(all tools)Filter to one tool’s audit rows
limitnumber100Max rows returned
  • Request body: none.

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'

MCP transport: /api/mcp/{tasks,memory,tools,teamchat}

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:
MethodRole
POSTJSON-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.
GETThe server-to-client SSE stream for the established session (server notifications + streamed results).
DELETETears 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 paramBinds
scopeTeamIdThe team whose facts/procedures are visible and the team a save is tagged with
scopeAgentIdThe agent scope (team + agent + global are read inclusively)
scopeTenantIdThe 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 paramBinds
roomTeamIdThe team room (team:<teamId> via resolveRoomForTeam)
postAuthorAgentIdThe 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.
  • Query params:
ParamTypeDefaultValues
serverstringtaskstasks | memory | tools | teamchat
runtimestringclaude-codeclaude-code | codex | openclaw
transportstringhttphttp | stdio
  • Request body: none.

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:
{ "error": "<message>" }

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