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,288 @@
1
+ # Troubleshooting
2
+
3
+ Every failure mode below was hit for real on a Windows 11 + Docker Desktop +
4
+ native 3CX SBC setup. Each entry gives the symptom, the log line that identifies
5
+ it, and the fix.
6
+
7
+ ## First: where to look
8
+
9
+ ```bash
10
+ docker compose logs -f # everything
11
+ docker logs claude-phone 2>&1 | grep -Ei "CALL |error|503"
12
+ curl http://127.0.0.1:3333/health # host Claude wrapper
13
+ ```
14
+
15
+ Inside the container:
16
+
17
+ ```bash
18
+ docker exec claude-phone bash -c 'IP=$(hostname -i|awk "{print \$1}"); \
19
+ for p in 5070 5080 8021 3000 9001 9002; do \
20
+ (echo >/dev/tcp/$IP/$p) 2>/dev/null && echo "$p UP" || echo "$p DOWN"; done'
21
+ ```
22
+
23
+ `3000`, `9001`, `9002` bind to loopback by design and will read DOWN on the
24
+ container IP — that is correct, not a fault.
25
+
26
+ ---
27
+
28
+ ## Call goes straight to voicemail
29
+
30
+ **Symptom:** "Please record your message." The app never answers.
31
+
32
+ **Log:**
33
+ ```
34
+ Connection refused (111) with tcp/[…]:5080
35
+ CALL Error: Sip non-success response: 503
36
+ ```
37
+
38
+ drachtio could not reach FreeSWITCH, so it returned 503 and 3CX fell through to
39
+ voicemail.
40
+
41
+ **Cause and fix.** FreeSWITCH binds sofia to the **container IP**, never to
42
+ loopback, and it must *advertise* the address it actually listens on. Check the
43
+ two do not disagree:
44
+
45
+ ```bash
46
+ docker exec claude-phone sh -c 'grep -E "ext_sip_ip|ext_rtp_ip" \
47
+ /usr/local/freeswitch/conf/vars_diff.xml'
48
+ ```
49
+
50
+ Expected — they are deliberately different:
51
+
52
+ ```
53
+ ext_rtp_ip=172.16.14.225 <- your LAN IP, so the SBC can reach the media
54
+ ext_sip_ip=172.18.0.2 <- the container IP, where sofia actually listens
55
+ ```
56
+
57
+ If `ext_sip_ip` is your LAN IP or `127.0.0.1`, drachtio dials a port with nothing
58
+ behind it. `docker/entrypoint.sh` computes the container IP at runtime; confirm
59
+ it ran:
60
+
61
+ ```
62
+ [bootstrap] FreeSWITCH SIP will advertise 172.18.0.2; RTP will advertise 172.16.14.225
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Call connects but there is no audio at all
68
+
69
+ **Symptom:** the call is up, no greeting, and nothing you say registers.
70
+
71
+ **Check the SDP** that FreeSWITCH sent back:
72
+
73
+ ```bash
74
+ docker logs claude-phone 2>&1 | grep "c=IN IP4"
75
+ ```
76
+
77
+ - `c=IN IP4 172.16.14.225` (your LAN IP) — correct.
78
+ - `c=IN IP4 172.18.0.x` (a Docker IP) — **broken**. The SBC is a native Windows
79
+ process and cannot route to the Docker bridge, so RTP dies in both directions.
80
+
81
+ **Cause.** FreeSWITCH's `nat.auto` ACL treats `172.16.0.0/12` as "local". Your
82
+ LAN (`172.16.x`) *and* the Docker bridge (`172.18.x`) both fall inside that
83
+ range, so FreeSWITCH decides the SBC is local and skips external-IP
84
+ substitution.
85
+
86
+ **Fix** — applied at build time in the `Dockerfile`; verify it stuck:
87
+
88
+ ```bash
89
+ docker exec claude-phone grep -E "local-network-acl|apply-nat-acl" \
90
+ /usr/local/freeswitch/conf/sip_profiles/mrf.xml
91
+ ```
92
+
93
+ You want `local-network-acl = none` present and **no** `apply-nat-acl` line.
94
+
95
+ Also confirm the RTP ports are published: `30000-30100/udp` in
96
+ `docker-compose.yml`.
97
+
98
+ ---
99
+
100
+ ## Greeting plays, then nothing — she never answers
101
+
102
+ **Symptom:** you hear the greeting and the beep, you speak, and nothing happens
103
+ until the call times out.
104
+
105
+ **Log — the tell:**
106
+ ```
107
+ LISTEN Got: … bytes
108
+ (no "Finalizing utterance" line ever appears)
109
+ ```
110
+
111
+ or
112
+
113
+ ```
114
+ VAD: isSpeech=true, inSpeech=true, silenceMs=0, RMS=1800, floor=…
115
+ ```
116
+ repeating, with `silenceMs` never rising.
117
+
118
+ **Cause.** The VAD never sees end-of-speech. Fixed thresholds assume the line
119
+ goes to digital silence (RMS 0) between words. Through a PBX/SBC the gaps carry
120
+ a **noise floor of roughly RMS 1000–2500**, which sits above a fixed threshold,
121
+ so every frame classifies as speech and the utterance is never finalized.
122
+
123
+ **Fix.** The VAD learns the floor as a rolling 20th percentile and requires
124
+ speech to exceed it by `VAD_NOISE_MULT`. If it still misbehaves, read the
125
+ `floor=` value in the logs and adjust:
126
+
127
+ ```bash
128
+ VAD_NOISE_MULT=3.0 # she keeps "hearing" your noise floor -> raise
129
+ VAD_NOISE_MULT=1.8 # she misses quiet speech -> lower
130
+ ```
131
+
132
+ > An EMA that only updates on "silent" frames **cannot** fix this: if the floor
133
+ > already exceeds the threshold, no frame is ever classified silent, so the floor
134
+ > never updates. The measurement must not depend on the decision it feeds.
135
+
136
+ ---
137
+
138
+ ## Answers are slow (30–60s of dead air)
139
+
140
+ Break the delay down from the timestamps:
141
+
142
+ ```bash
143
+ docker logs claude-phone 2>&1 | grep -E "LISTEN Got|WHISPER |CLAUDE Query|VOICE:"
144
+ ```
145
+
146
+ | Stage | Healthy | If it is slower |
147
+ |---|---|---|
148
+ | `LISTEN Got` → `WHISPER` | 4s (`small`) / 14s (`medium`) | drop `WHISPER_MODEL` to `small` |
149
+ | `CLAUDE Query` → `VOICE:` | 20–45s | see below |
150
+
151
+ **The big one: MCP startup.** Every turn spawns a fresh `claude` process. Without
152
+ `--strict-mcp-config` it first connects to *every* configured MCP server —
153
+ including remote HTTP ones needing auth, which simply time out. That added ~30s
154
+ to every answer.
155
+
156
+ This is on by default now. To confirm the flag is present:
157
+
158
+ ```bash
159
+ grep -n "strict-mcp-config" claude-api-server/server.js
160
+ ```
161
+
162
+ Set `PHONE_ENABLE_MCP=1` only if you deliberately want MCP tools by phone, and
163
+ accept the latency.
164
+
165
+ **Diagnostic trick:** send a deliberately invalid request. If it fails in ~1s
166
+ while a valid one takes 60s, the gap is process startup (MCP), not reasoning.
167
+
168
+ ---
169
+
170
+ ## She says she cannot check your PC
171
+
172
+ **Symptom:** *"I'm running on the server side, not your computer."* — untrue. She
173
+ runs as Claude Code **on your PC** with full shell access.
174
+
175
+ **Fix.** The device prompt must tell her so. In `data/config/devices.json`:
176
+
177
+ ```json
178
+ "prompt": "You are Maya… You are running as Claude Code directly on the user's
179
+ Windows PC, with full shell and file access. When asked about their PC, actually
180
+ RUN a command and report the real result. Never say you cannot check."
181
+ ```
182
+
183
+ Restart the container (config is a volume, no rebuild needed):
184
+ ```bash
185
+ docker compose restart claude-phone
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Container restart-loops immediately
191
+
192
+ **Log:**
193
+ ```
194
+ /usr/bin/env: 'bash\r': No such file or directory
195
+ ```
196
+
197
+ **Cause.** CRLF line endings — a Windows checkout or a Windows editor saved
198
+ `docker/entrypoint.sh` with `\r\n`, making the shebang `bash\r`.
199
+
200
+ **Fix.** The `Dockerfile` strips CR at build time and `.gitattributes` forces LF.
201
+ If you hit it anyway:
202
+
203
+ ```bash
204
+ sed -i 's/\r$//' docker/entrypoint.sh docker/supervisord.conf
205
+ docker compose build && docker compose up -d
206
+ ```
207
+
208
+ ---
209
+
210
+ ## SIP registration fails
211
+
212
+ **403 Invalid credentials** — you are registering against the wrong host. Send
213
+ REGISTER to the **local SBC**, not the cloud tenant:
214
+
215
+ ```
216
+ SIP_DOMAIN=1752.3cx.cloud # identity, used in From/To
217
+ SIP_REGISTRAR=172.16.14.225 # where REGISTER actually goes (the local SBC)
218
+ ```
219
+
220
+ **Port 5060 already in use** — the 3CX SBC owns it. drachtio uses **5070**; keep
221
+ `DRACHTIO_SIP_PORT=5070` and the matching published port.
222
+
223
+ Verify registration:
224
+ ```bash
225
+ docker logs claude-phone 2>&1 | grep MULTI-REGISTRAR
226
+ # [MULTI-REGISTRAR] Maya SUCCESS - Registered as ext 17512
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Models re-download on every restart
232
+
233
+ `./data` is not persisting. A healthy restart logs:
234
+
235
+ ```
236
+ [bootstrap] voice en_US-lessac-medium already present
237
+ [bootstrap] whisper model cached
238
+ [bootstrap] devices.json already present (left untouched)
239
+ ```
240
+
241
+ Check the bind mount is `./data:/data` in `docker-compose.yml`, that `data/` is
242
+ in `.gitignore`, and that Docker Desktop has file sharing enabled for the drive.
243
+
244
+ ---
245
+
246
+ ## Wrong language, or garbled Hindi/Marathi
247
+
248
+ ```bash
249
+ docker logs claude-phone 2>&1 | grep "WHISPER \["
250
+ # WHISPER [hi -> voice hi_IN-priyamvada-medium]: "…"
251
+ ```
252
+
253
+ - **Wrong language detected** — pin it: `STT_LANGUAGE=hi` (or `mr`, `en`).
254
+ - **Right language, garbled words** — `WHISPER_MODEL=medium` at minimum.
255
+ Marathi is the weakest of the three; `large-v3` helps but is ~3 GB and much
256
+ slower per turn on CPU.
257
+ - **Correct text, wrong voice** — check `LANG_VOICE_MAP` and that the model
258
+ exists: `curl http://127.0.0.1:9002/voices`.
259
+
260
+ ---
261
+
262
+ ## She talks over me / I cannot interrupt her
263
+
264
+ Barge-in is deliberately hard to trigger so a cough or echo cannot cut her off.
265
+
266
+ ```bash
267
+ docker logs claude-phone 2>&1 | grep "BARGE-IN"
268
+ ```
269
+
270
+ - **Never triggers** — lower `BARGE_MULT` (1.6 → 1.3) or `BARGE_MIN_MS` (320 → 200).
271
+ - **Triggers on its own** — raise them. If it fires while *she* is speaking, the
272
+ audio fork is carrying her own audio back; confirm `mixType: 'mono'` in
273
+ `sip-handler.js`.
274
+
275
+ ---
276
+
277
+ ## Falling back to the old layout
278
+
279
+ The previous five-container setup is preserved and still works:
280
+
281
+ ```bash
282
+ docker compose down
283
+ docker compose -f docker-compose.multi.yml up -d
284
+ ```
285
+
286
+ Note it needs the container-DNS env values (`DRACHTIO_HOST=drachtio`,
287
+ `FREESWITCH_HOST=freeswitch`, `AUDIO_BASE_URL=http://voice-app:3000`), which the
288
+ unified `docker-compose.yml` overrides to `127.0.0.1`.
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * claude-phone MCP server.
4
+ *
5
+ * Gives a Claude Code session a phone: it can ring the user mid-task, say why,
6
+ * and (in conversation mode) talk the decision through, then carry on.
7
+ *
8
+ * Wraps the voice-app HTTP API - it does no SIP work of its own.
9
+ *
10
+ * VOICE_APP_URL where voice-app listens (default http://127.0.0.1:3000)
11
+ * PHONE_DEFAULT_TO number/extension to ring (default from .env SIP_EXTENSION)
12
+ */
13
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
14
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
16
+
17
+ const VOICE_APP_URL = process.env.VOICE_APP_URL || 'http://127.0.0.1:3000';
18
+ const DEFAULT_TO = process.env.PHONE_DEFAULT_TO || '';
19
+
20
+ async function api(path, options = {}) {
21
+ const res = await fetch(`${VOICE_APP_URL}${path}`, {
22
+ headers: { 'Content-Type': 'application/json' },
23
+ ...options,
24
+ });
25
+ const text = await res.text();
26
+ let body;
27
+ try { body = JSON.parse(text); } catch { body = { raw: text }; }
28
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${text.slice(0, 300)}`);
29
+ return body;
30
+ }
31
+
32
+ const TOOLS = [
33
+ {
34
+ name: 'call_me',
35
+ description:
36
+ 'Ring the user on the phone and speak a message. Use when you need a decision, ' +
37
+ 'hit a blocker, or finished something they asked to be told about.\n\n' +
38
+ 'RETURNS IMMEDIATELY - it does not wait for the call to finish, so keep working ' +
39
+ 'on the task while the phone rings.\n\n' +
40
+ 'mode="conversation" keeps the line open so they can answer back and the voice ' +
41
+ 'agent talks with them. To hear what they said, call call_status with the ' +
42
+ 'returned callId once you reach a point where their answer matters. If they do ' +
43
+ 'not pick up or say nothing, the call ends by itself and call_status reports it.',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: {
47
+ message: {
48
+ type: 'string',
49
+ description: 'What to say when they pick up. One or two spoken sentences, no markdown.',
50
+ },
51
+ mode: {
52
+ type: 'string',
53
+ enum: ['announce', 'conversation'],
54
+ description:
55
+ 'announce = say it and hang up. conversation = stay on the line so they can reply.',
56
+ default: 'announce',
57
+ },
58
+ to: {
59
+ type: 'string',
60
+ description: 'Number or extension to ring. Defaults to the configured one.',
61
+ },
62
+ context: {
63
+ type: 'string',
64
+ description:
65
+ 'Background for the voice agent so it can answer follow-up questions. ' +
66
+ 'Never spoken aloud - put the detail here, not in message.',
67
+ },
68
+ },
69
+ required: ['message'],
70
+ },
71
+ },
72
+ {
73
+ name: 'call_status',
74
+ description:
75
+ 'Check a call started with call_me: whether it is still up, and in conversation ' +
76
+ 'mode what the user actually said. Returns conversationHistory with each turn. ' +
77
+ 'Poll this when you need their answer - do not block waiting for it.',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: { callId: { type: 'string', description: 'The callId returned by call_me.' } },
81
+ required: ['callId'],
82
+ },
83
+ },
84
+ {
85
+ name: 'hangup',
86
+ description: 'End a call that is still in progress.',
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: { callId: { type: 'string' } },
90
+ required: ['callId'],
91
+ },
92
+ },
93
+ ];
94
+
95
+ const server = new Server(
96
+ { name: 'claude-phone', version: '1.0.0' },
97
+ { capabilities: { tools: {} } }
98
+ );
99
+
100
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
101
+
102
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
103
+ const { name, arguments: args = {} } = req.params;
104
+ try {
105
+ if (name === 'call_me') {
106
+ const to = args.to || DEFAULT_TO;
107
+ if (!to) throw new Error('No destination: pass "to" or set PHONE_DEFAULT_TO.');
108
+
109
+ // The API only accepts 'announce' | 'conversation'. Accept the looser words a
110
+ // model might reach for rather than silently sending an unknown mode, which
111
+ // matches neither branch and leaves the call hanging.
112
+ const raw = (args.mode || 'announce').toLowerCase();
113
+ const mode = ['conversation', 'converse', 'talk', 'discuss'].includes(raw)
114
+ ? 'conversation'
115
+ : 'announce';
116
+
117
+ const body = {
118
+ to,
119
+ message: args.message,
120
+ mode,
121
+ ...(args.context ? { context: args.context } : {}),
122
+ };
123
+ const out = await api('/api/outbound-call', { method: 'POST', body: JSON.stringify(body) });
124
+ const callId = out.callId || 'unknown';
125
+
126
+ return {
127
+ content: [{
128
+ type: 'text',
129
+ text:
130
+ `Calling ${to} now (callId=${callId}, status=${out.status || 'started'}). ` +
131
+ 'This returned immediately - carry on with the task while it rings.' +
132
+ (mode === 'conversation'
133
+ ? `\n\nConversation mode: they can reply. When you need their answer, call ` +
134
+ `call_status with callId=${callId} and read conversationHistory. If they ` +
135
+ 'do not answer or stay silent, the call ends on its own.'
136
+ : '\n\nAnnounce mode: it speaks the message and hangs up. No reply is collected.'),
137
+ }],
138
+ };
139
+ }
140
+
141
+ if (name === 'call_status') {
142
+ const out = await api(`/api/call/${encodeURIComponent(args.callId)}`);
143
+ return { content: [{ type: 'text', text: JSON.stringify(out, null, 2) }] };
144
+ }
145
+
146
+ if (name === 'hangup') {
147
+ const out = await api(`/api/call/${encodeURIComponent(args.callId)}/hangup`, { method: 'POST' });
148
+ return { content: [{ type: 'text', text: JSON.stringify(out, null, 2) }] };
149
+ }
150
+
151
+ throw new Error(`Unknown tool: ${name}`);
152
+ } catch (err) {
153
+ // Surfaced to the model as a failure it can react to, not a crash.
154
+ return { isError: true, content: [{ type: 'text', text: `claude-phone: ${err.message}` }] };
155
+ }
156
+ });
157
+
158
+ await server.connect(new StdioServerTransport());
159
+ console.error('[claude-phone-mcp] ready, voice-app at ' + VOICE_APP_URL);