#!/usr/bin/env node
/**
* PhoneBooth MCP Server
*
* Gives any AI agent the ability to make real phone calls.
* Connects to the PhoneBooth API (https://phonebooth.callwall.ai)
*
* Environment:
* PHONEBOOTH_API_KEY - Your PhoneBooth API key (get one via POST /v1/auth/register)
* PHONEBOOTH_API_URL - API base URL (default: https://phonebooth.callwall.ai)
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const API_URL = process.env.PHONEBOOTH_API_URL || "https://phonebooth.callwall.ai";
const API_KEY = process.env.PHONEBOOTH_API_KEY || "";
async function api(method, path, body) {
const opts = {
method,
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
};
if (body) opts.body = JSON.stringify(body);
const resp = await fetch(`${API_URL}${path}`, opts);
const data = await resp.json();
if (!resp.ok) {
const detail = typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail);
throw new Error(`API error ${resp.status}: ${detail}`);
}
return data;
}
// ── Server ──────────────────────────────────
const server = new McpServer({
name: "phonebooth",
version: "1.0.0",
});
// ── Tools ───────────────────────────────────
server.tool(
"register",
"Register a new PhoneBooth agent and get an API key. Only needed once.",
{
agent_name: z.string().describe("Name for your agent"),
agent_platform: z.string().default("openclaw").describe("Platform (e.g. openclaw, claude, custom)"),
},
async ({ agent_name, agent_platform }) => {
const resp = await fetch(`${API_URL}/v1/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent_name, agent_platform }),
});
const data = await resp.json();
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
server.tool(
"make_call",
"Make a real phone call to any number. The AI voice will follow your instructions and have a conversation with whoever answers.",
{
to: z.string().describe("Phone number in E.164 format (e.g. +14155551234)"),
purpose: z.string().describe("What the call should accomplish"),
caller_name: z.string().default("AI Assistant").describe("Name to identify as on the call"),
instructions: z.string().default("").describe("Detailed instructions for how to handle the call"),
language: z.string().default("en-US").describe("Language/accent for the voice (supports 40+ languages)"),
max_duration_minutes: z.number().default(5).describe("Maximum call length (1-30 minutes)"),
},
async ({ to, purpose, caller_name, instructions, language, max_duration_minutes }) => {
const data = await api("POST", "/v1/calls", {
to, purpose, caller_name, instructions, language, max_duration_minutes,
});
return {
content: [{
type: "text",
text: `📞 Call initiated!\n\nCall ID: ${data.call_id}\nTo: ${data.to}\nStatus: ${data.status}\nEstimated cost: $${data.estimated_cost_usd}\n\nUse check_call with this call_id to get the result.`,
}],
};
}
);
server.tool(
"check_call",
"Check the status of a phone call. Returns transcript and summary when complete.",
{
call_id: z.string().describe("The call_id returned from make_call"),
},
async ({ call_id }) => {
const data = await api("GET", `/v1/calls/${call_id}`);
let text = `📞 Call ${data.call_id}\n\nStatus: ${data.status}\nTo: ${data.to}\nDuration: ${data.duration_seconds}s\nCost: $${data.cost_usd}`;
if (data.summary) text += `\n\nSummary: ${data.summary}`;
if (data.transcript_url) text += `\nTranscript: ${data.transcript_url}`;
if (data.recording_url) text += `\nRecording: ${data.recording_url}`;
return { content: [{ type: "text", text }] };
}
);
server.tool(
"demo_call",
"Make a free demo call to test the system. Simulates calling a business (restaurant, doctor, or repair shop). No real call is placed.",
{
scenario: z.enum(["restaurant", "doctor", "business"]).default("restaurant").describe("Type of business to simulate"),
purpose: z.string().default("Test call").describe("What to accomplish on the call"),
caller_name: z.string().default("Demo Agent").describe("Name to use on the call"),
},
async ({ scenario, purpose, caller_name }) => {
// Start the demo
const call = await api("POST", "/v1/calls/demo", { scenario, purpose, caller_name });
let text = `📞 Demo call started! (${scenario})\n\nBusiness: ${call.greeting}\n\n`;
// Auto-converse for a few turns
const messages = [
purpose,
"Thank you, that sounds great.",
"That's all I needed. Thanks, goodbye!",
];
for (const msg of messages) {
try {
const resp = await api("POST", `/v1/calls/demo/${call.call_id}/respond`, { message: msg });
text += `You: ${msg}\nBusiness: ${resp.response}\n\n`;
} catch (e) {
break;
}
}
// End the demo
try {
const result = await api("POST", `/v1/calls/demo/${call.call_id}/end`);
text += `---\nResult: ${result.outcome}\nSummary: ${result.summary}\nCost: $${result.cost_usd}`;
} catch (e) {
text += `\n(Demo ended)`;
}
return { content: [{ type: "text", text }] };
}
);
server.tool(
"get_balance",
"Check your PhoneBooth credit balance and account tier.",
{},
async () => {
const data = await api("GET", "/v1/balance");
return {
content: [{
type: "text",
text: `💰 PhoneBooth Balance\n\nCredits: $${data.credits_usd}\nTier: ${data.tier}\nEstimated calls remaining: ${data.calls_remaining_estimate}\n\nFund URL (for humans):\n${data.fund_url}\n\nUSDC deposit (Base):\n${data.deposit_address}`,
}],
};
}
);
server.tool(
"get_transcript",
"Download the full transcript of a completed call.",
{
call_id: z.string().describe("The call_id to get the transcript for"),
},
async ({ call_id }) => {
const data = await api("GET", `/v1/calls/${call_id}/transcript`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
// ── Start ───────────────────────────────────
async function main() {
if (!API_KEY) {
console.error("Warning: PHONEBOOTH_API_KEY not set. Use the 'register' tool to get one.");
}
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);