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

# MCP

> Connect Zelto to Claude Code, Codex, Cursor, and other MCP clients.

Zelto exposes a [Model Context Protocol](https://modelcontextprotocol.io/)
server at `https://api.zelto.ai/mcp`. Any MCP-compatible client can connect
and get read access to your agents, conversations, transcripts,
reviews, and findings, plus a small set of write tools for the
bucket-based review workflow.

Most read and write tools mirror a public [REST API](/api-reference/agents/list-agents)
endpoint, so anything you can do over HTTP you can do from inside your
editor. Some tools go beyond the REST surface, though — review queues,
solutions, bucket population, and a raw read-only SQL tool
(`query_database`) are MCP-only. The **Mirrors** column in each table
below tells you which is which (`— (MCP-only)` means there's no REST
equivalent).

## Get an API key

Mint one at
`https://dashboard.zelto.ai/org/<your-slug>/settings/integrations` →
**API Call Upload**. The key is shown **once** — copy it immediately. The
same key works for the [REST API](/api-reference/agents/list-agents) and every MCP client below.

Export it for the snippets below:

```bash theme={null}
export ZELTO_API_KEY=<paste-key>
export ZELTO_BASE_URL=https://api.zelto.ai   # or http://localhost:3000 for local dev
```

## Clients

### Claude Code

Add the server to `~/.claude.json` (create the file if it doesn't
exist):

```json theme={null}
{
  "mcpServers": {
    "zelto": {
      "url": "https://api.zelto.ai/mcp",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
    }
  }
}
```

Restart Claude Code (or run `/mcp` to refresh). The `zelto:` tools
appear in the picker; `zelto:health` returns your org id as a smoke
test.

> The integrations page also shows this snippet pre-filled with the
> current host — go to **Settings → Integrations → AI agent access** and
> open the **Claude** card.

### OpenAI Codex CLI

Codex CLI supports MCP servers via `~/.codex/config.toml`. Add a server
entry under `[mcp_servers.zelto]`:

```toml theme={null}
[mcp_servers.zelto]
url = "https://api.zelto.ai/mcp"
headers = { Authorization = "Bearer YOUR_API_KEY" }
```

Then run `codex` and ask it something that needs your data, e.g.
*"list my last 20 conversations and group failures by agent."* Codex
discovers the `zelto:` tools automatically.

### Cursor

`~/.cursor/mcp.json` accepts the same shape as Claude Code:

```json theme={null}
{
  "mcpServers": {
    "zelto": {
      "url": "https://api.zelto.ai/mcp",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
    }
  }
}
```

Then open Cursor's MCP panel and toggle the `zelto` server on. The
tools show up in the chat sidebar.

### Continue, Zed, and other Streamable-HTTP clients

The server speaks Streamable HTTP, so any client that accepts a URL +
`Authorization` header works. Continue uses `config.yaml`:

```yaml theme={null}
mcpServers:
  - name: zelto
    transport:
      type: http
      url: https://api.zelto.ai/mcp
      headers:
        Authorization: Bearer YOUR_API_KEY
```

Zed uses `~/.config/zed/settings.json` under `context_servers.zelto`
with the same `url` + `headers` shape.

### Quick smoke test (`curl`)

If a client is misbehaving, verify your key and connectivity against the
REST API — the same key authenticates both:

```bash theme={null}
curl -H "Authorization: Bearer $ZELTO_API_KEY" \
  "$ZELTO_BASE_URL/v1/agents"
```

A `200` with a `{ "data": [...] }` page means the server and your key are
good — the problem is in the client config. From inside a connected client,
the `health` tool returns `{ "status": "ok", "orgId": "…" }` as the same check.

## Authentication

Bearer-token. Every request must carry an `Authorization: Bearer <your-key>` header. Requests with no key or an invalid key return
`401` and never reach a tool. Each key is scoped to a single
organization — the org is resolved server-side from the key.

## Tools

### Read tools

| Tool                                 | Inputs                                               | Mirrors                                                         |
| ------------------------------------ | ---------------------------------------------------- | --------------------------------------------------------------- |
| `health`                             | —                                                    | (org-scoped smoke test)                                         |
| `list_agents`                        | `limit?`, `cursor?`                                  | `GET /v1/agents`                                                |
| `get_agent`                          | `id`                                                 | `GET /v1/agents/[id]`                                           |
| `list_conversations`                 | `agentId?`, `limit?`, `cursor?`                      | `GET /v1/conversations`                                         |
| `get_conversation`                   | `id`                                                 | `GET /v1/conversations/[id]`                                    |
| `get_transcript`                     | `conversationId`                                     | `GET /v1/conversations/[id]/transcript`                         |
| `list_reviews`                       | `status?`, `limit?`, `cursor?`                       | `GET /v1/reviews`                                               |
| `get_review`                         | `id`                                                 | `GET /v1/reviews/[id]`                                          |
| `list_buckets`                       | `agentId?`, `limit?`, `cursor?`                      | `GET /v1/buckets`                                               |
| `get_bucket`                         | `id`                                                 | `GET /v1/buckets/[id]`                                          |
| `list_bucket_conversations`          | `bucketId`, `limit?`, `cursor?`                      | `GET /v1/buckets/[id]/conversations`                            |
| `list_findings`                      | `status?`, `priority?`, `tags?`, `limit?`, `cursor?` | `GET /v1/findings`                                              |
| `get_finding`                        | `id`                                                 | `GET /v1/findings/[id]`                                         |
| `list_finding_conversations`         | `findingId`, `limit?`, `cursor?`                     | `GET /v1/findings/[id]/conversations`                           |
| `list_finding_conversation_comments` | `findingId`, `conversationId`                        | `GET /v1/findings/[id]/conversations/[conversationId]/comments` |
| `read_docs`                          | `path?`                                              | — (MCP-only)                                                    |

`read_docs` is MCP-only — call it with no arguments to list every
documentation page (slug + title + description), then pass a `path` (e.g.
`findings` or `integrations/slack`) to read that page's full Markdown.

`status` enums: reviews accept `pending` / `reviewed` / `flagged`;
findings accept `open` / `acknowledged` / `resolved` / `ignored`. Finding `priority`
accepts `none` / `low` / `medium` / `high` / `urgent`.

### Raw SQL (`query_database`)

When the dedicated `list_*` / `get_*` tools can't express what you need —
aggregations, group-bys, ad-hoc joins — `query_database` runs a single
read-only SQL statement against either store and returns
`{ rows, rowCount, columns, truncated }`. It's MCP-only (no REST
equivalent) and annotated `readOnlyHint: true`, `destructiveHint: false`.

