> ## Documentation Index
> Fetch the complete documentation index at: https://docs.claw.boo/llms.txt
> Use this file to discover all available pages before exploring further.

# Tools & MCP API

> REST reference for the tools broker (list, approvals, audit) and the in-process Streamable-HTTP MCP transport plus attach-config endpoint.

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.

<Note>
  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](/reference/mcp-tools).
</Note>

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              |

***

## `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:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  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:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "<masked message>" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
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:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  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`**:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "<masked message>" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
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`:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  decision: 'allow_once' | 'allow_always' | 'deny' // required
}
```

### Responses

**`400 Bad Request`**: the body fails Zod validation (missing/invalid `decision`):

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  "error": "invalid body",
  "details": {/* zod flatten() */}
}
```

**`404 Not Found`**: no row with that id exists at all (`getApproval` returns nothing):

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "approval not found" }
```

**`200 OK`**: the approval was resolved; the updated row is returned:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  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
  }
}
```

<Note>
  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.
</Note>

**`500 Internal Server Error`**:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "<masked message>" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
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**:

| Param      | Type   | Default     | Meaning                         |
| ---------- | ------ | ----------- | ------------------------------- |
| `toolName` | string | (all tools) | Filter to one tool's audit rows |
| `limit`    | number | `100`       | Max rows returned               |

* **Request body**: none.

### Responses

**`200 OK`**: the audit rows:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  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`**:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "<masked message>" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
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:

| 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.                                                                                                                                                                                                         |

<Info>
  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>" }`.
</Info>

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](/reference/mcp-tools).

### Example (raw initialize handshake)

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
# 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**:

| Param       | Type   | Default       | Values                                       |
| ----------- | ------ | ------------- | -------------------------------------------- |
| `server`    | string | `tasks`       | `tasks` \| `memory` \| `tools` \| `teamchat` |
| `runtime`   | string | `claude-code` | `claude-code` \| `codex` \| `openclaw`       |
| `transport` | string | `http`        | `http` \| `stdio`                            |

* **Request body**: none.

### Responses

**`400 Bad Request`**: `server` is not one of the four known MCP server names:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "unknown server: <server>" }
```

**`200 OK`**: the attach config for the requested runtime/server/transport:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  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`**:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "<message>" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
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](/reference/mcp-tools), the 4 servers, their tools, and Zod input shapes
* [Operating: MCP servers as teammates](/operating/mcp-servers), attach config, transports, scoping in practice
* [Memory API](/reference/rest-api/memory), the UI-facing memory REST surface over the same store
* [Governance API](/reference/rest-api/governance), budgets, delegation-approval, and the shared approval queue
* [Capabilities API](/reference/rest-api/capabilities), the unified capability inventory that surfaces broker tools
* [Runtimes API](/reference/rest-api/runtimes), a `:id/run` attaches to these `/api/mcp/*` endpoints
* [REST API overview](/reference/rest-api/index)
