@tina_summer/codepocket 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.
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Simple MCP client — connects to the running codepocket MCP server via SSE,
3
+ * sends tool calls, and returns results.
4
+ */
5
+
6
+ const http = require('http')
7
+ const fs = require('fs')
8
+ const path = require('path')
9
+ const readline = require('readline')
10
+ const os = require('os')
11
+
12
+ const MCP_PORTS = Array.from({ length: 10 }, (_, i) => 3007 + i)
13
+ const TRACK_FILE = path.join(os.tmpdir(), 'codepocket-last-track.json')
14
+
15
+ // ── MCP Client ──
16
+
17
+ class McpClient {
18
+ constructor(port) {
19
+ this.port = port
20
+ this.baseUrl = `http://127.0.0.1:${port}`
21
+ this.sseBuffer = ''
22
+ this.pending = new Map() // id -> { resolve, reject }
23
+ this.nextId = 1
24
+ this.connected = false
25
+ }
26
+
27
+ /** Discover and connect to a running MCP server */
28
+ static async connect() {
29
+ for (const port of MCP_PORTS) {
30
+ try {
31
+ const res = await new Promise((resolve, reject) => {
32
+ const req = http.get(`http://127.0.0.1:${port}/sse`, (res) => {
33
+ resolve(res)
34
+ })
35
+ req.on('error', reject)
36
+ req.setTimeout(2000, () => { req.destroy(); reject(new Error('timeout')) })
37
+ })
38
+
39
+ if (res.statusCode === 200) {
40
+ const client = new McpClient(port)
41
+ await client._startSSE(res)
42
+ return client
43
+ }
44
+ res.resume()
45
+ } catch {}
46
+ }
47
+ throw new Error('未找到运行中的 MCP server (tried ports 3007-3016)')
48
+ }
49
+
50
+ _startSSE(res) {
51
+ return new Promise((resolve, reject) => {
52
+ let buffer = ''
53
+ res.setEncoding('utf-8')
54
+ res.on('data', (chunk) => {
55
+ buffer += chunk
56
+ const lines = buffer.split('\n')
57
+ buffer = lines.pop() || ''
58
+
59
+ for (const line of lines) {
60
+ if (line.startsWith('data: ')) {
61
+ const data = line.slice(6).trim()
62
+ if (data.startsWith('http')) {
63
+ // endpoint event — connected
64
+ this.connected = true
65
+ resolve()
66
+ continue
67
+ }
68
+ try {
69
+ const msg = JSON.parse(data)
70
+ if (msg.id && this.pending.has(msg.id)) {
71
+ const { resolve: done } = this.pending.get(msg.id)
72
+ this.pending.delete(msg.id)
73
+ done(msg)
74
+ }
75
+ } catch {}
76
+ }
77
+ }
78
+ })
79
+ res.on('error', reject)
80
+ res.on('end', () => {
81
+ this.connected = false
82
+ // Reject all pending
83
+ for (const [id, { reject }] of this.pending) {
84
+ this.pending.delete(id)
85
+ reject(new Error('SSE connection closed'))
86
+ }
87
+ })
88
+ // Timeout for initial connection
89
+ setTimeout(() => {
90
+ if (!this.connected) reject(new Error('SSE connection timeout'))
91
+ }, 3000)
92
+ })
93
+ }
94
+
95
+ /** Call an MCP tool and return the parsed result */
96
+ async call(toolName, args = {}) {
97
+ const id = this.nextId++
98
+ const msg = {
99
+ jsonrpc: '2.0',
100
+ id,
101
+ method: 'tools/call',
102
+ params: { name: toolName, arguments: args },
103
+ }
104
+
105
+ return new Promise((resolve, reject) => {
106
+ const timer = setTimeout(() => {
107
+ this.pending.delete(id)
108
+ reject(new Error(`Tool call "${toolName}" timed out (60s)`))
109
+ }, 60000)
110
+
111
+ this.pending.set(id, {
112
+ resolve: (msg) => {
113
+ clearTimeout(timer)
114
+ resolve(msg)
115
+ },
116
+ reject: (err) => {
117
+ clearTimeout(timer)
118
+ reject(err)
119
+ },
120
+ })
121
+
122
+ const body = JSON.stringify(msg)
123
+ const req = http.request(`${this.baseUrl}/message`, {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
126
+ }, (res) => {
127
+ res.resume()
128
+ if (res.statusCode !== 202) {
129
+ this.pending.delete(id)
130
+ clearTimeout(timer)
131
+ reject(new Error(`Unexpected status: ${res.statusCode}`))
132
+ }
133
+ })
134
+ req.on('error', (err) => {
135
+ this.pending.delete(id)
136
+ clearTimeout(timer)
137
+ reject(err)
138
+ })
139
+ req.write(body)
140
+ req.end()
141
+ })
142
+ }
143
+
144
+ /** Extract tool result from MCP response */
145
+ static extractResult(mcpMsg) {
146
+ try {
147
+ const text = mcpMsg.result?.content?.[0]?.text
148
+ if (text) return JSON.parse(text)
149
+ } catch {}
150
+ return null
151
+ }
152
+ }
153
+
154
+ // ── Test Commands ──
155
+
156
+ async function cmdLocation() {
157
+ const client = await McpClient.connect()
158
+ console.log('获取位置中...')
159
+ const resp = await client.call('phone_location_get')
160
+ const result = McpClient.extractResult(resp)
161
+ if (result?.accepted && result.result) {
162
+ const loc = result.result
163
+ console.log(` 纬度: ${loc.latitude}`)
164
+ console.log(` 经度: ${loc.longitude}`)
165
+ console.log(` 精度: ${loc.accuracy}m`)
166
+ } else {
167
+ console.error(' 获取失败:', result?.error || '未知错误')
168
+ }
169
+ }
170
+
171
+ async function cmdTrack() {
172
+ const client = await McpClient.connect()
173
+ console.log('开始追踪,手机上确认...')
174
+ const resp = await client.call('phone_location_track')
175
+ const result = McpClient.extractResult(resp)
176
+ if (result?.accepted && result.result?.success) {
177
+ console.log(' 追踪已开始,走动一会儿后运行:')
178
+ console.log(' codepocket test stop')
179
+ } else {
180
+ console.error(' 启动失败:', result?.error || '未知错误')
181
+ }
182
+ }
183
+
184
+ async function cmdStop() {
185
+ const client = await McpClient.connect()
186
+ console.log('停止追踪...')
187
+ const resp = await client.call('phone_location_stop')
188
+ const result = McpClient.extractResult(resp)
189
+ if (result?.accepted && result.result?.success) {
190
+ const { points, count, debug } = result.result
191
+ const duration = count >= 2
192
+ ? ((points[points.length - 1].timestamp - points[0].timestamp) / 1000).toFixed(1) + 's'
193
+ : 'N/A'
194
+
195
+ console.log(` 采集点数: ${count}`)
196
+ console.log(` 时长: ${duration}`)
197
+
198
+ if (debug?.length) {
199
+ console.log(' 调试日志:')
200
+ debug.forEach((d) => console.log(` ${d}`))
201
+ }
202
+
203
+ // Save track data for map command
204
+ if (count >= 2) {
205
+ fs.writeFileSync(TRACK_FILE, JSON.stringify({ points, count, savedAt: Date.now() }))
206
+ console.log(`\n 轨迹已保存,运行下面命令查看地图:`)
207
+ console.log(' codepocket test map')
208
+ }
209
+ } else {
210
+ console.error(' 停止失败:', result?.error || '未知错误')
211
+ }
212
+ }
213
+
214
+ async function cmdMap() {
215
+ // Load saved track data
216
+ if (!fs.existsSync(TRACK_FILE)) {
217
+ console.error(' 未找到轨迹数据,请先运行 codepocket test track → stop')
218
+ process.exit(1)
219
+ }
220
+
221
+ const track = JSON.parse(fs.readFileSync(TRACK_FILE, 'utf-8'))
222
+ const { points } = track
223
+
224
+ console.log(` 加载 ${points.length} 个轨迹点...`)
225
+
226
+ const polylinePoints = points.map((p) => ({
227
+ latitude: p.latitude,
228
+ longitude: p.longitude,
229
+ timestamp: p.timestamp,
230
+ }))
231
+
232
+ const markers = [
233
+ { latitude: points[0].latitude, longitude: points[0].longitude, title: '起点' },
234
+ { latitude: points[points.length - 1].latitude, longitude: points[points.length - 1].longitude, title: '终点' },
235
+ ]
236
+
237
+ const client = await McpClient.connect()
238
+ console.log(' 发送地图到手机...')
239
+ const resp = await client.call('phone_map_show', {
240
+ polyline: [{ points: polylinePoints, color: '#00C853', width: 4 }],
241
+ markers,
242
+ title: '轨迹地图',
243
+ })
244
+
245
+ const result = McpClient.extractResult(resp)
246
+ if (result?.accepted) {
247
+ console.log(' 地图已显示到手机!')
248
+ } else {
249
+ console.error(' 显示失败:', result?.error || '未知错误')
250
+ }
251
+ }
252
+
253
+ async function cmdFlow() {
254
+ const client = await McpClient.connect()
255
+
256
+ // Step 1: Track
257
+ console.log('\n[1/3] 开始追踪,手机上确认...')
258
+ const trackResp = await client.call('phone_location_track')
259
+ const trackResult = McpClient.extractResult(trackResp)
260
+ if (!trackResult?.accepted || !trackResult.result?.success) {
261
+ console.error(' 追踪启动失败:', trackResult?.error || '未知错误')
262
+ process.exit(1)
263
+ }
264
+ console.log(' 追踪中...')
265
+
266
+ // Step 2: Wait for user
267
+ console.log('\n 走动一会儿,按回车停止追踪...')
268
+ await new Promise((resolve) => {
269
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
270
+ rl.on('line', () => { rl.close(); resolve() })
271
+ })
272
+
273
+ // Step 3: Stop
274
+ console.log('\n[2/3] 停止追踪...')
275
+ const stopResp = await client.call('phone_location_stop')
276
+ const stopResult = McpClient.extractResult(stopResp)
277
+ if (!stopResult?.accepted || !stopResult.result?.success) {
278
+ console.error(' 停止失败:', stopResult?.error || '未知错误')
279
+ process.exit(1)
280
+ }
281
+
282
+ const { points, count } = stopResult.result
283
+ const duration = count >= 2
284
+ ? ((points[points.length - 1].timestamp - points[0].timestamp) / 1000).toFixed(1) + 's'
285
+ : 'N/A'
286
+ console.log(` ${count} 个点, ${duration}`)
287
+
288
+ if (count < 2) {
289
+ console.error(' 点数不足,无法画地图')
290
+ process.exit(1)
291
+ }
292
+
293
+ // Save track
294
+ fs.writeFileSync(TRACK_FILE, JSON.stringify({ points, count, savedAt: Date.now() }))
295
+
296
+ // Step 4: Map
297
+ console.log('\n[3/3] 画地图...')
298
+ const polylinePoints = points.map((p) => ({
299
+ latitude: p.latitude,
300
+ longitude: p.longitude,
301
+ timestamp: p.timestamp,
302
+ }))
303
+ const markers = [
304
+ { latitude: points[0].latitude, longitude: points[0].longitude, title: '起点' },
305
+ { latitude: points[points.length - 1].latitude, longitude: points[points.length - 1].longitude, title: '终点' },
306
+ ]
307
+
308
+ const mapResp = await client.call('phone_map_show', {
309
+ polyline: [{ points: polylinePoints, color: '#00C853', width: 4 }],
310
+ markers,
311
+ title: '轨迹地图',
312
+ })
313
+
314
+ const mapResult = McpClient.extractResult(mapResp)
315
+ if (mapResult?.accepted) {
316
+ console.log(' 地图已显示到手机!')
317
+ } else {
318
+ console.error(' 显示失败:', mapResult?.error || '未知错误')
319
+ }
320
+ }
321
+
322
+ // ── CLI Entry ──
323
+
324
+ const COMMANDS = { location: cmdLocation, track: cmdTrack, stop: cmdStop, map: cmdMap, flow: cmdFlow }
325
+
326
+ async function runTestCli(args) {
327
+ const cmd = args[0]
328
+ if (!cmd || !COMMANDS[cmd]) {
329
+ console.log('用法: codepocket test <command>')
330
+ console.log('')
331
+ console.log('命令:')
332
+ console.log(' location 获取当前位置')
333
+ console.log(' track 开始位置追踪')
334
+ console.log(' stop 停止追踪并保存轨迹')
335
+ console.log(' map 用最近轨迹画地图')
336
+ console.log(' flow 全流程: 追踪 → 停止 → 画地图')
337
+ process.exit(cmd ? 1 : 0)
338
+ }
339
+
340
+ try {
341
+ await COMMANDS[cmd]()
342
+ } catch (err) {
343
+ console.error(`\n [!] ${err.message}`)
344
+ process.exit(1)
345
+ }
346
+ }
347
+
348
+ module.exports = { runTestCli }
@@ -0,0 +1,331 @@
1
+ const http = require('http')
2
+
3
+ // Tool definitions — each tool's JSON Schema for MCP tools/list
4
+ const TOOLS = [
5
+ {
6
+ name: 'phone_calendar_add',
7
+ description: 'Add an event to the phone system calendar. The user will confirm on their phone before the event is written.',
8
+ inputSchema: {
9
+ type: 'object',
10
+ properties: {
11
+ title: { type: 'string', description: 'Event title' },
12
+ startTime: { type: 'number', description: 'Start time as Unix timestamp in milliseconds' },
13
+ endTime: { type: 'number', description: 'End time as Unix timestamp in milliseconds (optional)' },
14
+ description: { type: 'string', description: 'Event description (optional)' },
15
+ reminderAlarm: { type: 'number', description: 'Minutes before event to trigger reminder (optional, default 0)' },
16
+ },
17
+ required: ['title', 'startTime'],
18
+ },
19
+ },
20
+ {
21
+ name: 'phone_calendar_query',
22
+ description: 'Query calendar events. Note: the phone cannot read system calendar events; this returns cached data if available.',
23
+ inputSchema: {
24
+ type: 'object',
25
+ properties: {
26
+ startDate: { type: 'number', description: 'Start of range as Unix timestamp ms' },
27
+ endDate: { type: 'number', description: 'End of range as Unix timestamp ms' },
28
+ },
29
+ },
30
+ },
31
+ {
32
+ name: 'phone_todo_add',
33
+ description: 'Add a todo item. The user will confirm on their phone.',
34
+ inputSchema: {
35
+ type: 'object',
36
+ properties: {
37
+ title: { type: 'string', description: 'Todo title' },
38
+ dueDate: { type: 'number', description: 'Due date as Unix timestamp ms (optional)' },
39
+ priority: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Priority level (optional)' },
40
+ },
41
+ required: ['title'],
42
+ },
43
+ },
44
+ {
45
+ name: 'phone_todo_list',
46
+ description: 'List todo items stored on the phone.',
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ filter: { type: 'string', enum: ['all', 'pending', 'completed'], description: 'Filter todos by status (default: pending)' },
51
+ },
52
+ },
53
+ },
54
+ {
55
+ name: 'phone_todo_complete',
56
+ description: 'Mark a todo item as completed. The user will confirm on their phone.',
57
+ inputSchema: {
58
+ type: 'object',
59
+ properties: {
60
+ todoId: { type: 'string', description: 'The ID of the todo item to complete' },
61
+ },
62
+ required: ['todoId'],
63
+ },
64
+ },
65
+ {
66
+ name: 'phone_location_get',
67
+ description: 'Get the phone current GPS location. The user will be asked for permission.',
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: {},
71
+ },
72
+ },
73
+ {
74
+ name: 'phone_location_track',
75
+ description: 'Start tracking the phone GPS location in the background. Points are collected until phone_location_stop is called. Returns immediately.',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {},
79
+ },
80
+ },
81
+ {
82
+ name: 'phone_location_stop',
83
+ description: 'Stop GPS location tracking and return all collected coordinate points since tracking started.',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: {},
87
+ },
88
+ },
89
+ {
90
+ name: 'phone_notification_send',
91
+ description: 'Send a notification to the phone. The user will confirm on their phone.',
92
+ inputSchema: {
93
+ type: 'object',
94
+ properties: {
95
+ title: { type: 'string', description: 'Notification title' },
96
+ body: { type: 'string', description: 'Notification body text' },
97
+ at: { type: 'number', description: 'Scheduled time as Unix timestamp ms (optional, send immediately if omitted)' },
98
+ },
99
+ required: ['title', 'body'],
100
+ },
101
+ },
102
+ {
103
+ name: 'phone_clipboard_write',
104
+ description: 'Write text to the phone clipboard. The user will confirm on their phone.',
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: {
108
+ text: { type: 'string', description: 'Text to write to clipboard' },
109
+ },
110
+ required: ['text'],
111
+ },
112
+ },
113
+ {
114
+ name: 'phone_map_show',
115
+ description: 'Show a map with markers and/or polyline on the phone screen. The user will see the map and confirm when done.',
116
+ inputSchema: {
117
+ type: 'object',
118
+ properties: {
119
+ markers: {
120
+ type: 'array',
121
+ description: 'Marker points to display on the map',
122
+ items: {
123
+ type: 'object',
124
+ properties: {
125
+ latitude: { type: 'number', description: 'Marker latitude' },
126
+ longitude: { type: 'number', description: 'Marker longitude' },
127
+ title: { type: 'string', description: 'Marker label text' },
128
+ },
129
+ required: ['latitude', 'longitude'],
130
+ },
131
+ },
132
+ polyline: {
133
+ type: 'array',
134
+ description: 'Polylines to draw on the map',
135
+ items: {
136
+ type: 'object',
137
+ properties: {
138
+ points: {
139
+ type: 'array',
140
+ items: {
141
+ type: 'object',
142
+ properties: {
143
+ latitude: { type: 'number' },
144
+ longitude: { type: 'number' },
145
+ timestamp: { type: 'number', description: 'Unix timestamp ms (optional, for speed calc)' },
146
+ },
147
+ required: ['latitude', 'longitude'],
148
+ },
149
+ },
150
+ color: { type: 'string', description: 'Line color, e.g. #FF0000', default: '#1677FF' },
151
+ width: { type: 'number', description: 'Line width in pixels', default: 4 },
152
+ },
153
+ required: ['points'],
154
+ },
155
+ },
156
+ title: { type: 'string', description: 'Map page title', default: '地图' },
157
+ },
158
+ },
159
+ },
160
+ ]
161
+
162
+ // phone_calendar_query and phone_todo_list are also sent to the mini-program,
163
+ // but the mini-program handles them without showing a confirmation modal.
164
+
165
+ let pendingPhoneActions = new Map() // actionId -> { resolve, timer, rpcId }
166
+ let sendToRelayFn = null
167
+ let sseResponse = null // SSE response object for sending events to Claude Code
168
+ let messageEndpoint = ''
169
+
170
+ /**
171
+ * Start the MCP HTTP server.
172
+ * @param {function} relaySender - function(msg) that sends a message to the relay WebSocket
173
+ * @returns {Promise<number>} the port the server is listening on
174
+ */
175
+ function startMcpServer(relaySender) {
176
+ sendToRelayFn = relaySender
177
+
178
+ return new Promise((resolve, reject) => {
179
+ const server = http.createServer((req, res) => {
180
+ // CORS headers
181
+ res.setHeader('Access-Control-Allow-Origin', '*')
182
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
183
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
184
+ if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return }
185
+
186
+ if (req.url === '/sse' && req.method === 'GET') {
187
+ handleSSE(req, res)
188
+ } else if (req.url === '/message' && req.method === 'POST') {
189
+ handleMessage(req, res)
190
+ } else {
191
+ res.writeHead(404)
192
+ res.end('Not found')
193
+ }
194
+ })
195
+
196
+ // Try ports 3007-3016
197
+ let port = 3007
198
+ function tryListen() {
199
+ server.listen(port, '127.0.0.1', () => {
200
+ console.log(` [✓] MCP server listening on http://127.0.0.1:${port}`)
201
+ messageEndpoint = `http://127.0.0.1:${port}/message`
202
+ resolve(port)
203
+ })
204
+ server.on('error', (err) => {
205
+ if (err.code === 'EADDRINUSE' && port < 3017) {
206
+ port++
207
+ server.listen(port, '127.0.0.1')
208
+ } else {
209
+ reject(err)
210
+ }
211
+ })
212
+ }
213
+ tryListen()
214
+ })
215
+ }
216
+
217
+ function handleSSE(req, res) {
218
+ res.writeHead(200, {
219
+ 'Content-Type': 'text/event-stream',
220
+ 'Cache-Control': 'no-cache',
221
+ 'Connection': 'keep-alive',
222
+ })
223
+
224
+ // Send the endpoint URL as the first event
225
+ res.write(`event: endpoint\ndata: ${messageEndpoint}\n\n`)
226
+
227
+ sseResponse = res
228
+
229
+ // Heartbeat every 15s to keep SSE connection alive
230
+ const heartbeat = setInterval(() => {
231
+ if (res.writableEnded) {
232
+ clearInterval(heartbeat)
233
+ return
234
+ }
235
+ res.write(': heartbeat\n\n')
236
+ }, 15000)
237
+
238
+ req.on('close', () => {
239
+ clearInterval(heartbeat)
240
+ if (sseResponse === res) sseResponse = null
241
+ })
242
+ }
243
+
244
+ function handleMessage(req, res) {
245
+ let body = ''
246
+ req.on('data', (chunk) => { body += chunk })
247
+ req.on('end', () => {
248
+ res.writeHead(202)
249
+ res.end('Accepted')
250
+
251
+ let rpcMsg
252
+ try { rpcMsg = JSON.parse(body) } catch { return }
253
+
254
+ const { id, method, params } = rpcMsg
255
+
256
+ if (method === 'initialize') {
257
+ sendSSE({
258
+ jsonrpc: '2.0', id,
259
+ result: {
260
+ protocolVersion: '2024-11-05',
261
+ capabilities: { tools: {} },
262
+ serverInfo: { name: 'codepocket-phone', version: '1.0.0' },
263
+ },
264
+ })
265
+ } else if (method === 'tools/list') {
266
+ sendSSE({ jsonrpc: '2.0', id, result: { tools: TOOLS } })
267
+ } else if (method === 'tools/call') {
268
+ handleToolCall(id, params)
269
+ } else if (method === 'notifications/initialized') {
270
+ // Client notification, no response needed
271
+ }
272
+ })
273
+ }
274
+
275
+ async function handleToolCall(rpcId, params) {
276
+ const toolName = params?.name
277
+ const toolArgs = params?.arguments || {}
278
+
279
+ // All phone tools are sent to the mini-program
280
+ const actionId = 'act_' + Array.from(
281
+ typeof crypto !== 'undefined' && crypto.getRandomValues
282
+ ? crypto.getRandomValues(new Uint8Array(4))
283
+ : require('crypto').randomBytes(4)
284
+ ).map(b => b.toString(16).padStart(2, '0')).join('')
285
+
286
+ if (sendToRelayFn) {
287
+ sendToRelayFn({
288
+ type: 'phone-action',
289
+ actionId,
290
+ tool: toolName,
291
+ params: toolArgs,
292
+ timeout: 60000,
293
+ })
294
+ }
295
+
296
+ // Block and wait for phone-action-response (60s timeout)
297
+ const result = await new Promise((resolve) => {
298
+ const timer = setTimeout(() => {
299
+ pendingPhoneActions.delete(actionId)
300
+ resolve({ success: false, error: '操作超时,手机端未响应' })
301
+ }, 60000)
302
+
303
+ pendingPhoneActions.set(actionId, { resolve, rpcId, timer })
304
+ })
305
+
306
+ sendSSE({ jsonrpc: '2.0', id: rpcId, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } })
307
+ }
308
+
309
+ /**
310
+ * Called when a phone-action-response is received from the relay.
311
+ */
312
+ function handlePhoneActionResponse(msg) {
313
+ const pending = pendingPhoneActions.get(msg.actionId)
314
+ if (!pending) return
315
+
316
+ clearTimeout(pending.timer)
317
+ pendingPhoneActions.delete(msg.actionId)
318
+ pending.resolve({
319
+ accepted: msg.accepted,
320
+ result: msg.result,
321
+ error: msg.error,
322
+ })
323
+ }
324
+
325
+ function sendSSE(data) {
326
+ if (sseResponse && !sseResponse.writableEnded) {
327
+ sseResponse.write(`event: message\ndata: ${JSON.stringify(data)}\n\n`)
328
+ }
329
+ }
330
+
331
+ module.exports = { startMcpServer, handlePhoneActionResponse }
Binary file