<Warning>
  Read-only by construction. Only `SELECT` / `WITH` / `DESCRIBE` /
  `EXPLAIN` / `SHOW` statements run, and exactly one statement per call —
  no DDL, no DML, no semicolon-chained queries. ClickHouse runs with
  `readonly=2`; Postgres wraps the query in `SET TRANSACTION READ ONLY`.
  Every query is automatically scoped to your organization at the engine
  level (you cannot reach another org's data even if you omit a
  `WHERE`), so **do not** add `organization_id` filters yourself.
</Warning>

Inputs:

| Input        | Meaning                                                |
| ------------ | ------------------------------------------------------ |
| `datasource` | `"clickhouse"` or `"postgres"` — which store to query. |
| `sql`        | The single read-only statement.                        |

What each store exposes:

* **`clickhouse`** — call & analytics data: `conversations`,
  `transcripts`, `conversation_analyses`, `analysis_findings`, `agents`,
  `webhooks`. Use it for aggregations over call data. The
  `ReplacingMergeTree` tables (`conversations`, `transcripts`,
  `conversation_analyses`) should use `FINAL` for accurate reads.
* **`postgres`** — app & config data: any table with an
  `organization_id` column — `receivers`, `integrations`,
  `reviews`, `buckets`, `findings`, `agents`, `jobs`, `webhooks`, and
  more. Tables without an `organization_id` column (e.g. raw
  `conversations`) aren't reachable here — query ClickHouse for those, or
  join through a scoped table.

How scoping and limits are enforced:

* **Org scoping** is injected for you — ClickHouse via
  `additional_table_filters`, Postgres by rewriting the SQL AST to add
  `organization_id = <your-org>` to every table reference. You can't
  bypass it by omitting a `WHERE`.
* **Row cap** of \~1,000 rows. Any `LIMIT` higher than that is clamped
  down, and a missing `LIMIT` gets one injected. The response sets
  `truncated: true` when the cap is hit.
* **Timeout** of 30s on ClickHouse queries.

To discover columns before writing a query:

```sql theme={null}
-- ClickHouse
DESCRIBE TABLE conversations

-- Postgres
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'receivers'
```

### Write tools

