Public
Public remote MCP: your agent phones you. BYO Vapi keys
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.

call-my-human β€” send an agent to phone your human πŸ“ž

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-agent lets your agent drive the call word by word instead of handing it off; dispatch-my-human-escape adds 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.

The loop

Two tools cover the common case:

  1. dispatch_call { objective, context?, questions?, number?, first_message?, caller_identity?, max_minutes? } β€” the agent hands off the call.
  2. await_report { wait_seconds } β€” long-polls; the agent keeps calling it until done: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.

What you need

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.

Add it as a connector

claude.ai (web / desktop)

  1. Settings β†’ Connectors β†’ Add custom connector.
  2. Name: call-my-human (anything you like β€” it's just a label).
  3. 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
    
  4. Leave OAuth Client ID / Secret blank, then click Add.

⚠️ The + in human_number must be URL-encoded as %2B (+15551234567 β†’ %2B15551234567).

Claude Code / Codex (CLI)

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."

Example

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." }

Watching a call 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.

Testing with curl

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

Self-host: remix for full privacy πŸ”’

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.

  1. Remix (fork) the val: open dcm31/call-my-human and click Fork (top-right). You now own an identical, zero-config copy.

  2. Put your secrets in environment variables on your fork (its Environment Variables settings) instead of in the URL:

    Env varValue
    VAPI_KEYyour Vapi private key
    VAPI_PHONE_NUMBER_IDyour Vapi phone number ID
    HUMAN_NUMBERyour cell in E.164, e.g. +15551234567
    VAPI_MODEL (optional)brain model β€” defaults to gpt-4o

    πŸ‘‰ Set them at: https://www.val.town/x/<your-handle>/call-my-human/environment-variables

  3. 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=….

Files

  • main.ts β€” routes, per-tenant Vapi webhook (transcripts + end-of-call analysis), env/URL credential resolution, landing + live feed, the /stats tally
  • tools.ts β€” the tools, shared by MCP and REST; builds the agent's system prompt from your brief
  • vapi.ts β€” Vapi REST client: native-model call creation with the endCall tool + structured-data analysis plan
  • mcp.ts β€” minimal stateless streamable-HTTP MCP plumbing (no SDK)
  • db.ts β€” SQLite: tenant-scoped calls + events (held only during a call), purgeCallData (erase-on-delivery), the anonymous call_log tally, and the backstop sweep
  • cleanup.ts β€” interval (every 15 min) that erases any call a dispatcher never collected, past RETENTION_MINUTES (the call_log tally is never touched)

Multi-tenancy & privacy

  • 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 with x-vapi-secret).
  • Erased on delivery. The moment await_report hands 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.

Notes

  • The agent ends calls itself via Vapi's endCall tool 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_report may return done:false once 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.