MCP server and clients
Connect Claude Code, Claude Desktop, Cursor, VS Code, Codex or any MCP client to your workspace, and the full tool reference.
Sondo speaks the Model Context Protocol, so an AI client can work with your workspace from wherever you already are: read the conversations your widget had overnight, reply to one as yourself, add a page to the Knowledge Base, or change what the assistant is allowed to say.
The one endpoint every client connects to is https://usesondo.com/api/mcp.
The tools cover the work the dashboard does day to day: conversations, the Knowledge Base, connected sites, the workspace and widget settings, the API keys and the model spend. Billing, creating a workspace, issuing a key and the outbound webhook stay in the dashboard.
Nothing here takes a workspace argument. The key you connect with decides which workspace you are in, and it cannot reach another one.
Get a key first
Every client authenticates with its own API key from the Integrations page in your dashboard. It
starts with sondo_sk_, it is shown once, and you can revoke it without touching your other
clients. See API keys and plans for how to issue one.
There is no OAuth. The key travels as a bearer token on the Authorization header, which is why
every command below carries one.
Connect your client
Replace the placeholder key in each command with your own.
Claude Code
Run this once:
claude mcp add --scope user --transport http sondo https://usesondo.com/api/mcp --header "Authorization: Bearer sondo_sk_…"Issue the key under Settings > Integrations first. It is shown once.
Claude Desktop
Settings → Developer → Edit Config (claude_desktop_config.json), then restart the app.
{
"mcpServers": {
"sondo": {
"type": "http",
"url": "https://usesondo.com/api/mcp",
"headers": { "Authorization": "Bearer sondo_sk_…" }
}
}
}Cursor
Add this to your Cursor MCP settings file:
{
"mcpServers": {
"sondo": {
"type": "http",
"url": "https://usesondo.com/api/mcp",
"headers": { "Authorization": "Bearer sondo_sk_…" }
}
}
}Paste your key from Settings > Integrations, then enable Sondo in Cursor.
VS Code
Add this to .vscode/mcp.json:
{
"mcpServers": {
"sondo": {
"type": "http",
"url": "https://usesondo.com/api/mcp",
"headers": { "Authorization": "Bearer sondo_sk_…" }
}
}
}Paste your key from Settings > Integrations, then start the server.
Codex CLI
Run this once:
codex mcp add sondo --url https://usesondo.com/api/mcp --bearer-token-env-var SONDO_API_KEYExport SONDO_API_KEY with your key from Settings > Integrations, then start codex.
Codex reads that environment variable when it starts, not when you export it, so export it and then start Codex, in that order.
Gemini CLI
Run this once:
gemini mcp add --transport http --header "Authorization: Bearer sondo_sk_…" sondo https://usesondo.com/api/mcpIssue the key under Settings > Integrations first. It is shown once.
Anything else
Point it at the endpoint and send the key on this header:
https://usesondo.com/api/mcp
Authorization: Bearer sondo_sk_…Issue the key under Settings > Integrations. It is shown once.
Chat products that connect through their own interface, including Claude, ChatGPT, Grok and Meta Muse, need the same two things: the endpoint, and the key on that header. Where their interface puts those fields is their business, and it changes often enough that this page will not guess.
Check that it worked
Ask your client to use Sondo's ping tool by name. It answers with the workspace it is connected
to, which is the fastest way to tell a working key from a working connection to the wrong workspace.
Naming the tool matters. Asked loosely, a coding agent will often reach for curl instead, which
proves only that the endpoint is reachable.
Tool reference
Generated from the server, so it cannot drift from what your client will actually find.
An unknown id, or one from another workspace, comes back as "not found" rather than an error about permission, because a permission error would confirm that the thing exists.
Sondo exposes 27 tools.
Conversations
Read what visitors asked, reply as the operator, and keep the list tidy.
conversation_reply
Write your own reply into a conversation, as the operator. The visitor sees it within a few seconds. This is not a takeover: the assistant still answers their next message, having read your reply. Pause it first with conversation_set if you want the thread.
| Argument | Type | Notes |
|---|---|---|
id | uuid | |
content | string | 1 to 4,000 characters |
conversation_set
Set a conversation's star, block its visitor, or pause the assistant in it. Pass at least one of starred, visitorBlocked or aiPaused. Read and archived belong to set_read and set_archived.
| Argument | Type | Notes |
|---|---|---|
id | uuid | |
starred (optional) | boolean | |
visitorBlocked (optional) | boolean | |
blockMessage (optional) | string | What a blocked visitor is told. Only meaningful when blocking; unblocking clears it. up to 300 characters |
aiPaused (optional) | boolean | Paused, the assistant stops answering until you resume it. Manual only |
get_conversation
The full transcript of one conversation, plus visitor context, signals and model cost. Ids come from list_conversations.
| Argument | Type | Notes |
|---|---|---|
id | uuid |
list_conversations
Overview of this workspace's conversations: the total, then the 10 most recent by activity (id, title, unread). Start here and stay zoomed out. Pass detail:'summaries' for a gist and triage signals per conversation, or get_conversation for one full transcript.
| Argument | Type | Notes |
|---|---|---|
view (optional) | one of all, unread, archived | all (default, excludes archived) | unread | archived |
q (optional) | string | Search titles, messages and visitor emails |
detail (optional) | one of titles, summaries | Default titles |
page (optional) | integer | at least 1 |
set_archived
Archive one conversation (out of the inbox) or restore it. Archiving is reversible and deletes nothing.
| Argument | Type | Notes |
|---|---|---|
id | uuid | |
archived (optional) | boolean | Default true |
set_read
Mark one conversation read/unread by id, or pass id:'all' to clear the whole inbox. 'all' only marks READ; there is no bulk unread.
| Argument | Type | Notes |
|---|---|---|
id | uuid or all | |
read (optional) | boolean | Default true |
unread_count
How many conversations are unread (excludes archived). Takes no arguments.
Takes no arguments.
Knowledge Base
Everything the Add source form does, plus editing and removing what is already there.
kb_add
Add a document by pasting its text. Chunked and embedded at once, so the assistant can use it on the next message. Costs pages from the plan's allowance.
| Argument | Type | Notes |
|---|---|---|
title | string | 1 to 200 characters |
text | string | 1 to 200,000 characters |
kb_add_file
Add a text-based PDF, Markdown or plain-text file, base64-encoded. Up to 2 MB decoded. A scanned, image-only PDF has no text to extract and is refused.
| Argument | Type | Notes |
|---|---|---|
filename | string | Used to pick the extractor and the title. 1 to 255 characters |
contentBase64 | string | at least 1 characters |
title (optional) | string | 1 to 200 characters |
kb_add_url
Fetch a public page and add its main content as a document. The address is checked against private networks before anything is fetched. Title defaults to the page's own.
| Argument | Type | Notes |
|---|---|---|
url | URL | up to 2,048 characters |
title (optional) | string | 1 to 200 characters |
kb_get
One document's stored text, with its title, source and size in pages. Ids come from kb_list. This is the text the assistant retrieves from.
| Argument | Type | Notes |
|---|---|---|
id | uuid |
kb_list
This workspace's Knowledge Base: how many pages of the plan's allowance are used, then one line per document (id, title, where it came from, status, chunks). No text. Use kb_get for one document's content.
| Argument | Type | Notes |
|---|---|---|
page (optional) | integer | at least 1 |
kb_refresh
Bring one document up to date. A document added from a URL is fetched again from that address, overwriting any hand edits. Every other document is re-chunked and re-embedded from its stored text. Not for a site's posts, which their site syncs.
| Argument | Type | Notes |
|---|---|---|
id | uuid |
kb_remove
Delete a document and its chunks. The assistant stops retrieving from it immediately, and past answers that cited it read as 'source removed'. A site's posts are removed by removing the site.
| Argument | Type | Notes |
|---|---|---|
id | uuid |
kb_update
Replace a document's title, its text, or both. Re-chunks and re-embeds. Omitted fields keep what is stored. A document synced from a connected site cannot be edited here.
| Argument | Type | Notes |
|---|---|---|
id | uuid | |
title (optional) | string | 1 to 200 characters |
text (optional) | string | 1 to 200,000 characters |
Connected sites
Connect a blog, sync it, and disconnect it.
site_connect
Connect a blog so its posts sync into the Knowledge Base. Posts are found from the site's sitemap, feed or llms.txt; pass manualIndexUrl (same host) if it has none. The first sync starts in the background. Read site_list for its progress.
| Argument | Type | Notes |
|---|---|---|
rootUrl | URL | up to 2,048 characters |
pathPrefix (optional) | string matching ^\/.* | up to 200 characters |
manualIndexUrl (optional) | URL | up to 2,048 characters |
site_list
Every site connected to this workspace: where it is, how its posts were found, how many it holds, and what its last sync did. Read this after site_connect or site_sync to see how the run ended.
Takes no arguments.
site_remove
Disconnect a site and delete every post it synced. The assistant stops retrieving from them at once. Nothing else in the Knowledge Base is touched.
| Argument | Type | Notes |
|---|---|---|
id | uuid |
site_sync
Re-run one site's sync: fetch its index, then re-embed the posts that changed. Runs in the background and returns at once. Read site_list for progress and the result.
| Argument | Type | Notes |
|---|---|---|
id | uuid |
Settings and the widget
The workspace's own configuration, and how the chat looks and behaves.
settings_get
Everything settings_update can change, as it is now, plus the plan and how much of its Knowledge Base allowance is used. The OpenRouter key is reported as configured or not, never as its value.
Takes no arguments.
settings_update
Change any of the workspace's settings. Pass only the keys you want changed; the rest are left alone. Validation is all or nothing: if any key is rejected, nothing at all is written. guardrails and responseStyle are whole objects, and postSuggestions and postCards must be sent together.
| Argument | Type | Notes |
|---|---|---|
name (optional) | string | The workspace's display name. 1 to 64 characters |
websiteUrl (optional) | string | The workspace's own website as a full http(s) URL, or "" to clear it. Its icon is fetched too. up to 2,048 characters |
timezone (optional) | string, or "" to clear it | An IANA name such as "Europe/Amsterdam", or "" for Auto. 1 to 64 characters |
persona (optional) | string | How the assistant should behave. up to 8,000 characters |
guardrails (optional) | object with allowCode (required), allowCreative (required), allowGeneralQA (required) | All three flags, always. Which deliverables the assistant may produce |
guardrails.allowCode | boolean | |
guardrails.allowCreative | boolean | |
guardrails.allowGeneralQA | boolean | |
responseStyle (optional) | object with enabled (required), level (required) | Both fields, always. level 1 is briefest, 5 is fullest |
responseStyle.enabled | boolean | |
responseStyle.level | one of 1, 2, 3, 4, 5 | |
resumeWindowHours (optional) | number | How long a returning visitor picks up where they left off. One of: 1, 6, 24, 48, 168, 336 (hours) |
postSuggestions (optional) | one of never, when_relevant, only_when_asked | Send with postCards |
postCards (optional) | boolean | Send with postSuggestions |
models (optional) | object with chat, summary, extract | A provider/model slug pins a task; null clears it back to the default |
models.chat (optional) | string matching ^[\w.-]+\/[\w.:-]+$, or null | 1 to 120 characters |
models.summary (optional) | string matching ^[\w.-]+\/[\w.:-]+$, or null | 1 to 120 characters |
models.extract (optional) | string matching ^[\w.-]+\/[\w.:-]+$, or null | 1 to 120 characters |
openrouterKey (optional) | string, or "" to clear it | Write-only; it is never read back. "" REMOVES the key, which pauses the assistant for every visitor until a new one is saved. There is no server key to fall back to. 20 to 300 characters |
widget_get
What visitors see and where the widget may load: its name, header, welcome message, suggested questions, booking link, showcase cards, colors, allowed origins, and the embed snippet to paste into a site.
Takes no arguments.
widget_update
Change any of the widget's settings. Pass only what you want changed; everything else keeps its current value. An array (starters, showcase, allowedOrigins) is replaced whole, so pass the full list, and [] really does clear it.
| Argument | Type | Notes |
|---|---|---|
name (optional) | string | 1 to 80 characters |
welcomeMessage (optional) | string | up to 500 characters |
bookingUrl (optional) | URL, or "" to clear it | up to 2,048 characters |
starters (optional) | array (up to 8) of string | |
headerTitle (optional) | string | up to 80 characters |
showHeader (optional) | boolean | |
chatOrb (optional) | boolean | |
colors (optional) | object with light (required), dark (required) | Both modes, each #rrggbb or "" |
colors.light | object with primary (required), userBubble (required), userBubbleForeground (required), assistantBubble (required), assistantBubbleForeground (required) | |
colors.dark | object with primary (required), userBubble (required), userBubbleForeground (required), assistantBubble (required), assistantBubbleForeground (required) | |
shader (optional) | string | The launcher orb's shader key. 1 to 64 characters |
emailCapture (optional) | object with contactReason (required), gapReason (required) | |
emailCapture.contactReason | string | up to 300 characters |
emailCapture.gapReason | string | up to 300 characters |
contactEmail (optional) | email, or "" to clear it | up to 200 characters |
showcase (optional) | array (up to 6) of object with title (required), description (required), imageUrl (required), url (required) | |
showcase[].title | string | 1 to 80 characters |
showcase[].description | string | up to 140 characters |
showcase[].imageUrl | URL or string matching ^\/(?!\/)\S*$, or "" to clear it | up to 2,048 characters |
showcase[].url | URL, or "" to clear it | up to 2,048 characters |
allowedOrigins (optional) | array (up to 20) of string | Sites the widget may be embedded on. Replaces the whole list; [] blocks every external embed |
resumeWindowHours (optional) | number |
API keys
List and revoke the keys other clients connect with.
key_list
Every API key for this workspace, oldest first: label, when it was made, when it was last used, and whether it is revoked. Key values are never stored and never shown. Issue a new key from the dashboard's Integrations page.
Takes no arguments.
key_revoke
Revoke one API key by id, from key_list. The row survives as a record, with its revoke date. Revoking the key you are calling with is allowed, and ends this session's access immediately.
| Argument | Type | Notes |
|---|---|---|
id | uuid |
Everything else
A connectivity check, and what this workspace has spent.
ping
Check the Sondo connection and report which workspace this API key opens. Takes no arguments.
Takes no arguments.
usage_summary
Model spend in USD, bucketed by day (last 30), week (12) or month (12). UTC calendar days.
| Argument | Type | Notes |
|---|---|---|
unit (optional) | one of day, week, month |