Skip to main content
Zelto exposes a Model Context Protocol 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 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/integrationsAPI Call Upload. The key is shown once — copy it immediately. The same key works for the REST API and every MCP client below. Export it for the snippets below:
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):
{
  "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]:
[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:
{
  "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:
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:
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

ToolInputsMirrors
health(org-scoped smoke test)
list_agentslimit?, cursor?GET /v1/agents
get_agentidGET /v1/agents/[id]
list_conversationsagentId?, limit?, cursor?GET /v1/conversations
get_conversationidGET /v1/conversations/[id]
get_transcriptconversationIdGET /v1/conversations/[id]/transcript
list_reviewsstatus?, limit?, cursor?GET /v1/reviews
get_reviewidGET /v1/reviews/[id]
list_bucketsagentId?, limit?, cursor?GET /v1/buckets
get_bucketidGET /v1/buckets/[id]
list_bucket_conversationsbucketId, limit?, cursor?GET /v1/buckets/[id]/conversations
list_findingsstatus?, priority?, tags?, limit?, cursor?GET /v1/findings
get_findingidGET /v1/findings/[id]
list_finding_conversationsfindingId, limit?, cursor?GET /v1/findings/[id]/conversations
list_finding_conversation_commentsfindingId, conversationIdGET /v1/findings/[id]/conversations/[conversationId]/comments
read_docspath?— (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.
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.
Inputs:
InputMeaning
datasource"clickhouse" or "postgres" — which store to query.
sqlThe 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:
-- ClickHouse
DESCRIBE TABLE conversations

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

Write tools

ToolInputsMirrors
create_ai_agentname, description?, systemPromptPOST /v1/agents (ai)
create_human_agentname, description?POST /v1/agents (human)
flag_call_for_reviewconversationId, notes?upserts reviews.status = "pending" for the call
create_bucketagentId, name, type?, filters?POST /v1/buckets
add_conversation_to_bucketbucketId, conversationIdPOST /v1/buckets/[id]/conversations
remove_conversation_from_bucketbucketId, conversationIdDELETE /v1/buckets/[id]/conversations/[conversationId]
create_findingtitle, description?, status?, priority?, tags?, findingType?POST /v1/findings
update_findingid, title?, description?, status?, priority?, tags?, findingType?, assigneeUserId?PATCH /v1/findings/[id]
add_conversation_to_findingfindingId, conversationIdPOST /v1/findings/[id]/conversations
annotate_finding_conversationfindingId, 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.
ToolInputsMirrors
list_queueslimit?, cursor?— (MCP-only)
get_queueid— (MCP-only)
list_queue_conversationsqueueId, limit?, cursor?— (MCP-only)
create_queuename, description?, filters?— (MCP-only)
update_queueid, name?, description?, filters?— (MCP-only)
delete_queueid— (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.
ToolInputsMirrors
list_solutionsagentId?, status?, draft?, limit?, cursor?— (MCP-only)
get_solutionid— (MCP-only)
list_solution_findingssolutionId— (MCP-only)
create_solutionagentId, findingIds, title?, description?— (MCP-only)
update_solutionid, title?, description?, status?, assigneeUserId?— (MCP-only)
publish_solutionid— (MCP-only)
discard_solutionid— (MCP-only)
delete_solutionid— (MCP-only, destructive)
link_finding_to_solutionsolutionId, findingId— (MCP-only)
unlink_finding_from_solutionsolutionId, 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.
ToolInputsMirrors
list_reportsstatus?, limit?, cursor?— (MCP-only)
get_reportid— (MCP-only)
create_reporttitle, prompt?, description?, status?— (MCP-only)
update_reportid, 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 from queues or raw filters. They are MCP-only.
ToolInputsMirrors
create_bucket_from_queuequeueId, namePrefix?— (MCP-only)
populate_bucket_from_queuebucketId, queueId— (MCP-only)
populate_bucket_from_filtersbucketId, 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.
ToolInputsMirrors
update_bucketid, name?, type?, filters?— (MCP-only)
delete_bucketid— (MCP-only, destructive)
delete_findingid— (MCP-only, destructive)
remove_conversation_from_findingfindingId, conversationIdDELETE /v1/findings/[id]/conversations/[conversationId] (destructive)
update_finding_conversation_commentid, body?, annotationStartMs?, annotationEndMs?— (MCP-only)
delete_finding_conversation_commentid— (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 account(s) into Zelto. Connect a Retell API key first under Settings → Integrations. These tools are MCP-only.
ToolInputsMirrors
retell_list_agentslimit?, isLatest?, paginationKey?— (MCP-only)
retell_import_agentagentId— (MCP-only)
retell_import_conversationsagentId, 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 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.