| Tool                              | Inputs                                                                                             | Mirrors                                                          |
| --------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `create_ai_agent`                 | `name`, `description?`, `systemPrompt`                                                             | `POST /v1/agents` (ai)                                           |
| `create_human_agent`              | `name`, `description?`                                                                             | `POST /v1/agents` (human)                                        |
| `flag_call_for_review`            | `conversationId`, `notes?`                                                                         | upserts `reviews.status = "pending"` for the call                |
| `create_bucket`                   | `agentId`, `name`, `type?`, `filters?`                                                             | `POST /v1/buckets`                                               |
| `add_conversation_to_bucket`      | `bucketId`, `conversationId`                                                                       | `POST /v1/buckets/[id]/conversations`                            |
| `remove_conversation_from_bucket` | `bucketId`, `conversationId`                                                                       | `DELETE /v1/buckets/[id]/conversations/[conversationId]`         |
| `create_finding`                  | `title`, `description?`, `status?`, `priority?`, `tags?`, `findingType?`                           | `POST /v1/findings`                                              |
| `update_finding`                  | `id`, `title?`, `description?`, `status?`, `priority?`, `tags?`, `findingType?`, `assigneeUserId?` | `PATCH /v1/findings/[id]`                                        |
| `add_conversation_to_finding`     | `findingId`, `conversationId`                                                                      | `POST /v1/findings/[id]/conversations`                           |
| `annotate_finding_conversation`   | `findingId`, `conversationId`, `body`, `type?`, `annotationStartMs?`, `annotationEndMs?`           | `POST /v1/findings/[id]/conversations/[conversationId]/comments` |

Idempotent: `add_conversation_to_bucket` and `add_conversation_to_finding`
return the existing row with `alreadyExisted: true` on re-add. The
conversation must belong to the same agent as the bucket. `create_bucket`
accepts only `type: "static"` today. AI agent creation runs the system
prompt through the audit pipeline before insert.

### Review queues

A **queue** is a saved set of filters defining a dynamic collection of
conversations to review. These tools are MCP-only.

| Tool                       | Inputs                                    | Mirrors                   |
| -------------------------- | ----------------------------------------- | ------------------------- |
| `list_queues`              | `limit?`, `cursor?`                       | — (MCP-only)              |
| `get_queue`                | `id`                                      | — (MCP-only)              |
| `list_queue_conversations` | `queueId`, `limit?`, `cursor?`            | — (MCP-only)              |
| `create_queue`             | `name`, `description?`, `filters?`        | — (MCP-only)              |
| `update_queue`             | `id`, `name?`, `description?`, `filters?` | — (MCP-only)              |
| `delete_queue`             | `id`                                      | — (MCP-only, destructive) |

The `filters` object accepts: `agentIds`, `agentType` (`ai` / `human`),
`durationMinSeconds`, `durationMaxSeconds`, `endedReasons`, `dateFrom`
(`YYYY-MM-DD`), `dateTo` (`YYYY-MM-DD`), `alreadyReviewed`, `hasAudio`,
`isInteresting`, `findingTypes`. An empty/omitted `filters` matches every
conversation in the org. On `update_queue`, passing `filters` **replaces
the whole object** — it is not merged with the existing one.

### Solutions

A **solution** is one atomic, agent-scoped suggestion that addresses a
set of findings. Each new solution starts as a **draft** (no workflow
status, hidden from the main list) until a human publishes it. These
tools are MCP-only.

| Tool                           | Inputs                                                       | Mirrors                   |
| ------------------------------ | ------------------------------------------------------------ | ------------------------- |
| `list_solutions`               | `agentId?`, `status?`, `draft?`, `limit?`, `cursor?`         | — (MCP-only)              |
| `get_solution`                 | `id`                                                         | — (MCP-only)              |
| `list_solution_findings`       | `solutionId`                                                 | — (MCP-only)              |
| `create_solution`              | `agentId`, `findingIds`, `title?`, `description?`            | — (MCP-only)              |
| `update_solution`              | `id`, `title?`, `description?`, `status?`, `assigneeUserId?` | — (MCP-only)              |
| `publish_solution`             | `id`                                                         | — (MCP-only)              |
| `discard_solution`             | `id`                                                         | — (MCP-only)              |
| `delete_solution`              | `id`                                                         | — (MCP-only, destructive) |
| `link_finding_to_solution`     | `solutionId`, `findingId`                                    | — (MCP-only)              |
| `unlink_finding_from_solution` | `solutionId`, `findingId`                                    | — (MCP-only)              |

`create_solution` inserts exactly **one** draft per call — never bundle
multiple ideas into one body; call it again for each distinct suggestion.
A published solution's `status` is one of `backlog` / `todo` /
`in_progress` / `done` / `cancelled`. You can't set `status` on a draft:
move it out of draft first with `publish_solution` (→ `backlog`) or
`discard_solution` (→ `cancelled`). Deleting a solution removes its
finding links by cascade but never deletes the underlying findings.

### Reports

A **report** is a free-form analyst write-up authored in the same
rich-text editor as findings and solutions. `prompt` holds the question or
brief; `description` holds the body as a TipTap/ProseMirror JSON document.
`status` tracks the generation lifecycle (`pending` / `generating` /
`completed` / `failed`) — a report you create sits in `pending` unless you
pass a finished body and set `completed`. These tools are MCP-only.

