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/integrations →
API 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.
| 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.
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:
| 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:
-- ClickHouse
DESCRIBE TABLE conversations
-- Postgres
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'receivers'
| 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 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 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).
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.