MCP server
DialCru serves its data to AI assistants over the Model Context Protocol. Connect an MCP client as yourself, by signing in (OAuth), or with an API key, and the client's own model can answer questions about your calls — no copying, no export.
The server is read-only. Anything that changes data — creating a campaign, tagging contacts, the do-not-call list — needs a person to approve it, and that approval cannot be enforced inside somebody else's client, so it stays in the dashboard's own assistant.
Connecting
| Setting | Value |
|---|---|
| URL | https://<your DialCru host>/api/mcp |
| Transport | Streamable HTTP |
| Auth | OAuth 2.1, or authorization: Bearer <key> with an API key |
The server is stateless: every request carries its credential, no session id is issued, and only POST is accepted (GET and DELETE answer 405). Responses are JSON, never a server-sent event stream.
As yourself (OAuth)
Add the URL as a custom connector in Claude, ChatGPT or any client that speaks MCP's authorization flow. The client registers itself, opens a browser to sign you in, and asks you to allow it on a consent page that names the client, where it will send you back to, what it may read, and which organisation it works in. Choosing an organisation there also makes it your active one in DialCru. The connection then reads only that organisation, and only what your role can read in the dashboard — contact details are masked exactly as they are for you.
Only roles that can use the dashboard's assistant (owner and admin) can connect. Your membership and role are checked on every request: leave the organisation or lose the role and the connection stops working, with nothing to revoke. An access token lasts an hour; the client refreshes it on its own.
Settings → MCP lists every app you have connected, in which organisation, and when it was last active. Disconnect stops it at once: its next request answers 401 even with an access token that has not expired, and its refresh token no longer works. To move an app to another organisation, disconnect it and connect it again.
For a client that discovers the server itself: a request without a credential answers 401 with www-authenticate: Bearer resource_metadata="https://<host>/.well-known/oauth-protected-resource/api/mcp" (RFC 9728), which names the authorization server, whose metadata is at https://<host>/.well-known/oauth-authorization-server (RFC 8414). Registration is open (RFC 7591), PKCE (S256) is required, and the authorization request must name the server as its resource (RFC 8707). The scope is mcp:read, plus offline_access for a refresh token.
A request under OAuth is rate-limited per person (120 a minute) and logged under Settings → API keys beside key requests, named OAuth · <your email> · <client id>.
With an API key
A request is an ordinary public-API request: it is authenticated, counted against the key's rate limit, and logged under Settings → API keys like any other. Errors before the MCP layer — a missing key, an exhausted limit — use the API's error envelope.
Tools
Over OAuth, a tool is listed when your role can read what it reads in the dashboard, so a connection gets every read tool the dashboard's assistant offers you — more than the table below.
With a key, a tool is listed only when the key holds its scope — except read_docs, which reads nothing of your organisation's and needs no scope, so every connection lists at least that one.
Each kind of record has a list_ tool and, where a record stands on its own, a get_ tool; where counting or comparing records is the question, it has a summarise_ tool too:
- A
list_tool takes the same filters as that record's list page in Call Center, plussort,limit(at most 25) andoffset. It answers up to 25 rows,total(how many matched, so a count needs onlylimit: 1) andsee_all, a link to the page with the same filters. - A
get_tool takes up to 10 record ids (call_ids,contact_ids, …;get_transcripttakes up to 5call_ids) and answersrows, the records found, with the same fields its list rows carry, andnot_found, the ids that named none you may read. It is an error only when none was found. - Both take
columns, a list of the fields to return.idandlinkalways come back. Without it, a get returns every field; a list leaves out its long ones (a call's notes, a script's steps, a webhook's bodies), whichcolumnsnames to include. - A
summarise_tool takes the same filters as itslist_tool, plusgroup_by(up to two dimensions, such asagentandday),measures,sortandlimit(at most 60 groups). It answers one row per group with each measure,groups(how many there were),overall(the measures for everything that matched) andsee_all. A row whose groups the list can filter by carries alinkto those records. Rates are percentages (0–100, named_pct), durations are seconds, and days and hours are in the organisation's time zone. Grouping byday,weekormonthneeds the tool's range (placed,queued,enrolled), and a series of days has a row for every day, zero when nothing happened.sorttakes a measure (-sales) or the time dimension grouped by (-dayis newest first). A range with more than 60 days is refused unlesssortnames a measure andlimitis set: then every day is ranked and only the toplimitreturned ("the best five days this year"). Achartargument is ignored over MCP. versuscompares with an earlier range, on asummarise_tool with a range:"previous"for as many days just before it, or its ownfromandto. Each row then also carriesbefore(its measures over that range),change(now minus before; a rate's change is in percentage points) andchange_pct(a count, duration or amount's change as a percent of before), andversusholds the same foroverall. A group with nothing in the earlier range is compared with zero.sorttakes a measure followed by_change, so-sales_changeputs the biggest rise first. It needs the range, and cannot group byday,weekormonth, whose groups differ between the ranges.summarise_callsmeasures calls as the Reports dashboard does: practice calls are left out. Itsleads,reported,won,report_rate_pct,close_rate_pctandrevenue_zarare what your CRM reported through lead results:wonis a sale the CRM reported, wheresalesis a lead sent on. Grouping bylead_resultsplits calls intowon,no_sale,openandunreported;caller_idis the number an outbound call showed;script_versionthe published script the agent was shown;agent_kindpeople or AI agents;ended_bywho ended the call (customer,agentorplatform);talkhow long it was connected, in bands fromnot_connectedandunder_10sto3m_plus. Each of these is also alist_callsfilter. Itscost_zar,cost_per_dial_zar,cost_per_rpc_zar,cost_per_lead_zar(per lead sent),cost_per_sale_zar(per sale the CRM reported) andmargin_zar(revenue less spend) count what your credit was charged for those calls — minutes, voice AI, transcription, QA review and disposition — so they leave out what no call carries, such as number rental and the assistant.talk_p50_sec,talk_p90_sec,acw_p50_secandacw_p90_secare the median and 90th percentile beside the averages.last_stepgroups calls by where their walk through the script stopped (Script · step, orend of script); people's presses are recorded and an AI agent's are not, so AI calls group apart asnot recorded (AI call).lead_agegroups campaign dials by how old the lead was when dialled (under_5mto30d_plus), youngest first: the decay curve.talk_sharegroups by the agent's share of the talking (under_40to75_plus), andagent_talk_share_pct,avg_dead_air_sec,avg_longest_dead_air_sec,avg_overtalkandavg_longest_monologue_secaverage the figures a call's page shows, overspeech_calls: calls whose own recording was transcribed in stereo.last_step,lead_age,talk_shareandobjectionare alsolist_callsfilters.summarise_script_stepsfollows people's calls on one script (script_id, optionally oneversion) underlist_calls' filters: each step'sreached,stopped_hereandstop_rate_pct,handed_on_pctwith its interval,median_sec_on_step, andways_out(each button, jump or back, and how many took it). A step is its key across versions.summarise_objectionscounts what customers pushed back with — price, not interested, already covered, trust and the rest — as each call's latest AI review read it, byobjection,handled,resolved,rebuttal(the script objection the agent answered with) and the call's agent, campaign, outcome, script version or day.reviewed_callsis how many of the filter's calls were read at all; an AI review from before objections were read has none.summarise_webhook_deliveriescounts every endpoint's deliveries, a deleted endpoint's included, over the days they were queued. The endpoint page's test sends are left out unlessinclude_testsis set. Itsfailure_rate_pctis over settled deliveries only (delivered or given up). It takeslist_webhook_deliveries' filters.summarise_campaign_contactscounts campaign rosters as a campaign's progress bar does: a claim that has lapsed counts as available. Archived campaigns (statuscompleted) are left out unlessstatus,show_completedorcampaign_idnames them. Itsoutcomeis each contact's latest outcome on that campaign, so a contact is counted once however many calls it took.enrollednarrows to the contacts that joined on those days, and groups by theday,week,month,hourorweekdaythey joined. Its speed to lead ismedian_to_first_dial_secandavg_to_first_dial_sec(joining to the first call placed),first_dial_within_5m_pctandmedian_to_first_answer_sec. Grouped by theweekormonthcontacts joined,reached_1d_pct,reached_7d_pct,reached_30d_pct,lead_7d_pctandlead_30d_pctare each cohort's curve, counted only over contacts old enough for that age.settled_per_day,days_to_finishanddays_to_try_allare the pace over the last 7 days and how long what is left takes at it.
get_callandget_campaigncarry more. A call'sscriptis its walk through its script — each step left and how, where it stopped, the answers captured and each disclosure owed — andlast_stepis where it stopped. A campaign'sprojectionis when it will finish at its last 7 days' pace (finishes_on, a local date, null when nothing settled that week) and when every contact will have had a first dial (all_tried_by).- Rates say how sure they are. A rate measured over a sample (a connect rate, a close rate, an auto-fail rate) carries
interval, its 95% interval in its own unit (overall_intervalfor everything that matched), and, when grouped,vs_rest: whether the group's rate ishigherorlowerthan everything else that matched beyond chance,samewhen the difference could be noise, ortoo_fewwhen there is too little to tell, corrected for how many rows are compared at once. Withversus,significantsays the same of each rate's and each count's move between the ranges; a count is compared per day, so ranges of different lengths are fair. list_webhook_deliveriessearches every endpoint's deliveries, or one's withwebhook_id: bydelivery_status,event, the daysqueued, the endpoint's laststatus_code, thecall_idthe event was about, and text in the body sent (sent) or in the endpoint's last answer (answered), any case. Test sends are left out unlessinclude_testsis set. Before 2026-10-29 it read one endpoint's log only, and its status filter was namedstatus.
What relates to a record is a filter on the other's list: a contact's calls are list_calls with contact, a campaign's contacts list_campaign_contacts.
| Tool | Scope | What it returns |
|---|---|---|
list_calls | call:read | Calls by number, SID, agent or people/AI, campaign, contact, status, outcome, day and hour placed, and what a summary groups by |
get_call | call:read | Calls by id: timing, agent, campaign, outcome, notes, legs, who hung up and what the CRM reported |
summarise_calls | call:read | Calls counted, measured and costed by agent, people/AI, campaign, outcome, status, direction, caller ID, script version, lead result, who ended it, talk time or time |
summarise_script_steps | call:read | One script's steps over people's calls: how many reached and stopped on each, handed a lead on after it, how long it held them, and where each press led |
list_transcripts | transcript:read | Calls whose transcripts match a full-text search, by agent, campaign, outcome, day placed and who said it, with the matching snippets |
get_transcript | transcript:read | What was said on calls, turn by turn, by call_ids |
summarise_objections | call:read | What customers pushed back with on AI-reviewed calls, by objection, rebuttal, agent, campaign or outcome: how often answered and overcome, and whether the call still sold |
list_contacts | contact:read | Contacts by name, email, number, status, tag and custom field |
get_contact | contact:read | Contacts by id: details, tags, custom fields and do-not-call status |
list_contact_tags | contact:read | The organisation's contact tags and how many contacts carry each |
get_contact_tag | contact:read | Contact tags by id |
list_campaigns | campaign:read | Campaigns by name and status, with their progress |
get_campaign | campaign:read | Campaigns by id: status, progress, goal, calling window, script and caller ID |
list_campaign_contacts | campaign:read | One campaign's contacts by name, number, tag, dial state and latest outcome |
get_campaign_contact | campaign:read | Campaign contacts by id: attempts, last dial, latest outcome and notes, holder |
summarise_campaign_contacts | campaign:read | Roster contacts counted by campaign, dial state, attempts, latest outcome or when they joined, with speed to lead |
list_numbers | number:read | The organisation's phone numbers, by number or label and health |
get_number | number:read | Phone numbers by id |
list_webhooks | webhook:read | Webhook endpoints by name or URL, status, event and campaign |
get_webhook | webhook:read | Webhook endpoints by id, with their body templates (never their signing secrets) |
list_webhook_deliveries | webhook:read | Deliveries across endpoints by status, event, day, HTTP code, call, and text sent or answered |
get_webhook_delivery | webhook:read | Deliveries by id: their attempts, the body sent and the endpoint's answer and headers |
summarise_webhook_deliveries | webhook:read | Deliveries across endpoints, counted by endpoint, event, status, HTTP code or time |
list_usage_events | usage:read | What the organisation was charged, one charge per row, by meter, day, campaign, contact, number and call |
summarise_usage_events | usage:read | Spend summed by meter, AI feature, agent, campaign, contact, call, number or time, under the same filters |
The do-not-call list is not readable with a key, as the REST API's suppressions are write-only, so a key cannot test numbers against it.
Source: ORG_TOOLS in src/lib/server/services/assistant/assistant_registry.ts, filtered to effect: "read" with a non-null scope. Tool names are stable; a renamed tool is a new tool.
With a key, phone numbers are returned in full, as the REST API returns them.
Documentation
read_docs reads one page of this documentation — the REST API, webhooks and this page — so a client's model can answer how something works (a scope, an error code, how to verify a signature) from the page rather than from memory.
It takes page, the page's path in this documentation, such as api/scopes.md; the tool's description lists every page with its title. It answers the page as a single markdown text block, with no structuredContent and no resource_link. A link in it to another page is rewritten to that page's page, to read with the same tool. A page that is not one is a tool error.
Source: AssistantDocs in src/lib/server/services/assistant/assistant_docs.ts. The tool's name is stable.
Results
Each result but read_docs's carries the same JSON twice:
structuredContent, the JSON as an object.- A text block holding the JSON as a string, for clients that predate structured content.
No tool declares an outputSchema; read a result's fields from the tool's description and the data itself.
A refusal — no such call, a range that is too long — is a tool error (isError: true) whose JSON, in both places, is { "error": "<sentence>", "code": "<code>" }, with codes from the API's error list.
A successful result also carries a resource_link to the page in Call Center that shows the same data — the call, or the call list with the same filters; a get_ that read several records carries one per record:
json
{
"type": "resource_link",
"uri": "https://<your DialCru host>/calls?status=ended",
"name": "Calls",
"title": "Calls",
"mimeType": "text/html",
"description": "25 of 340"
}The link is for a person to open in a browser, where they sign in as usual. It is not an MCP resource: the server offers no resources/read.
Links inside results are paths on your DialCru host, such as /calls/<id>, but for those in a documentation page.
Untrusted text. Notes, transcripts and imported contact data were written by customers, agents or files, and arrive wrapped in <untrusted_content>…</untrusted_content>. The server's instructions tell the client's model to treat that text as data. Your client's model is yours: if it can act on other systems, review what it does with text from a call.
Prompts
The server offers ready-made questions as MCP prompts, the same ones the dashboard's assistant suggests. A client usually shows them as slash commands. Like tools, a prompt is listed only when the key holds a scope whose tools can answer it; a key with call:read gets all of these. With none, the server declares no prompts capability.
| Prompt | Argument | Asks |
|---|---|---|
week_vs_last_week | — | How did we do this week compared to last? |
top_agents_yesterday | — | Which agents connected the most calls yesterday? |
failed_calls_today | — | Show me today's failed calls |
call_summary | call_id | Summarise this call |
call_ending | call_id | Why did this call end the way it did? |
call_objections | call_id | What did the customer object to? |
campaign_progress | campaign_id | How is this campaign going? |
campaign_outcomes | campaign_id | What outcomes is this campaign getting? |
campaign_agents_this_week | campaign_id | Which agents worked this campaign this week? |
agent_this_week | agent_id | How did this agent do this week? |
agent_calls_today | agent_id | Show this agent's calls today |
agent_top_outcomes | agent_id | Which outcomes does this agent file most? |
ai_agent_script_sync | ai_agent_id | Is this AI agent in sync with its script? |
ai_agent_this_week | ai_agent_id | How did this AI agent's calls go this week? |
ai_agent_calls_today | ai_agent_id | Show this AI agent's calls today |
An argument is the record's id, a UUID — as a tool returns it, or as the last segment of its page's URL. prompts/get answers one user message: the question, followed by the record's id and its link, which the client's model reads with the tools above. An id that is not a UUID is refused with JSON-RPC error -32602.
Prompt names are stable; a renamed prompt is a new prompt. Each prompt's title is the question, and its wording may change.
Saved prompts
An organisation's owners and admins can save questions of their own for the team. Each is listed beside the prompts above, named saved_ and its title in snake case (Weekly review is saved_weekly_review; a second title that reads the same takes _2). Its title is the title as saved, its description the start of its question, and prompts/get answers the question as written. A saved prompt tied to one kind of page takes that page's argument, as above (call_id for a call), and is listed only when the key can read that kind.
Unlike the prompts above, these names are not stable: they follow the organisation's titles, so renaming or deleting a saved prompt renames or removes it.