| Tool            | Inputs                                               | Mirrors      |
| --------------- | ---------------------------------------------------- | ------------ |
| `list_reports`  | `status?`, `limit?`, `cursor?`                       | — (MCP-only) |
| `get_report`    | `id`                                                 | — (MCP-only) |
| `create_report` | `title`, `prompt?`, `description?`, `status?`        | — (MCP-only) |
| `update_report` | `id`, `title?`, `prompt?`, `description?`, `status?` | — (MCP-only) |

On `update_report`, passing `description` **replaces the whole body** — it
is not merged with the existing document. Pass `prompt: null` to clear the
ask.

### Bucket population

These tools snapshot conversations into [buckets](/docs/buckets) from queues
or raw filters. They are MCP-only.

| Tool                           | Inputs                   | Mirrors      |
| ------------------------------ | ------------------------ | ------------ |
| `create_bucket_from_queue`     | `queueId`, `namePrefix?` | — (MCP-only) |
| `populate_bucket_from_queue`   | `bucketId`, `queueId`    | — (MCP-only) |
| `populate_bucket_from_filters` | `bucketId`, `filters`    | — (MCP-only) |

Because queues can span multiple agents but buckets are agent-scoped,
`create_bucket_from_queue` creates **one bucket per matched agent** and
returns an array. The `populate_*` tools are idempotent — re-running adds
zero new rows — and matches belonging to a different agent than the
bucket are skipped and counted in `skippedOtherAgentCount`.

### Bucket & finding mutations

The remaining MCP-only mutations, plus the one finding-conversation
unlink that does mirror a REST endpoint.

| Tool                                  | Inputs                                                  | Mirrors                                                                 |
| ------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------- |
| `update_bucket`                       | `id`, `name?`, `type?`, `filters?`                      | — (MCP-only)                                                            |
| `delete_bucket`                       | `id`                                                    | — (MCP-only, destructive)                                               |
| `delete_finding`                      | `id`                                                    | — (MCP-only, destructive)                                               |
| `remove_conversation_from_finding`    | `findingId`, `conversationId`                           | `DELETE /v1/findings/[id]/conversations/[conversationId]` (destructive) |
| `update_finding_conversation_comment` | `id`, `body?`, `annotationStartMs?`, `annotationEndMs?` | — (MCP-only)                                                            |
| `delete_finding_conversation_comment` | `id`                                                    | — (MCP-only, destructive)                                               |

`type` accepts only `"static"`. Every delete cascades its own link rows
(bucket tasks, finding-conversation links, comments) but never deletes
the underlying conversations.

### Retell import

Pull agents and calls directly from your connected
[Retell](/docs/integrations/retell) account(s) into Zelto. Connect a Retell
API key first under **Settings → Integrations**. These tools are MCP-only.

| Tool                          | Inputs                                  | Mirrors      |
| ----------------------------- | --------------------------------------- | ------------ |
| `retell_list_agents`          | `limit?`, `isLatest?`, `paginationKey?` | — (MCP-only) |
| `retell_import_agent`         | `agentId`                               | — (MCP-only) |
| `retell_import_conversations` | `agentId`, `since`                      | — (MCP-only) |

`agentId` for all three tools is the **Retell** `agent_id` — list it with
`retell_list_agents`, which returns every agent in the connected account(s),
each annotated with `alreadyImported` (and `importedAgentId` once imported).
Retell's List Agents API supports only `limit` / `isLatest` / `paginationKey`;
there are no status/date filters there (those live on calls). `retell_import_agent`
**fails if the agent is already imported**. `retell_import_conversations` imports
that agent's calls from `since` (an ISO 8601 timestamp, e.g.
`2026-03-01T00:00:00Z`) until now, in the background — the agent must already be
imported, and re-running with an overlapping window is safe (already-imported
calls are skipped).

### Output shapes & pagination

Tool responses match the REST API exactly. See the
[API reference](/api-reference/agents/list-agents) for the JSON shapes of `Agent`,
`Conversation`, `Transcript`, `Review`, `Bucket`, and `Finding`, plus
the `{ data, nextCursor }` pagination envelope.

`limit` clamps to 200 (default 50). `cursor` is opaque — pass back the
`nextCursor` value from a previous response to fetch the next page.

## Troubleshooting

* **`401` from every tool** — the bearer token is wrong or revoked.
  Test the key directly: `curl -H "Authorization: Bearer
  $ZELTO_API_KEY" https://api.zelto.ai/v1/agents`. If that also
  returns `401`, mint a new key.
* **Tools list is empty in the client** — most clients cache the tool
  list. Restart the client (Claude Code: `/mcp`; Cursor: toggle the
  server off/on; Codex: restart the CLI).
* **`429` rate-limited** — back off and retry. Use the `cursor`
  pagination instead of fetching large pages.
* **Cross-org access denied** — keys are scoped to the org they were
  minted in. Switch organizations in the web app and mint a new key
  from that org's **Settings → Integrations**.
