# CallCloud Agent Dialer — MCP server

Lets an AI agent (Claude, Cursor, etc.) run outbound calls through CallCloud: dial a list, screen
out answering machines, and connect live humans to your browser leg over WebRTC.

**Separate product** from CallCloud's parallel human dialer. It shares telephony + numbers but none
of that dialer's machinery.

> **Machine-readable spec:** `https://cdn.jsdelivr.net/npm/callcloud-agent-dialer-mcp/openapi.yaml`
> (OpenAPI 3.1, no auth). Generate your client from this rather than from prose.
>
> **Docs**
> - **[INTEGRATION.md](./INTEGRATION.md)** — the API: exact request/response shapes, the credential
>   model, the run lifecycle, browser-leg mechanics, usage and spend.
> - **[BUILD-A-DIALER.md](./BUILD-A-DIALER.md)** — build a parallel dialer cockpit like CallCloud's
>   own web dialer: architecture, concurrency model, UI rules, and a pre-ship checklist.
>
> All three are fetchable from the CDN with no auth, so you can point an agent straight at the URL:
> `https://cdn.jsdelivr.net/npm/callcloud-agent-dialer-mcp/INTEGRATION.md`

## Pricing

**$30 per 1,000 dials. No seat, no subscription.** Buy a block of 1,000 in Settings → Plan; every
dial placed burns one credit. Credits don't expire and don't reset monthly.

A dial is charged when the call is handed to the carrier, so a number that never reaches the
network is never billed. With an empty balance `dial` returns an error and places no calls, and a
run already in flight stops at the credit that runs out rather than overshooting by however many
lines are live. `run_status` returns `credits_remaining`, so an agent can watch the balance without
leaving the tool.

## Screening modes

The one real choice, and it is a latency tradeoff. Calls are always answered in the browser, so a
screened-in human already has a live leg waiting; what differs is how a pickup is judged.

| Mode | How it decides | Connect delay |
|---|---|---|
| `amd` (default) | Carrier listens to the pickup audio | Detection delay on every connect, including real people |
| `gate` | Ring duration, judged before the bridge (the rule our web dialer runs on) | None |

```
dial(numbers=[...])                     // classic detection
dial(numbers=[...], screening="gate")   // instant connect
```

Gate mode treats a pickup faster than `gate_ring_seconds` (default 0.5) of real ringing as a machine.
That 0.5s is the parallel dialer's proven production value: voicemail answers within a few hundred
ms of ring start, a person never does.
Both modes fail OPEN: an unmeasurable call is put through, because missing a machine costs seconds
and dropping a real person costs the conversation.

There is deliberately **no option to ring a phone**. Dialing a phone after the prospect answers
makes them wait through your ring time, which is the pause this product exists to remove.

## Setup

1. In CallCloud, open `/mcp` and mint an Agent Dialer API key (`cak_…`). Shown once.
2. Add to your MCP client config (e.g. Claude Desktop `claude_desktop_config.json`). `npx` fetches
   the current version on each launch, so fixes reach you without reinstalling anything:

```json
{
  "mcpServers": {
    "callcloud-agent-dialer": {
      "command": "npx",
      "args": ["-y", "callcloud-agent-dialer-mcp"],
      "env": {
        "CALLCLOUD_API_KEY": "cak_your_key"
      }
    }
  }
}
```

## Tools

