claude-phone-local 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.env.example +108 -0
  2. package/Dockerfile +84 -0
  3. package/LICENSE +22 -0
  4. package/README.md +231 -0
  5. package/claude-api-server/package-lock.json +832 -0
  6. package/claude-api-server/package.json +20 -0
  7. package/claude-api-server/server.js +540 -0
  8. package/claude-api-server/structured.js +208 -0
  9. package/cli/README.md +231 -0
  10. package/cli/bin/check-publish-safety.js +53 -0
  11. package/cli/bin/claude-phone.js +23 -0
  12. package/cli/bin/cli-main.js +284 -0
  13. package/cli/bin/postinstall.js +20 -0
  14. package/cli/lib/commands/api-server.js +77 -0
  15. package/cli/lib/commands/backup.js +69 -0
  16. package/cli/lib/commands/config/path.js +30 -0
  17. package/cli/lib/commands/config/reset.js +73 -0
  18. package/cli/lib/commands/config/show.js +86 -0
  19. package/cli/lib/commands/device/add.js +143 -0
  20. package/cli/lib/commands/device/list.js +55 -0
  21. package/cli/lib/commands/device/remove.js +83 -0
  22. package/cli/lib/commands/doctor.js +422 -0
  23. package/cli/lib/commands/logs.js +186 -0
  24. package/cli/lib/commands/restore.js +172 -0
  25. package/cli/lib/commands/setup.js +1246 -0
  26. package/cli/lib/commands/start.js +346 -0
  27. package/cli/lib/commands/status.js +139 -0
  28. package/cli/lib/commands/stop.js +101 -0
  29. package/cli/lib/commands/uninstall.js +183 -0
  30. package/cli/lib/commands/update.js +205 -0
  31. package/cli/lib/config.js +113 -0
  32. package/cli/lib/docker.js +384 -0
  33. package/cli/lib/mcp-register.js +108 -0
  34. package/cli/lib/network.js +109 -0
  35. package/cli/lib/platform.js +85 -0
  36. package/cli/lib/port-check.js +135 -0
  37. package/cli/lib/prereqs/checks/compose.js +147 -0
  38. package/cli/lib/prereqs/checks/disk.js +89 -0
  39. package/cli/lib/prereqs/checks/docker.js +155 -0
  40. package/cli/lib/prereqs/checks/network.js +78 -0
  41. package/cli/lib/prereqs/checks/node.js +95 -0
  42. package/cli/lib/prereqs/installers/docker-desktop.js +248 -0
  43. package/cli/lib/prereqs/installers/docker.js +229 -0
  44. package/cli/lib/prereqs/installers/node.js +254 -0
  45. package/cli/lib/prereqs/platform.js +175 -0
  46. package/cli/lib/prereqs/utils/execute.js +208 -0
  47. package/cli/lib/prereqs/utils/rollback.js +223 -0
  48. package/cli/lib/prereqs/utils/sudo.js +177 -0
  49. package/cli/lib/prereqs.js +228 -0
  50. package/cli/lib/prerequisites.js +115 -0
  51. package/cli/lib/process-manager.js +176 -0
  52. package/cli/lib/utils.js +78 -0
  53. package/cli/lib/validators.js +219 -0
  54. package/cli/lib/voice-downloader.js +79 -0
  55. package/docker/entrypoint.sh +110 -0
  56. package/docker/supervisord.conf +78 -0
  57. package/docker-compose.yml +40 -0
  58. package/docs/CLAUDE-CODE-SKILL.md +45 -0
  59. package/docs/LANGUAGES.md +113 -0
  60. package/docs/MCP-SERVER.md +111 -0
  61. package/docs/TROUBLESHOOTING.md +288 -0
  62. package/mcp-server/index.js +159 -0
  63. package/mcp-server/package-lock.json +1201 -0
  64. package/mcp-server/package.json +9 -0
  65. package/package.json +73 -0
  66. package/stt-local/Dockerfile +12 -0
  67. package/stt-local/server.py +45 -0
  68. package/tts-local/Dockerfile +9 -0
  69. package/tts-local/server.py +83 -0
  70. package/voice-app/API-QUERY-CONTRACT.md +238 -0
  71. package/voice-app/DEPLOYMENT.md +176 -0
  72. package/voice-app/Dockerfile +20 -0
  73. package/voice-app/README-OUTBOUND.md +316 -0
  74. package/voice-app/config/devices.json.example +18 -0
  75. package/voice-app/index.js +273 -0
  76. package/voice-app/lib/audio-fork.js +465 -0
  77. package/voice-app/lib/claude-bridge.js +113 -0
  78. package/voice-app/lib/connection-retry.js +58 -0
  79. package/voice-app/lib/conversation-loop.js +490 -0
  80. package/voice-app/lib/device-registry.js +171 -0
  81. package/voice-app/lib/http-server.js +242 -0
  82. package/voice-app/lib/logger.js +35 -0
  83. package/voice-app/lib/multi-registrar.js +133 -0
  84. package/voice-app/lib/outbound-handler.js +299 -0
  85. package/voice-app/lib/outbound-routes.js +417 -0
  86. package/voice-app/lib/outbound-session.js +324 -0
  87. package/voice-app/lib/query-routes.js +484 -0
  88. package/voice-app/lib/registrar.js +144 -0
  89. package/voice-app/lib/sip-handler.js +490 -0
  90. package/voice-app/lib/tts-service.js +205 -0
  91. package/voice-app/lib/whisper-client.js +140 -0
  92. package/voice-app/package.json +38 -0
  93. package/voice-app/static/gotit-beep.wav +0 -0
  94. package/voice-app/static/hold-music.wav +0 -0
  95. package/voice-app/static/ready-beep.wav +0 -0
  96. package/voice-app/test/freeswitch-retry.test.js +100 -0
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bash
2
+ # Bootstraps persistent state in /data, then hands off to supervisord.
3
+ # Everything here is idempotent: it only fetches what is missing, so a restart
4
+ # with a populated ./data volume starts instantly and never re-downloads.
5
+ set -euo pipefail
6
+
7
+ log() { echo "[bootstrap] $*"; }
8
+
9
+ : "${EXTERNAL_IP:?must be set to this host LAN IP}"
10
+ export DRACHTIO_SECRET="${DRACHTIO_SECRET:-cymru}"
11
+ export RTP_START="${RTP_START:-30000}"
12
+ export RTP_END="${RTP_END:-30100}"
13
+ # FreeSWITCH binds sofia to the container IP (mrf.xml sip-ip = $${local_ip_v4}),
14
+ # never to loopback. drachtio lives in this same network namespace, so it must
15
+ # be pointed at that same address - advertising 127.0.0.1 would send it to a
16
+ # port nothing is listening on. RTP still advertises EXTERNAL_IP for the SBC.
17
+ FS_SIP_IP="$(hostname -i | awk '{print $1}')"
18
+ export FS_SIP_IP
19
+ log "FreeSWITCH SIP will advertise ${FS_SIP_IP}; RTP will advertise ${EXTERNAL_IP}"
20
+
21
+ export PIPER_VOICES_DIR=/data/voices
22
+ export HF_HOME=/data/models/huggingface
23
+
24
+ mkdir -p /data/voices /data/models/huggingface /data/audio /data/config
25
+
26
+ # --- Piper voices -----------------------------------------------------------
27
+ # VOICES is a space-separated list of Piper voice names. Each is resolved to its
28
+ # path in the rhasspy/piper-voices repo from the language prefix.
29
+ PIPER_BASE="https://huggingface.co/rhasspy/piper-voices/resolve/main"
30
+ VOICES="${PIPER_VOICES:-en_US-lessac-medium hi_IN-priyamvada-medium mr_IN-google-medium}"
31
+
32
+ voice_repo_path() {
33
+ # en_US-lessac-medium -> en/en_US/lessac/medium/en_US-lessac-medium
34
+ local name="$1"
35
+ local locale="${name%%-*}" # en_US
36
+ local rest="${name#*-}" # lessac-medium
37
+ local speaker="${rest%%-*}" # lessac
38
+ local quality="${rest##*-}" # medium
39
+ local lang="${locale%%_*}" # en
40
+ echo "${lang}/${locale}/${speaker}/${quality}/${name}"
41
+ }
42
+
43
+ for v in $VOICES; do
44
+ if [ -f "/data/voices/${v}.onnx" ]; then
45
+ log "voice ${v} already present"
46
+ continue
47
+ fi
48
+ path="$(voice_repo_path "$v")"
49
+ log "downloading voice ${v} ..."
50
+ if curl -fsSL --retry 3 --retry-delay 2 "${PIPER_BASE}/${path}.onnx" -o "/data/voices/${v}.onnx.part" \
51
+ && curl -fsSL --retry 3 --retry-delay 2 "${PIPER_BASE}/${path}.onnx.json" -o "/data/voices/${v}.onnx.json"; then
52
+ mv "/data/voices/${v}.onnx.part" "/data/voices/${v}.onnx"
53
+ log "voice ${v} ready"
54
+ else
55
+ rm -f "/data/voices/${v}.onnx.part"
56
+ log "WARNING: could not download voice ${v} - it will be unavailable"
57
+ fi
58
+ done
59
+
60
+ # --- Whisper model ----------------------------------------------------------
61
+ # faster-whisper pulls the model on first use into HF_HOME, which lives on the
62
+ # /data volume, so it survives restarts. Pre-fetch it so the first call is fast.
63
+ if [ "${PREFETCH_WHISPER:-1}" = "1" ]; then
64
+ log "ensuring Whisper model '${WHISPER_MODEL:-medium}' is cached (first run may take a while) ..."
65
+ /opt/venv/bin/python - <<'PY' || log "WARNING: Whisper prefetch failed; it will download on first call"
66
+ import os
67
+ from faster_whisper import WhisperModel
68
+ WhisperModel(
69
+ os.environ.get("WHISPER_MODEL", "medium"),
70
+ device=os.environ.get("WHISPER_DEVICE", "cpu"),
71
+ compute_type=os.environ.get("WHISPER_COMPUTE_TYPE", "int8"),
72
+ )
73
+ print("[bootstrap] whisper model cached")
74
+ PY
75
+ fi
76
+
77
+ # --- Device config ----------------------------------------------------------
78
+ # Seed /data/config/devices.json once, then never touch it again - it holds the
79
+ # user's SIP credentials and is theirs to edit.
80
+ if [ ! -f /data/config/devices.json ]; then
81
+ if [ -n "${SIP_EXTENSION:-}" ]; then
82
+ log "generating devices.json for extension ${SIP_EXTENSION}"
83
+ /usr/bin/node -e '
84
+ const fs = require("fs");
85
+ const ext = process.env.SIP_EXTENSION;
86
+ fs.writeFileSync("/data/config/devices.json", JSON.stringify({
87
+ [ext]: {
88
+ name: process.env.DEVICE_NAME || "Maya",
89
+ extension: ext,
90
+ authId: process.env.SIP_AUTH_ID || ext,
91
+ password: process.env.SIP_PASSWORD || "",
92
+ voice: process.env.PIPER_VOICE || "en_US-lessac-medium",
93
+ language: process.env.STT_LANGUAGE || "auto",
94
+ prompt: process.env.DEVICE_PROMPT ||
95
+ "You are " + (process.env.DEVICE_NAME || "Maya") +
96
+ ", a helpful AI assistant speaking to the user over the phone. " +
97
+ "ALWAYS reply in the same language the user just used, using that " +
98
+ "language native script. Keep voice responses under 40 words."
99
+ }
100
+ }, null, 2) + "\n");
101
+ '
102
+ else
103
+ log "SIP_EXTENSION not set - skipping devices.json generation"
104
+ fi
105
+ else
106
+ log "devices.json already present (left untouched)"
107
+ fi
108
+
109
+ log "bootstrap complete - starting services"
110
+ exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
@@ -0,0 +1,78 @@
1
+ ; claude-phone-local - all five services in one container.
2
+ ; Everything talks over 127.0.0.1 here, which is why the unified image needs no
3
+ ; Docker DNS names and sidesteps the bridge-networking issues of the split setup.
4
+ [supervisord]
5
+ nodaemon=true
6
+ logfile=/dev/null
7
+ logfile_maxbytes=0
8
+ pidfile=/run/supervisord.pid
9
+ user=root
10
+
11
+ [program:freeswitch]
12
+ command=/entrypoint.sh freeswitch
13
+ --sip-port 5080
14
+ --rtp-range-start %(ENV_RTP_START)s
15
+ --rtp-range-end %(ENV_RTP_END)s
16
+ --ext-rtp-ip %(ENV_EXTERNAL_IP)s
17
+ --ext-sip-ip %(ENV_FS_SIP_IP)s
18
+ --advertise-external-ip
19
+ -- -nc -nf
20
+ priority=10
21
+ autorestart=true
22
+ startsecs=5
23
+ stdout_logfile=/dev/fd/1
24
+ stdout_logfile_maxbytes=0
25
+ stderr_logfile=/dev/fd/2
26
+ stderr_logfile_maxbytes=0
27
+ stdout_events_enabled=true
28
+
29
+ [program:drachtio]
30
+ command=/usr/local/bin/drachtio
31
+ --contact sip:*:5070;transport=tcp,udp
32
+ --external-ip %(ENV_EXTERNAL_IP)s
33
+ --secret %(ENV_DRACHTIO_SECRET)s
34
+ --port 9022
35
+ --loglevel info
36
+ priority=10
37
+ autorestart=true
38
+ startsecs=3
39
+ stdout_logfile=/dev/fd/1
40
+ stdout_logfile_maxbytes=0
41
+ stderr_logfile=/dev/fd/2
42
+ stderr_logfile_maxbytes=0
43
+
44
+ [program:stt]
45
+ command=/opt/venv/bin/uvicorn server:app --host 127.0.0.1 --port 9001
46
+ directory=/opt/stt
47
+ environment=HF_HOME="/data/models/huggingface",HOME="/root"
48
+ priority=20
49
+ autorestart=true
50
+ ; the first run downloads the Whisper model, which can take several minutes
51
+ startsecs=10
52
+ stdout_logfile=/dev/fd/1
53
+ stdout_logfile_maxbytes=0
54
+ stderr_logfile=/dev/fd/2
55
+ stderr_logfile_maxbytes=0
56
+
57
+ [program:tts]
58
+ command=/opt/venv/bin/uvicorn server:app --host 127.0.0.1 --port 9002
59
+ directory=/opt/tts
60
+ environment=PIPER_VOICES_DIR="/data/voices"
61
+ priority=20
62
+ autorestart=true
63
+ startsecs=5
64
+ stdout_logfile=/dev/fd/1
65
+ stdout_logfile_maxbytes=0
66
+ stderr_logfile=/dev/fd/2
67
+ stderr_logfile_maxbytes=0
68
+
69
+ [program:voice-app]
70
+ command=/usr/bin/node index.js
71
+ directory=/app
72
+ priority=30
73
+ autorestart=true
74
+ startsecs=5
75
+ stdout_logfile=/dev/fd/1
76
+ stdout_logfile_maxbytes=0
77
+ stderr_logfile=/dev/fd/2
78
+ stderr_logfile_maxbytes=0
@@ -0,0 +1,40 @@
1
+ # claude-phone-local - one container, one volume.
2
+ #
3
+ # All five services (drachtio, FreeSWITCH, voice-app, STT, TTS) run inside a
4
+ # single image and talk over 127.0.0.1, so there is no container DNS to get
5
+ # wrong. Everything that must survive a restart lives in ./data:
6
+ #
7
+ # ./data/voices Piper voice models (downloaded on first run)
8
+ # ./data/models Whisper model cache (downloaded on first run)
9
+ # ./data/config devices.json (generated once, then yours to edit)
10
+ # ./data/audio generated speech (scratch)
11
+ #
12
+ # The previous five-container layout is preserved in docker-compose.multi.yml.
13
+ #
14
+ # EXTERNAL_IP in .env must be this PC's LAN IP (run `ipconfig` / `ip addr`) --
15
+ # it is the address 3CX sends SIP and RTP to.
16
+
17
+ services:
18
+ claude-phone:
19
+ build: .
20
+ image: claude-phone-local:latest
21
+ container_name: claude-phone
22
+ restart: unless-stopped
23
+ env_file:
24
+ - .env
25
+ environment:
26
+ # Everything is in-process now, so all hosts are loopback.
27
+ - DRACHTIO_HOST=127.0.0.1
28
+ - FREESWITCH_HOST=127.0.0.1
29
+ - STT_LOCAL_URL=http://127.0.0.1:9001
30
+ - TTS_LOCAL_URL=http://127.0.0.1:9002
31
+ - AUDIO_BASE_URL=http://127.0.0.1:3000
32
+ - RTP_START=30000
33
+ - RTP_END=30100
34
+ ports:
35
+ - "3000:3000" # HTTP API + audio file serving
36
+ - "5070:5070/udp" # drachtio SIP
37
+ - "5070:5070/tcp"
38
+ - "30000-30100:30000-30100/udp" # RTP
39
+ volumes:
40
+ - ./data:/data
@@ -0,0 +1,45 @@
1
+ # Claude Code Call Skill — superseded
2
+
3
+ > **This approach has been replaced by the [MCP server](MCP-SERVER.md).**
4
+
5
+ This document used to describe a Python-based Claude Code *skill* that wrapped
6
+ the outbound-call API, so you could say "call me when the backup finishes".
7
+
8
+ That capability now ships as a proper **MCP server** (`mcp-server/`), which is
9
+ better in every way that matters here:
10
+
11
+ | | Old skill | MCP server |
12
+ |---|---|---|
13
+ | Integration | Python script invoked by a skill | Native MCP tools |
14
+ | Discovery | Claude had to be told it existed | Advertised via `tools/list` |
15
+ | Errors | Raised as script output | Returned as tool errors Claude can act on |
16
+ | Install | Copy files into a skills directory | `claude mcp add` |
17
+ | Status/hangup | Manual HTTP calls | `call_status`, `hangup` tools |
18
+
19
+ ## Use this instead
20
+
21
+ ```bash
22
+ cd mcp-server && npm install
23
+
24
+ claude mcp add claude-phone --scope user \
25
+ --env PHONE_DEFAULT_TO=17510 \
26
+ -- node /absolute/path/to/mcp-server/index.js
27
+ ```
28
+
29
+ Restart Claude Code, then just ask:
30
+
31
+ > "Run the migration and call me if anything needs a decision."
32
+
33
+ Full documentation: **[MCP-SERVER.md](MCP-SERVER.md)**.
34
+
35
+ ## If you still want the old skill
36
+
37
+ It remains in git history. To recover it:
38
+
39
+ ```bash
40
+ git log --oneline -- docs/CLAUDE-CODE-SKILL.md
41
+ git show <commit>:docs/CLAUDE-CODE-SKILL.md > old-skill.md
42
+ ```
43
+
44
+ The underlying HTTP API it called (`POST /api/outbound-call`) is unchanged and
45
+ still supported — see [../voice-app/README-OUTBOUND.md](../voice-app/README-OUTBOUND.md).
@@ -0,0 +1,113 @@
1
+ # Languages — English, Hindi, Marathi
2
+
3
+ Speech is fully offline in all three. Whisper detects the language of each
4
+ utterance, and the reply comes back in that same language with a matching voice.
5
+
6
+ ## How it works
7
+
8
+ ```
9
+ you speak
10
+ → faster-whisper transcribes AND reports the language code (hi / mr / en)
11
+ → the code picks a Piper voice from LANG_VOICE_MAP
12
+ → Claude is instructed to reply in the same language
13
+ → Piper speaks it in that voice
14
+ ```
15
+
16
+ Detection is **per utterance**, so you can switch language mid-call and she
17
+ follows.
18
+
19
+ ## Configuration
20
+
21
+ ```bash
22
+ STT_LANGUAGE=auto # detect per utterance; or pin to en | hi | mr
23
+ SUPPORTED_LANGS=en,hi,mr # languages we will answer in
24
+ LANG_VOICE_MAP={"en":"en_US-lessac-medium","hi":"hi_IN-priyamvada-medium","mr":"mr_IN-google-medium"}
25
+ WHISPER_MODEL=medium # see accuracy below
26
+ ```
27
+
28
+ `SUPPORTED_LANGS` is a safety net, not decoration. Whisper can return any of
29
+ ~99 codes but only the installed voices exist on disk. A code outside this list
30
+ falls back to the device voice, so a misdetection — `ne` for Marathi is very
31
+ plausible, both are Devanagari — costs you the wrong accent rather than a 404
32
+ mid-call.
33
+
34
+ ## Voices
35
+
36
+ Downloaded automatically on first run into `data/voices/`:
37
+
38
+ | Language | Voice | Size |
39
+ |---|---|---|
40
+ | English | `en_US-lessac-medium` | 61 MB |
41
+ | Hindi | `hi_IN-priyamvada-medium` | 61 MB |
42
+ | Marathi | `mr_IN-google-medium` | 73 MB |
43
+
44
+ Change the set with `PIPER_VOICES` (space-separated) in `.env`; the entrypoint
45
+ resolves each name to its path in the
46
+ [rhasspy/piper-voices](https://huggingface.co/rhasspy/piper-voices) repo.
47
+
48
+ Other English voices worth trying: `en_US-amy-medium`, `en_US-kristin-medium`,
49
+ `en_US-hfc_female-medium`, `en_US-ryan-high`.
50
+
51
+ ```bash
52
+ curl http://127.0.0.1:9002/voices # what is actually installed
53
+ ```
54
+
55
+ ## Accuracy — read this before judging Marathi
56
+
57
+ Measured on this stack:
58
+
59
+ | Language | `small` | `medium` |
60
+ |---|---|---|
61
+ | English | good | very good |
62
+ | Hindi | fair | **very good** |
63
+ | Marathi | poor | **fair** |
64
+
65
+ Whisper's Marathi training data is thin compared with Hindi. Detection is
66
+ reliable in both; it is the *words* that suffer in Marathi.
67
+
68
+ `large-v3` improves Marathi noticeably but is ~3 GB and materially slower per
69
+ turn on CPU — a real cost on a live call, where every second is dead air.
70
+
71
+ ## The speed/accuracy trade-off
72
+
73
+ `WHISPER_MODEL` is the single biggest latency lever:
74
+
75
+ | Model | Transcription | Notes |
76
+ |---|---|---|
77
+ | `small` | ~4s | fine for English, weak on Marathi |
78
+ | `medium` | ~14s | needed for solid Hindi/Marathi |
79
+ | `large-v3` | ~40s+ | best accuracy, painful on a phone call |
80
+
81
+ If you mostly speak English, `small` makes calls feel dramatically snappier.
82
+ Keep `medium` while testing the Indian languages.
83
+
84
+ ## Making her reply in the right language
85
+
86
+ Transcription alone is not enough — Claude has to be told, or she will answer a
87
+ Hindi question in English. The device prompt in `data/config/devices.json`
88
+ handles it:
89
+
90
+ > ALWAYS reply in the same language the user just used, using that language's
91
+ > native script (Devanagari for Hindi and Marathi). Never transliterate.
92
+
93
+ The "never transliterate" clause matters: Piper's Hindi and Marathi voices read
94
+ **Devanagari**. Hand them romanised Hindi ("aap kaise hain") and you get an
95
+ English speaker mangling syllables.
96
+
97
+ ## Ending a call
98
+
99
+ Goodbye phrases are recognised in all three languages, including
100
+ `बंद करो`, `फोन बंद करो`, `ठेवतो`, `अलविदा`, `बाय`, plus romanised forms
101
+ (`band karo`, `alvida`). See `goodbyePhrases` in `voice-app/lib/sip-handler.js`.
102
+
103
+ ## Adding another language
104
+
105
+ 1. Find a voice in the [Piper catalogue](https://huggingface.co/rhasspy/piper-voices)
106
+ (56 languages available).
107
+ 2. Add its name to `PIPER_VOICES` in `.env`.
108
+ 3. Add the Whisper code → voice mapping to `LANG_VOICE_MAP`.
109
+ 4. Add the code to `SUPPORTED_LANGS`.
110
+ 5. Add goodbye phrases for it in `sip-handler.js`.
111
+ 6. `docker compose restart claude-phone` — the entrypoint downloads it on boot.
112
+
113
+ No image rebuild is needed; voices live on the `./data` volume.
@@ -0,0 +1,111 @@
1
+ # MCP Server — let Claude phone you
2
+
3
+ Gives a Claude Code session a phone. It can ring you mid-task, say why, and —
4
+ in conversation mode — talk the decision through before carrying on.
5
+
6
+ Useful when Claude is doing something long and hits a fork in the road: a
7
+ migration that needs a call, a deploy that wants confirmation, a test suite that
8
+ went red in a way worth discussing.
9
+
10
+ ## How it fits
11
+
12
+ The MCP server does **no SIP work**. It is a thin wrapper over the voice-app
13
+ HTTP API that already existed:
14
+
15
+ ```
16
+ Claude Code session
17
+ │ MCP (stdio)
18
+ ▼
19
+ mcp-server/index.js
20
+ │ HTTP
21
+ ▼
22
+ voice-app /api/outbound-call → drachtio → FreeSWITCH → 3CX → your phone
23
+ ```
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ cd mcp-server && npm install
29
+
30
+ claude mcp add claude-phone --scope user \
31
+ --env VOICE_APP_URL=http://127.0.0.1:3000 \
32
+ --env PHONE_DEFAULT_TO=17510 \
33
+ -- node /absolute/path/to/mcp-server/index.js
34
+ ```
35
+
36
+ Then **restart Claude Code** and confirm:
37
+
38
+ ```bash
39
+ claude mcp list
40
+ # claude-phone: node …/mcp-server/index.js - ✔ Connected
41
+ ```
42
+
43
+ | Env | Purpose |
44
+ |---|---|
45
+ | `VOICE_APP_URL` | Where voice-app listens (default `http://127.0.0.1:3000`) |
46
+ | `PHONE_DEFAULT_TO` | Number/extension to ring when `to` is omitted |
47
+
48
+ ## Tools
49
+
50
+ ### `call_me`
51
+
52
+ Rings you and speaks a message.
53
+
54
+ | Arg | Required | Notes |
55
+ |---|---|---|
56
+ | `message` | yes | What to say. One or two spoken sentences, no markdown. |
57
+ | `mode` | no | `announce` (say it, hang up) or `converse` (stay on the line) |
58
+ | `to` | no | Defaults to `PHONE_DEFAULT_TO` |
59
+ | `context` | no | Background so the voice agent can field follow-ups |
60
+
61
+ ### `call_status`
62
+
63
+ Poll a call by `callId` — whether it is still up and how it ended.
64
+
65
+ ### `hangup`
66
+
67
+ End a call that is still in progress.
68
+
69
+ ## Using it
70
+
71
+ Just ask in plain language:
72
+
73
+ > "Run the migration, and call me if anything needs a decision."
74
+
75
+ > "This test suite takes 20 minutes — phone me when it finishes with a summary."
76
+
77
+ Claude picks the tool itself. For a back-and-forth, ask for conversation mode:
78
+
79
+ > "Call me and talk me through the options — I want to answer, not just listen."
80
+
81
+ ## Writing a good message
82
+
83
+ It is **spoken aloud**, so:
84
+
85
+ - Keep it to one or two sentences.
86
+ - No markdown, code blocks, paths, or long identifiers — they sound terrible.
87
+ - Lead with the decision, not the backstory:
88
+ *"The migration is ready but will drop two columns — should I continue?"*
89
+ - Use `context` for the detail. It never gets read out, but the voice agent can
90
+ draw on it when you ask a follow-up question.
91
+
92
+ ## Notes and limits
93
+
94
+ - **voice-app must be running** (`docker compose up -d`) — the MCP server only
95
+ proxies to it.
96
+ - **`announce` does not wait.** It returns as soon as the call starts. Poll
97
+ `call_status` if you need the outcome.
98
+ - **If you do not answer**, the call ends on the ring timeout
99
+ (`OUTBOUND_RING_TIMEOUT`, default 30s) and `call_status` reports it. Claude
100
+ should carry on rather than block.
101
+ - Errors come back as tool failures with the HTTP status, so Claude can react
102
+ instead of crashing.
103
+
104
+ ## Troubleshooting
105
+
106
+ | Symptom | Check |
107
+ |---|---|
108
+ | `✘ Failed to connect` in `claude mcp list` | `cd mcp-server && npm install`; use an absolute path in the registration |
109
+ | `claude-phone: 500 …` | Is voice-app up? `curl http://127.0.0.1:3000/api/devices` |
110
+ | Phone never rings | SIP registration — `docker logs claude-phone \| grep MULTI-REGISTRAR` |
111
+ | Rings, then silence | Same RTP/SDP checks as inbound: [TROUBLESHOOTING.md](TROUBLESHOOTING.md) |