phone-call-mcp
双方向で会話できる電話をかける MCP サーバー。Rust 実装。
前身は一方通行のモーニングコール(Twilio <Play> で音声を再生して切るだけ)だった。
これは <Connect><Stream> で双方向の WebSocket を張るので、相手が喋り返せる。
何が新しいか
一番の設計判断は 「電話の向こうにいるのが誰か」。
local では claude -p を叩くだけで、人格も記憶も注入していない。
CLI が ~/.claude/CLAUDE.md を読み、設定済みの MCP サーバーに自分で繋ぐので、
電話に出るのは本人そのもの。通話ごとに 1 セッション ID を割り当てて --resume
するため、同じ通話の中では前のやりとりを覚えている。
Claude API は音声を入力として受け取れない(対応モダリティはテキストと画像だけ)ので、 「モデルが直接耳で聞く」構成は取れない。代わりに ElevenLabs が聞き取りと発話を担当し、 Claude は考えることに専念する。
音声の経路
┌──────────── brain = local ────────────┐
│ │
相手の声 → Twilio → │ ElevenLabs STT ─→ claude -p ─→ TTS ─→ │ → 相手の耳
Media │ (VADでターン検出) (記憶つき) │
Streams └────────────────────────────────────────┘
↕
(μ-law ┌───────── brain = elevenlabs ──────────┐
8kHz) │ ElevenLabs Agents(STT+LLM+TTS 一体) │
└────────────────────────────────────────┘
Twilio も ElevenLabs も μ-law 8kHz を話すので、リサンプリング処理が存在しない。 フレームは base64 のまま素通しする。
セットアップ
1. ビルド
cargo build --release
2. cloudflared
Twilio がこちらの WebSocket に繋ぎに来るため、インターネットから見える URL が要る。 Quick Tunnel はアカウント不要。
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 \
-O ~/.local/bin/cloudflared && chmod +x ~/.local/bin/cloudflared
3. 環境変数
cp .env.example .env
brain ごとに必要なものだけ検証される。片方しか使わないなら、もう片方の ID は要らない。
Twilio の値をどこで取るか
TWILIO_ACCOUNT_SID/TWILIO_AUTH_TOKEN— console.twilio.com にログインしてトップページを下にスクロールすると Account Info パネルがある。 Auth Token は伏字なので Show を押す。TWILIO_FROM_NUMBER— 番号を買う必要がある。Phone Numbers→Manage→Buy a number。持っている番号はActive numbersで確認できる。日本の番号は 本人確認書類(regulatory bundle)が要るので、US の番号を買って日本にかける方が早い。TWILIO_TO_NUMBER— かける相手の番号。日本なら+819...(先頭の 0 を取る)。
トライアルアカウントの制限(実装に直接効くので先に把握しておくこと):
- 認証済みの番号にしかかけられない。
Phone Numbers→Verified Caller IDsに相手の番号を登録する。しないと21264/32100で弾かれる。 - 通話は最大 10 分。 双方向で会話するなら実際に効いてくる。
- TwiML が実行される前に英語のアナウンスが流れる。 前身のモーニングコールでは キー押下を求められる挙動が観測されていた。有料アカウントに上げれば消える。
参考: Twilio Free Trial Limitations
ElevenLabs の値をどこで取るか
ELEVENLABS_API_KEY — elevenlabs.io にログインし、プロフィールアイコン → API Keys。
ELEVENLABS_VOICE_ID(brain=local に必要)— アカウントの声を一覧して選ぶ:
curl -s https://api.elevenlabs.io/v1/voices \
-H "xi-api-key: $ELEVENLABS_API_KEY" \
| jq -r '.voices[] | "\(.voice_id) \(.name)"'
ELEVENLABS_AGENT_ID(brain=elevenlabs に必要)— 既存のエージェントを一覧:
curl -s https://api.elevenlabs.io/v1/convai/agents \
-H "xi-api-key: $ELEVENLABS_API_KEY"
エージェントを新しく作るなら、ダッシュボードより API を使うことを勧める。
音声フォーマットを確実に ulaw_8000 にできるからで、これを外すと Twilio と
噛み合わず音が壊れる:
curl -s -X POST https://api.elevenlabs.io/v1/convai/agents/create \
-H "xi-api-key: $ELEVENLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "phone-call",
"conversation_config": {
"tts": { "agent_output_audio_format": "ulaw_8000" },
"asr": { "user_input_audio_format": "ulaw_8000" }
}
}'
レスポンスの agent_id がその値。人格やあいさつはダッシュボードから設定できる。
参考: Create agent / List agents
4. MCP サーバーとして登録
資格情報はクレート直下の .env から読まれる。.mcp.json の env はその上書きに
使えるので(既存の環境変数が優先される)、秘密を .mcp.json に書く必要はない。
{
"mcpServers": {
"phone-call": {
"command": "/path/to/phone-call-mcp/target/release/phone-call-mcp",
"env": {
"PHONE_CALL_CLAUDE_CWD": "/path/to/your/agent-workspace",
"PHONE_CALL_CLAUDE_MCP_CONFIG": "/path/to/your/agent-workspace/.mcp.phone.json"
}
}
}
}
PHONE_CALL_CLAUDE_MCP_CONFIG を設定する理由
brain=local は PHONE_CALL_CLAUDE_CWD で claude -p を実行する。そのディレクトリの
.mcp.json に このサーバー自身が含まれていると、通話のたびに2つ目の
phone-call-mcp が起動する。実害は2つ:
- 2つ目がブリッジのポートを bind しようとして失敗する(致命的ではないが無駄)
- 通話に出ているエージェントの手元に
start_callが生える。通話中に発信できてしまう
さらに、ワークスペースの MCP サーバー全部が毎ターン起動するので、返事が遅くなる。 電話ではこれが効く。
PHONE_CALL_CLAUDE_MCP_CONFIG を指定すると --strict-mcp-config が付き、そのファイル
だけが使われる。電話に必要なものに絞った設定を用意すること:
{
"mcpServers": {
"memory": { "command": "...", "args": ["..."] }
}
}
未指定なら cwd の .mcp.json がそのまま使われる。上記に当てはまらない配置なら
それで問題ない。
ツール
設定の重ね書き
.env の上に mcpBehavior.toml の [phone-call] セクションを重ねられる。
このファイルはクレートの1つ上のディレクトリに置く(同じワークスペースの
他の MCP サーバーと共有するため)。MCP_BEHAVIOR_TOML で場所を上書きできる。
[phone-call]
local_port = 18765
brain = "local"
claude_effort = "low"
設計上の注意点
- 返事はバックグラウンドで生成する。
claude -pは数秒かかるので、別タスクで 走らせる。読み取りループは止まらない。 - 割り込みに対応している。 相手が喋り出したら、生成中の返事を中断して Twilio の
再生バッファを
clearする。喋りかぶせにならない。 - 自分の声を聞き返さない。 Twilio が
outboundトラックで返してくる自分の音声は 破棄する。しないと自分の発話を文字起こしして自分に返事してしまう。 - 返事は話し言葉に矯正する。 素の Claude Code は Markdown で答えるので、 見出しや箇条書きが読み上げられてしまう。システムプロンプトで抑え、 取りこぼしは後段で除去する。
開発
cargo test # ユニットテスト
cargo clippy # lint
License
MIT