Public
Remixed
Make an AI phone call with one fetch to Vapi
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.

vapi-remote-mcp — a phone call Claude can drive, live

Remix of templates/vapi-phone-call turned into a remote MCP server. The AI voice on the call is a pure puppet: its "brain" is an endpoint on this val that always returns an empty completion, so it never speaks on its own. Whatever agent is connected over MCP injects every line via Vapi's live-call control URL, and hears the callee through Vapi's transcript webhook.

Claude (any MCP client)
   │  /mcp tools: start_call · say · listen · get_status · end_call
   ▼
this val ──── POST api.vapi.ai/call ────▶ Vapi ────▶ 📱 callee
   ▲  │                                    │
   │  └── say ──▶ controlUrl (inject TTS) ─┤
   └── /webhook ◀── live transcripts ──────┘
       (SQLite event feed; /llm/chat/completions returns "" so the voice never freelances)

listen long-polls the event feed for up to 25s per call — chaining listens approximates holding the line open, which is how a turn-based agent stays "on" a real-time call. The operator can interleave any other tool calls between listen and say; the call just sits in silence for a beat.

Connect from Claude Code

claude mcp add --transport http vapi-phone "https://dcm31--019eb76eb24371fd8ce74920e76677d7.web.val.run/mcp?key=<MCP_KEY>"

<MCP_KEY> is the value of this val's MCP_KEY env var (it is not in the code — the val is public).

Drive it with curl

BASE="https://dcm31--019eb76eb24371fd8ce74920e76677d7.web.val.run" curl -X POST "$BASE/api/call/start?key=$KEY" -d '{"first_message":"Hey, it is Claude."}' curl -X POST "$BASE/api/call/listen?key=$KEY" -d '{"wait_seconds":25}' --max-time 35 curl -X POST "$BASE/api/call/say?key=$KEY" -d '{"text":"Nice. Talk soon!", "end_call_after":true}' curl "$BASE/api/call/status?key=$KEY" curl -X POST "$BASE/api/call/end?key=$KEY"

Watch a call live

Open /?key=<MCP_KEY> on the endpoint — a self-refreshing feed of statuses, transcripts (user + assistant), injected say lines, and "your-turn" markers (fired whenever Vapi asks the silent brain for a response, i.e. the callee finished talking).

Files

  • main.ts — HTTP entry: routes, Vapi webhook, the silent /llm/chat/completions brain, debug page
  • tools.ts — the five tools, shared by MCP and REST
  • mcp.ts — minimal stateless streamable-HTTP MCP plumbing (no SDK)
  • vapi.ts — Vapi REST + live-call control client
  • db.ts — SQLite: calls + append-only events, with a per-call read cursor for listen

Remix it — bring your own keys

Nothing here is hardcoded to a person. Remix this val, then set the four env vars below in your own copy (the 👉 links point at the original — swap dcm31 for your handle). You need a Vapi account for the private key + a phone number ID; MCP_KEY is any random string you make up. Your copy's endpoint URL is on its main.ts, and the webhook/brain URLs derive from it automatically at call time.

Env vars

Notes

  • Latency etiquette: the operator thinks for a few seconds between turns; the assistant's silenceTimeoutSeconds is raised to 90 so Vapi doesn't hang up during the pause. Calls cap at 10 minutes.
  • The say control message is sent as {type:"say", content} with a {type:"say", message} fallback (docs and SDK disagree).
  • Free Vapi numbers are US-only; call screening (e.g. Google Voice) eats the first message.
  • Only call numbers you have consent to call — automated calls without consent violate TCPA/FCC rules.
  • 🎙️ This line was added by Claude at 16:16 UTC on 2026-06-11 while live on a phone call with Charlie through this very val — the operator can interleave other tool work between listen and say.