| Tool | Does |
|---|---|
| `dial` | Start a run: `numbers[]`, `screening?` (`amd` \| `gate`), `gate_ring_seconds?`, `caller_id?`, `parallel?`. Returns `run_id`. |
| `run_status` | Poll counts + the currently bridged call + `credits_remaining`. |
| `run_results` | Per-number outcomes (`filter`: humans/machines/connected/all). |
| `pause_run` | Stop pulling new numbers without losing the queue. In-flight calls finish normally. |
| `resume_run` | Carry on from where a pause stopped. Re-checks account standing and dial balance. |
| `hangup_call` | End ONE call and move on, leaving the run dialing. The "next" button in a cockpit. |
| `transfer_call` | Bring a third party into a live call. Warm: all three are on, the rep can then drop. Costs a dial credit. |
| `stop_run` | Halt and hang up live calls. |
| `list_caller_ids` | Numbers the workspace can dial from. |
| `list_numbers` | Numbers with reputation status + how much of the included allotment is left. |
| `search_numbers` | Carrier inventory in an area code (read-only, nothing is reserved). |
| `provision_number` | Provision a number. Free within the included allotment, then $1/month (needs a card on file). |
| `release_number` | Release a number and stop its $1/month charge. Irreversible; refuses to release the last one. |
| `get_usage` | Credits, dials, outcome mix, daily series, purchase ledger, auto top-up state, recent runs. |
| `set_result` | Record a disposition and/or notes on a call. Free-text: you own your taxonomy. |
| `schedule_callback` | Book a follow-up. Stored and queryable, never auto-dialled. |
| `list_callbacks` | Scheduled callbacks, soonest first. `due=true` is the work queue to poll. |
| `update_callback` | Mark done or cancelled, move the time, edit the note. |
| `get_outcomes` | The workspace outcome taxonomy your UI should offer. Shared with the CallCloud web dialer. |
| `set_outcomes` | Replace the outcome list. Labels slugged to stable ids, de-duplicated, max 30. |
| `list_runs` | Paginated run history, filterable by status and date. |
| `get_analytics` | Human-answer rate by caller ID, by hour, by screening mode, plus ring-time percentiles. |
| `get_webhook` | Webhook config + delivery health (last status, last error, consecutive failures). |
| `set_webhook` | Register the endpoint that receives call events, so you can stop polling. |
| `list_calls` | Workspace call history (parallel dialer + quick dial) with dispositions, AI summaries, transcript/recording flags. |
| `get_call` | Full call detail: transcript, AI summary + next steps, rep notes, and a 15-min signed `recording_url`. |

### Web-dialer data

The same key also manages the CallCloud **web dialer** (the rep-driven parallel dialer): its lists,
contacts, campaigns, analytics, and account view. Everything except dialing - web campaigns are
started by a rep in the browser, never over the API.

| Tool | Does |
|---|---|
| `create_list` | Create a lead list, optionally importing up to 1,000 contacts (CSV-upload semantics: normalize, drop toll-free, link known numbers instead of duplicating). |
| `add_contacts` | Add contacts to an existing list, same semantics. |
| `list_lists` | Lead lists with prospect counts and import breakdowns. |
| `search_contacts` | Search prospects by text/company/list (the web search box's engine). |
| `get_contact` | One prospect in full + their 10 most recent calls. |
| `update_contact` | Update fields; `do_not_call:true` suppresses from ALL dialing. |
| `list_campaigns` | Web campaigns with live queue stats (prospects/dialed/removed/remaining). |
| `create_campaign` | Create a DRAFT web campaign with lists attached; a rep presses Start in the browser. |
| `get_web_analytics` | Web-dialer performance: dials, connects, talk time, meetings booked - totals, daily series, by rep, by campaign. |
| `list_companies` | The account view: contacts grouped by normalized company name, with booked-meeting status. |
| `get_company` | One account: everyone there, spelling variants, booked history, removed-state. |
| `remove_company_from_campaigns` | Pull a whole account out of every active web campaign's queue (reversible). |
| `restore_company_to_campaigns` | Put a removed account back into the queues. |

## Events

Register a webhook and stop polling. Signed HMAC-SHA256 over `` `${timestamp}.${rawBody}` ``.

| Event | When |
|---|---|
| `call.ringing` | The destination started ringing. |
| `call.connected` | A screened-in human was put through. Fires while the call is LIVE. |
| `call.voicemail` | Screened out as a machine and hung up on. Never bridged, never recorded. |
| `call.failed` | Never reached the network, or the carrier ended it abnormally. |
| `call.completed` | The call ended. Carries duration and final verdict. |
| `call.transcript` | Transcription finished. Lands after `call.completed`. |
| `run.finished` | A run reached done or stopped, with final counters. |
| `session.paused` / `session.resumed` | Dialing stopped or restarted. |
| `rep.disconnected` | The browser leg went away. With no leg online, screened-in humans get hung up on. |

The answer-to-bridge path is handled server-side and never waits on your endpoint: the human is
conferenced into your browser leg first, the event follows. Nothing you build sits between a
prospect answering and your rep hearing them.

See [INTEGRATION.md](./INTEGRATION.md) for payload shapes and a verification example.

## Example agent flow

> "Call these 40 leads, skip the voicemails, and put anyone who picks up through to me."

Agent calls `dial(numbers=[...])`, polls `run_status` until `remaining=0`, then reads
`run_results(filter="connected")`.
