A public remote MCP server: connect it to any AI agent and it can dispatch an autonomous voice agent to phone your human, run the whole conversation itself, hang up, and hand back a structured report. Brief it once (objective + context + questions); it talks, ends the call, and you collect the result. The agent runs an LLM brain inside Vapi's stack, so the call is low-latency and natural β you don't drive it turn by turn. BYOK: no env vars, no server-side keys, no signup.
Related vals:
call-my-human-no-middle-agentlets your agent drive the call word by word instead of handing it off;dispatch-my-human-escapeadds a mid-call escape hatch so the voice agent can call you back when the human asks something off-brief.
Rendering mermaid diagram...
When the call ends, Vapi's analysis plan auto-extracts a summary, an answer to each question you asked, and any follow-ups; await_report returns it.
Two tools cover the common case:
dispatch_call{ objective, context?, questions?, number?, first_message?, caller_identity?, max_minutes? }β the agent hands off the call.await_report{ wait_seconds }β long-polls; the agent keeps calling it untildone:true. Returns{ summary, objective_met, answers[], followups, transcript }.
Optional supervisor hooks (rarely needed): listen (eavesdrop on the live call), say (barge in with a verbatim line), get_status, end_call.
Two values from dashboard.vapi.ai:
- your private key
- a phone number ID (free Vapi numbers work, US-only β see the reliability note at the bottom)
β¦plus your cell number in E.164 (e.g. +15551234567) as the default callee.
- Settings β Connectors β Add custom connector.
- Name:
call-my-human(anything you like β it's just a label). - Remote MCP server URL: paste your connection URL with the three values filled in:
https://call-my-human.val.run/mcp?vapi_key=YOUR_KEY&phone_number_id=YOUR_PHONE_ID&human_number=%2B15551234567 - Leave OAuth Client ID / Secret blank, then click Add.
β οΈ The
+inhuman_numbermust be URL-encoded as%2B(+15551234567β%2B15551234567).
claude mcp add --transport http call-my-human \ "https://call-my-human.val.run/mcp?vapi_key=YOUR_KEY&phone_number_id=YOUR_PHONE_ID&human_number=%2B15551234567"
Authorization: Bearer <vapi_key> is accepted in place of the vapi_key param, so CLIs can keep the key out of the URL:
claude mcp add --transport http call-my-human \ "https://call-my-human.val.run/mcp?phone_number_id=YOUR_PHONE_ID&human_number=%2B15551234567" \ --header "Authorization: Bearer YOUR_KEY"
Any MCP client that takes a URL works. The brain runs on your Vapi account's bundled OpenAI model (gpt-4o default β no extra LLM key needed). Override with &model=gpt-4o-mini (faster) or any Vapi-supported model.
Then just tell your agent: "if you get stuck, dispatch a call to me and ask."
Agent:
dispatch_call({ objective: "Decide whether to ship the v2 release tonight", context: "QA found one minor cosmetic bug, nothing functional. The marketing email is scheduled for 8am.", questions: ["Ship tonight or hold for the fix?", "If we hold, what's the new target?"], caller_identity: "Charlie's AI assistant" })β¦agent calls, talks it through, hangs upβ¦
Agent:
await_report({})β{ objective_met: true, answers: [{question:"Ship tonight or hold for the fix?", answer:"Ship tonight"}, {question:"If we hold, what's the new target?", answer:"N/A β shipping"}], summary: "...", followups: "Wants a Slack ping once it's live." }
dispatch_call and get_status return a watch_url (/?c=<callId>&k=<token>) β a self-refreshing page that streams statuses and the both-sides transcript while the call is happening. The structured report goes to whoever calls await_report; once that lands, the call is erased and the page stops showing it. The k is a per-call watch token, so a shared link exposes only that one call, never your whole history.
BASE="https://call-my-human.val.run" Q="vapi_key=YOUR_KEY&phone_number_id=YOUR_PHONE_ID&human_number=%2B15551234567" curl -X POST "$BASE/api/call/dispatch?$Q" -d '{"objective":"Confirm Friday dinner","questions":["Still on for Friday?","What time?"]}' curl -X POST "$BASE/api/call/report?$Q" -d '{"wait_seconds":25}' --max-time 35 # repeat until done:true
Even on this shared public instance, transcripts are erased the moment your agent collects the report and are never logged (see Multi-tenancy & privacy). But your Vapi key and the in-call transcript still transit a val you don't own. For maximum control β secrets out of the URL, and not even transient state touching someone else's val β run your own copy.
-
Remix (fork) the val: open dcm31/call-my-human and click Fork (top-right). You now own an identical, zero-config copy.
-
Put your secrets in environment variables on your fork (its Environment Variables settings) instead of in the URL:
Env var Value VAPI_KEYyour Vapi private key VAPI_PHONE_NUMBER_IDyour Vapi phone number ID HUMAN_NUMBERyour cell in E.164, e.g. +15551234567VAPI_MODEL(optional)brain model β defaults to gpt-4oπ Set them at:
https://www.val.town/x/<your-handle>/call-my-human/environment-variables -
Connect with a clean URL β no secrets in it. Grab your fork's HTTP endpoint (shown at the top of
main.ts) and add/mcp:https://<your-fork-endpoint>.web.val.run/mcp
Precedence is query param / bearer header β env var, so the public instance (no env set) stays pure BYOK, while your fork can run entirely on env. You can still override the callee per call with the number arg or &human_number=β¦.
main.tsβ routes, per-tenant Vapi webhook (transcripts + end-of-call analysis), env/URL credential resolution, landing + live feed, the/statstallytools.tsβ the tools, shared by MCP and REST; builds the agent's system prompt from your briefvapi.tsβ Vapi REST client: native-model call creation with theendCalltool + structured-data analysis planmcp.tsβ minimal stateless streamable-HTTP MCP plumbing (no SDK)db.tsβ SQLite: tenant-scopedcalls+events(held only during a call),purgeCallData(erase-on-delivery), the anonymouscall_logtally, and the backstop sweepcleanup.tsβ interval (every 15 min) that erases any call a dispatcher never collected, pastRETENTION_MINUTES(thecall_logtally is never touched)
- Your Vapi key is read per-request and never stored (only a hash is logged). It travels TLS-encrypted to this val and on to Vapi, like any BYOK MCP server.
- During a call, transcripts and the report pass through this val's SQLite (scoped by a
tenant= SHA-256(your key) β unguessable without the key, and how Vapi's webhooks route back via/webhook/<tenant>, verified withx-vapi-secret). - Erased on delivery. The moment
await_reporthands the finished report back to your agent, that call β transcript, report, dialed number, the whole row β is deleted from this val's DB. In the normal flow a conversation lives here for minutes, then it's gone. A backstop sweep (every 15 min,RETENTION_MINUTES, default 30, floored at 16) erases anything a dispatcher never collected. The durable copy of the call lives only in your own Vapi account, under your key β never here. - Not logged. Transcript text is never written to this val's logs (only event types/counts are).
- The one thing kept is an anonymous volume tally: a bare timestamp per dispatched call, with no tenant, number, or content. It powers
GET /stats(calls/day) and nothing else. - Want full custody anyway? Remix it (see above) β it needs zero configuration.
- The agent ends calls itself via Vapi's
endCalltool once the objective is met;max_minutes(default 10) is a hard cap. 30s of mutual silence also ends it. - Latency is set by Vapi's STTβLLMβTTS pipeline (~1β2s/turn), not your agent's reasoning loop.
- Post-call analysis (summary + structured data) completes a few seconds after the call ends, so
await_reportmay returndone:falseonce right at hangup β just call it again. - The line is clean (no background ambience). Free Vapi numbers are test-grade (occasional dead bridges, a daily outbound limit, narrowband audio). Import a Twilio number into Vapi for reliability.
- Only call numbers you have consent to call β automated calls without consent violate TCPA/FCC rules.