mobile-debug-mcp 0.21.4 → 0.22.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 (84) hide show
  1. package/AGENTS.md +74 -0
  2. package/README.md +24 -5
  3. package/dist/interact/index.js +263 -41
  4. package/dist/observe/ios.js +10 -3
  5. package/dist/server-core.js +707 -0
  6. package/dist/server.js +6 -693
  7. package/dist/utils/resolve-device.js +15 -3
  8. package/docs/CHANGELOG.md +9 -1
  9. package/docs/tools/interact.md +69 -30
  10. package/package.json +3 -3
  11. package/skills/README.md +35 -0
  12. package/skills/test-authoring/SKILL.md +57 -0
  13. package/skills/test-authoring/references/repo-test-layout.md +47 -0
  14. package/skills/test-authoring/references/test-authoring-workflow.md +73 -0
  15. package/skills/test-authoring/references/test-quality-checklist.md +39 -0
  16. package/src/interact/index.ts +286 -38
  17. package/src/observe/ios.ts +12 -3
  18. package/src/server-core.ts +762 -0
  19. package/src/server.ts +8 -754
  20. package/src/types.ts +10 -1
  21. package/src/utils/resolve-device.ts +19 -3
  22. package/test/device/automated/observe/capture_screenshot.android.smoke.ts +30 -0
  23. package/test/device/automated/observe/capture_screenshot.ios.smoke.ts +30 -0
  24. package/test/{observe/device → device/automated/observe}/get_logs.android.smoke.ts +1 -1
  25. package/test/{observe/device → device/automated/observe}/get_logs.ios.smoke.ts +1 -1
  26. package/test/device/automated/observe/get_ui_tree.android.smoke.ts +31 -0
  27. package/test/device/automated/observe/get_ui_tree.ios.smoke.ts +31 -0
  28. package/test/device/index.ts +52 -0
  29. package/test/{interact/device/smoke-test.ts → device/manual/interact/app_lifecycle.manual.ts} +5 -5
  30. package/test/{manage/device/run-build-install-ios.ts → device/manual/manage/build_install_ios.manual.ts} +1 -1
  31. package/test/{manage/device → device/manual/manage}/install.integration.ts +6 -6
  32. package/test/{manage/device/run-install-android.ts → device/manual/manage/install_android.manual.ts} +1 -1
  33. package/test/{manage/device/run-install-ios.ts → device/manual/manage/install_ios.manual.ts} +1 -1
  34. package/test/device/manual/observe/capture_screenshot.manual.ts +29 -0
  35. package/test/{helpers/run-get-logs.ts → device/manual/observe/get_logs.manual.ts} +1 -1
  36. package/test/device/manual/observe/get_ui_tree.manual.ts +29 -0
  37. package/test/{observe/device/logstream-real.ts → device/manual/observe/logstream.manual.ts} +1 -1
  38. package/test/{observe/device/run-screen-fingerprint.ts → device/manual/observe/screen_fingerprint.manual.ts} +1 -1
  39. package/test/{observe/device/run-scroll-test-android.ts → device/manual/observe/scroll_to_element_android.manual.ts} +1 -1
  40. package/test/{observe/device/test-ui-tree.ts → device/manual/observe/ui_tree.manual.ts} +6 -6
  41. package/test/unit/index.ts +47 -27
  42. package/test/unit/interact/handler_shapes.test.ts +55 -0
  43. package/test/unit/interact/tap_element.test.ts +170 -0
  44. package/test/unit/interact/wait_for_screen_change.test.ts +34 -0
  45. package/test/{interact/unit → unit/interact}/wait_for_ui_contract.test.ts +11 -10
  46. package/test/unit/interact/wait_for_ui_selector_matching.test.ts +76 -0
  47. package/test/unit/manage/handler_shapes.test.ts +43 -0
  48. package/test/{observe/unit → unit/observe}/capture_debug_snapshot.test.ts +5 -1
  49. package/test/{observe/unit → unit/observe}/find_element.test.ts +12 -6
  50. package/test/unit/observe/get_screen_fingerprint.test.ts +71 -0
  51. package/test/unit/observe/ios-getlogs.test.ts +53 -0
  52. package/test/unit/observe/scroll_to_element.test.ts +127 -0
  53. package/test/unit/server/contract.test.ts +45 -0
  54. package/test/unit/server/response_shapes.test.ts +93 -0
  55. package/test/unit/system/adb_version.test.ts +35 -0
  56. package/test/unit/system/get_system_status.test.ts +20 -0
  57. package/test/unit/system/system_status.test.ts +141 -0
  58. package/test/{utils → unit/utils}/detect_java.test.ts +1 -1
  59. package/test/unit/utils/exec.test.ts +51 -0
  60. package/test/unit/utils/resolve_device.test.ts +63 -0
  61. package/tsconfig.json +2 -2
  62. package/test/interact/device/run-real-test.ts +0 -3
  63. package/test/interact/unit/wait_for_screen_change.test.ts +0 -32
  64. package/test/interact/unit/wait_for_ui.test.ts +0 -76
  65. package/test/interact/unit/wait_for_ui_new.test.ts +0 -57
  66. package/test/observe/device/wait_for_element_real.ts +0 -3
  67. package/test/observe/unit/get_screen_fingerprint.test.ts +0 -69
  68. package/test/observe/unit/ios-getlogs.test.ts +0 -67
  69. package/test/observe/unit/scroll_to_element.test.ts +0 -129
  70. package/test/observe/unit/wait_for_element_mock.ts +0 -2
  71. package/test/observe/unit/wait_for_ui_edge_cases.test.ts +0 -41
  72. package/test/observe/unit/wait_for_ui_stability.test.ts +0 -30
  73. package/test/system/adb_version.test.ts +0 -25
  74. package/test/system/get_system_status.test.ts +0 -52
  75. package/test/system/system_status.test.ts +0 -109
  76. /package/test/{manage/unit → unit/manage}/build.test.ts +0 -0
  77. /package/test/{manage/unit → unit/manage}/build_and_install.test.ts +0 -0
  78. /package/test/{manage/unit → unit/manage}/detection.test.ts +0 -0
  79. /package/test/{manage/unit → unit/manage}/diagnostics.test.ts +0 -0
  80. /package/test/{manage/unit → unit/manage}/install.test.ts +0 -0
  81. /package/test/{manage/unit → unit/manage}/mcp_disable_autodetect.test.ts +0 -0
  82. /package/test/{observe/unit → unit/observe}/get_logs.test.ts +0 -0
  83. /package/test/{observe/unit → unit/observe}/logparse.test.ts +0 -0
  84. /package/test/{observe/unit → unit/observe}/logstream.test.ts +0 -0
package/src/server.ts CHANGED
@@ -1,759 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js"
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
- import type { SchemaOutput } from "@modelcontextprotocol/sdk/server/zod-compat.js"
5
- import {
6
- ListToolsRequestSchema,
7
- CallToolRequestSchema
8
- } from "@modelcontextprotocol/sdk/types.js"
9
-
10
- import {
11
- StartAppResponse,
12
- TerminateAppResponse,
13
- RestartAppResponse,
14
- ResetAppDataResponse,
15
- InstallAppResponse
16
- } from "./types.js"
17
-
18
- import { ToolsManage } from './manage/index.js'
19
- import { ToolsInteract } from './interact/index.js'
20
- import { ToolsObserve } from './observe/index.js'
21
- import { AndroidManage } from './manage/index.js'
22
- import { iOSManage } from './manage/index.js'
23
-
24
-
25
- const server = new Server(
26
- {
27
- name: "mobile-debug-mcp",
28
- version: "0.7.0"
29
- },
30
- {
31
- capabilities: {
32
- tools: {}
33
- }
34
- }
35
- );
36
-
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
+ import { createServer } from './server-core.js'
37
4
  import { getSystemStatus } from './system/index.js'
38
5
 
39
- // Run a quick startup healthcheck (non-fatal) by calling getSystemStatus directly and log a short summary
40
- getSystemStatus().then(res => {
41
- console.debug('[startup] system status summary:', { adb: res.adbAvailable, ios: res.iosAvailable, devices: res.devices, iosDevices: res.iosDevices })
42
- }).catch(e => console.warn('[startup] healthcheck failed:', e instanceof Error ? e.message : String(e)))
43
-
44
- function wrapResponse<T>(data: T) {
45
- return {
46
- content: [{
47
- type: "text" as const,
48
- text: JSON.stringify(data, null, 2)
49
- }]
50
- }
51
- }
52
-
53
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
54
- tools: [
55
- {
56
- name: "start_app",
57
- description: "Launch a mobile app on Android or iOS simulator",
58
- inputSchema: {
59
- type: "object",
60
- properties: {
61
- platform: {
62
- type: "string",
63
- enum: ["android", "ios"]
64
- },
65
- appId: {
66
- type: "string",
67
- description: "Android package name or iOS bundle id"
68
- },
69
- deviceId: {
70
- type: "string",
71
- description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected."
72
- }
73
- },
74
- required: ["platform", "appId"]
75
- }
76
- },
77
- {
78
- name: "terminate_app",
79
- description: "Terminate a mobile app on Android or iOS simulator",
80
- inputSchema: {
81
- type: "object",
82
- properties: {
83
- platform: {
84
- type: "string",
85
- enum: ["android", "ios"]
86
- },
87
- appId: {
88
- type: "string",
89
- description: "Android package name or iOS bundle id"
90
- },
91
- deviceId: {
92
- type: "string",
93
- description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected."
94
- }
95
- },
96
- required: ["platform", "appId"]
97
- }
98
- },
99
- {
100
- name: "restart_app",
101
- description: "Restart a mobile app on Android or iOS simulator",
102
- inputSchema: {
103
- type: "object",
104
- properties: {
105
- platform: {
106
- type: "string",
107
- enum: ["android", "ios"]
108
- },
109
- appId: {
110
- type: "string",
111
- description: "Android package name or iOS bundle id"
112
- },
113
- deviceId: {
114
- type: "string",
115
- description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected."
116
- }
117
- },
118
- required: ["platform", "appId"]
119
- }
120
- },
121
- {
122
- name: "reset_app_data",
123
- description: "Reset app data (clear storage) for a mobile app on Android or iOS simulator",
124
- inputSchema: {
125
- type: "object",
126
- properties: {
127
- platform: {
128
- type: "string",
129
- enum: ["android", "ios"]
130
- },
131
- appId: {
132
- type: "string",
133
- description: "Android package name or iOS bundle id"
134
- },
135
- deviceId: {
136
- type: "string",
137
- description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected."
138
- }
139
- },
140
- required: ["platform", "appId"]
141
- }
142
- },
143
- {
144
- name: "install_app",
145
- description: "Install an app on Android or iOS. Accepts a built binary (apk/.ipa/.app) or a project directory to build then install. platform and projectType are required.",
146
- inputSchema: {
147
- type: "object",
148
- properties: {
149
- platform: { type: "string", enum: ["android", "ios"], description: "Platform to install to (required)." },
150
- projectType: { type: "string", enum: ["native","kmp","react-native","flutter"], description: "Project type to guide build/install tool selection (required)." },
151
- appPath: { type: "string", description: "Path to APK, .app, .ipa, or project directory" },
152
- deviceId: { type: "string", description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected." }
153
- },
154
- required: ["platform", "projectType", "appPath"]
155
- }
156
- },
157
- {
158
- name: "build_app",
159
- description: "Build a project for Android or iOS and return the built artifact path. Does not install. platform and projectType are required.",
160
- inputSchema: {
161
- type: "object",
162
- properties: {
163
- platform: { type: "string", enum: ["android", "ios"], description: "Platform to build for (required)." },
164
- projectType: { type: "string", enum: ["native","kmp","react-native","flutter"], description: "Project type to guide build tool selection (required)." },
165
- projectPath: { type: "string", description: "Path to project directory (contains gradlew or xcodeproj/xcworkspace)" },
166
- variant: { type: "string", description: "Optional build variant (e.g., Debug/Release)" }
167
- },
168
- required: ["platform", "projectType", "projectPath"]
169
- }
170
- },
171
-
172
- {
173
- name: "get_logs",
174
- description: "Get recent logs from Android or iOS simulator. Returns device metadata and structured logs suitable for AI consumption.",
175
- inputSchema: {
176
- type: "object",
177
- properties: {
178
- platform: {
179
- type: "string",
180
- enum: ["android", "ios"]
181
- },
182
- appId: {
183
- type: "string",
184
- description: "Filter by Android package name or iOS bundle id"
185
- },
186
- deviceId: {
187
- type: "string",
188
- description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected."
189
- },
190
- pid: { type: "number", description: "Filter by process id" },
191
- tag: { type: "string", description: "Filter by tag (Android) or subsystem/category (iOS)" },
192
- level: { type: "string", description: "Log level filter (VERBOSE, DEBUG, INFO, WARN, ERROR)" },
193
- contains: { type: "string", description: "Substring to match in log message" },
194
- since_seconds: { type: "number", description: "Only return logs from the last N seconds" },
195
- limit: { type: "number", description: "Override default number of returned lines" },
196
- lines: {
197
- type: "number",
198
- description: "Legacy - number of log lines (android only)"
199
- }
200
- },
201
- required: ["platform"]
202
- }
203
- },
204
- {
205
- name: "list_devices",
206
- description: "List connected devices and their metadata (android + ios).",
207
- inputSchema: {
208
- type: "object",
209
- properties: {
210
- platform: { type: "string", enum: ["android", "ios"] }
211
- }
212
- }
213
- },
214
- {
215
- name: "get_system_status",
216
- description: "Quick healthcheck of local mobile debugging environment (adb, devices, logs, env, iOS).",
217
- inputSchema: { type: "object", properties: {} }
218
- },
219
- {
220
- name: "capture_screenshot",
221
- description: "Capture a screenshot from an Android device or iOS simulator. Returns device metadata and the screenshot image.",
222
- inputSchema: {
223
- type: "object",
224
- properties: {
225
- platform: {
226
- type: "string",
227
- enum: ["android", "ios"]
228
- },
229
- deviceId: {
230
- type: "string",
231
- description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected."
232
- }
233
- },
234
- required: ["platform"]
235
- }
236
- },
237
- {
238
- name: "capture_debug_snapshot",
239
- description: "Capture a complete debug snapshot (screenshot, ui tree, activity, fingerprint, logs). Returns structured JSON."
240
- ,
241
- inputSchema: {
242
- type: "object",
243
- properties: {
244
- reason: { type: "string", description: "Optional reason for snapshot" },
245
- includeLogs: { type: "boolean", description: "Whether to include logs", default: true },
246
- logLines: { type: "number", description: "Maximum number of log lines to include", default: 200 },
247
- platform: { type: "string", enum: ["android","ios"], description: "Optional platform override" },
248
- appId: { type: "string", description: "Optional appId to scope logs (package/bundle id)" },
249
- deviceId: { type: "string", description: "Optional device serial/udid" },
250
- sessionId: { type: "string", description: "Optional log stream session id to prefer" }
251
- }
252
- }
253
- },
254
- {
255
- name: "start_log_stream",
256
- description: "Start streaming logs for a target application on Android or iOS. For Android this uses adb logcat --pid=<pid>; for iOS it streams `xcrun simctl spawn <device> log stream` with a predicate.",
257
- inputSchema: {
258
- type: "object",
259
- properties: {
260
- platform: { type: "string", enum: ["android", "ios"], default: "android" },
261
- packageName: { type: "string", description: "Android package name or iOS bundle id" },
262
- level: { type: "string", enum: ["error", "warn", "info", "debug"], default: "error" },
263
- deviceId: { type: "string", description: "Device Serial (Android) or UDID (iOS). Defaults to connected/booted device." },
264
- sessionId: { type: "string", description: "Session identifier for the log stream" }
265
- },
266
- required: ["packageName"]
267
- }
268
- },
269
- {
270
- name: "read_log_stream",
271
- description: "Read accumulated log stream entries for the active session.",
272
- inputSchema: {
273
- type: "object",
274
- properties: {
275
- sessionId: { type: "string" }
276
- }
277
- }
278
- },
279
- {
280
- name: "stop_log_stream",
281
- description: "Stop an active log stream for the session.",
282
- inputSchema: {
283
- type: "object",
284
- properties: {
285
- sessionId: { type: "string" }
286
- }
287
- }
288
- },
289
-
290
- {
291
- name: "get_ui_tree",
292
- description: "Get the current UI hierarchy from an Android device or iOS simulator. Returns a structured JSON representation of the screen content.",
293
- inputSchema: {
294
- type: "object",
295
- properties: {
296
- platform: {
297
- type: "string",
298
- enum: ["android", "ios"],
299
- description: "Platform to get UI tree for"
300
- },
301
- deviceId: {
302
- type: "string",
303
- description: "Device Serial (Android) or UDID (iOS). Defaults to connected/booted device."
304
- }
305
- },
306
- required: ["platform"]
307
- }
308
- },
309
- {
310
- name: "get_current_screen",
311
- description: "Get the currently visible activity on an Android device. Returns package and activity name.",
312
- inputSchema: {
313
- type: "object",
314
- properties: {
315
- deviceId: {
316
- type: "string",
317
- description: "Device Serial (Android). Defaults to connected/booted device."
318
- }
319
- }
320
- }
321
- },
322
- {
323
- name: "get_screen_fingerprint",
324
- description: "Generate a stable fingerprint representing the current visible screen (activity + visible UI elements).",
325
- inputSchema: {
326
- type: "object",
327
- properties: {
328
- platform: { type: "string", enum: ["android", "ios"], description: "Optional platform override (android|ios)" },
329
- deviceId: { type: "string", description: "Optional device id/udid to target" }
330
- }
331
- }
332
- },
333
- {
334
- name: "wait_for_screen_change",
335
- description: "Wait until the current screen fingerprint differs from a provided previousFingerprint. Useful to wait for navigation/animation completion.",
336
- inputSchema: {
337
- type: "object",
338
- properties: {
339
- platform: { type: "string", enum: ["android", "ios"], description: "Optional platform override (android|ios)" },
340
- previousFingerprint: { type: "string", description: "The fingerprint to compare against (required)" },
341
- timeoutMs: { type: "number", description: "Timeout in ms to wait for change (default 5000)", default: 5000 },
342
- pollIntervalMs: { type: "number", description: "Polling interval in ms (default 300)", default: 300 },
343
- deviceId: { type: "string", description: "Optional device id/udid to target" }
344
- },
345
- required: ["previousFingerprint"]
346
- }
347
- },
348
- {
349
- name: "wait_for_ui",
350
- description: "Deterministic UI wait primitive. Waits for selector condition with retries and backoff.",
351
- inputSchema: {
352
- type: "object",
353
- properties: {
354
- selector: {
355
- type: "object",
356
- properties: {
357
- text: { type: "string" },
358
- resource_id: { type: "string" },
359
- accessibility_id: { type: "string" },
360
- contains: { type: "boolean", description: "When true, perform substring matching", default: false }
361
- }
362
- },
363
- condition: { type: "string", enum: ["exists","not_exists","visible","clickable"], default: "exists" },
364
- timeout_ms: { type: "number", default: 60000 },
365
- poll_interval_ms: { type: "number", default: 300 },
366
- match: { type: "object", properties: { index: { type: "number" } } },
367
- retry: { type: "object", properties: { max_attempts: { type: "number", default: 1 }, backoff_ms: { type: "number", default: 0 } } },
368
- platform: { type: "string", enum: ["android","ios"], description: "Optional platform override" },
369
- deviceId: { type: "string", description: "Optional device serial/udid" }
370
- }
371
- }
372
- },
373
-
374
-
375
-
376
- {
377
- name: "find_element",
378
- description: "Find a UI element by semantic query (text, content-desc, resource-id, class). Returns best match.",
379
- inputSchema: {
380
- type: "object",
381
- properties: {
382
- query: { type: "string", description: "Search query (text or label)" },
383
- exact: { type: "boolean", description: "Require exact match (true/false)", default: false },
384
- timeoutMs: { type: "number", description: "Timeout in ms to keep searching", default: 3000 },
385
- platform: { type: "string", enum: ["android","ios"], description: "Optional platform override" },
386
- deviceId: { type: "string", description: "Optional device serial/udid" }
387
- },
388
- required: ["query"]
389
- }
390
- },
391
-
392
- {
393
- name: "tap",
394
- description: "Simulate a finger tap on the device screen at specific coordinates.",
395
- inputSchema: {
396
- type: "object",
397
- properties: {
398
- platform: {
399
- type: "string",
400
- enum: ["android", "ios"],
401
- description: "Platform to tap on"
402
- },
403
- x: {
404
- type: "number",
405
- description: "X coordinate"
406
- },
407
- y: {
408
- type: "number",
409
- description: "Y coordinate"
410
- },
411
- deviceId: {
412
- type: "string",
413
- description: "Device Serial/UDID. Defaults to connected/booted device."
414
- }
415
- },
416
- required: ["x", "y"]
417
- }
418
- },
419
- {
420
- name: "swipe",
421
- description: "Simulate a swipe gesture on an Android device.",
422
- inputSchema: {
423
- type: "object",
424
- properties: {
425
- platform: {
426
- type: "string",
427
- enum: ["android","ios"],
428
- description: "Platform to swipe on (android or ios)"
429
- },
430
- x1: { type: "number", description: "Start X coordinate" },
431
- y1: { type: "number", description: "Start Y coordinate" },
432
- x2: { type: "number", description: "End X coordinate" },
433
- y2: { type: "number", description: "End Y coordinate" },
434
- duration: { type: "number", description: "Duration in ms" },
435
- deviceId: {
436
- type: "string",
437
- description: "Device Serial/UDID. Defaults to connected/booted device."
438
- }
439
- },
440
- required: ["x1", "y1", "x2", "y2", "duration"]
441
- }
442
- },
443
- {
444
- name: "scroll_to_element",
445
- description: "Scroll the current screen until a target UI element becomes visible, then return its details.",
446
- inputSchema: {
447
- type: "object",
448
- properties: {
449
- platform: { type: "string", enum: ["android", "ios"], description: "Platform to operate on (required)" },
450
- selector: {
451
- type: "object",
452
- properties: {
453
- text: { type: "string" },
454
- resourceId: { type: "string" },
455
- contentDesc: { type: "string" },
456
- className: { type: "string" }
457
- }
458
- },
459
- direction: { type: "string", enum: ["down", "up"], default: "down" },
460
- maxScrolls: { type: "number", default: 10 },
461
- scrollAmount: { type: "number", default: 0.7 },
462
- deviceId: { type: "string", description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected." }
463
- },
464
- required: ["platform", "selector"]
465
- }
466
- },
467
- {
468
- name: "type_text",
469
- description: "Type text into the currently focused input field on an Android device.",
470
- inputSchema: {
471
- type: "object",
472
- properties: {
473
- platform: {
474
- type: "string",
475
- enum: ["android"],
476
- description: "Platform to type on (currently only android supported)"
477
- },
478
- text: {
479
- type: "string",
480
- description: "The text to type"
481
- },
482
- deviceId: {
483
- type: "string",
484
- description: "Device Serial/UDID. Defaults to connected/booted device."
485
- }
486
- },
487
- required: ["text"]
488
- }
489
- },
490
- {
491
- name: "press_back",
492
- description: "Simulate pressing the Android Back button.",
493
- inputSchema: {
494
- type: "object",
495
- properties: {
496
- platform: {
497
- type: "string",
498
- enum: ["android"],
499
- description: "Platform (currently only android supported)"
500
- },
501
- deviceId: {
502
- type: "string",
503
- description: "Device Serial/UDID. Defaults to connected/booted device."
504
- }
505
- }
506
- }
507
- }
508
- ]
509
- }));
510
-
511
- server.setRequestHandler(CallToolRequestSchema, async (request: SchemaOutput<typeof CallToolRequestSchema>) => {
512
- const { name, arguments: args } = request.params
513
-
514
- try {
515
- if (name === "start_app") {
516
- const { platform, appId, deviceId } = args as any
517
- // Defensive validation: ensure caller provided platform and appId.
518
- if (!platform || !appId) {
519
- const msg = 'Both platform and appId parameters are required (platform: ios|android, appId: bundle id or package name).'
520
- const payload = { ts: new Date().toISOString(), tool: 'start_app', args }
521
- let logged = false
522
-
523
- // Prefer the diagnostics module when available
524
- try {
525
- const diag = require('./utils/diagnostics.js')
526
- if (diag && diag.appendDiagnosticFile) {
527
- diag.appendDiagnosticFile('bad_requests.log', payload)
528
- logged = true
529
- }
530
- } catch (err) {
531
- console.error('Diagnostics append failed:', String(err))
532
- }
533
-
534
- // Fallback to /tmp file (synchronous) and report failures rather than swallowing
535
- if (!logged) {
536
- try {
537
- const fs = require('fs')
538
- fs.appendFileSync('/tmp/mcp_bad_requests.log', JSON.stringify(payload) + '\n')
539
- logged = true
540
- } catch (err) {
541
- console.error('Failed to write bad request to /tmp/mcp_bad_requests.log:', String(err))
542
- }
543
- }
544
-
545
- // Final fallback: emit payload to stderr so it's visible in server logs
546
- if (!logged) {
547
- try {
548
- console.error('Bad request (start_app) payload:', JSON.stringify(payload))
549
- } catch (err) {
550
- // Last resort: still log the failure
551
- console.error('Failed to emit bad request payload to stderr:', String(err))
552
- }
553
- }
554
-
555
- return wrapResponse({ error: msg })
556
- }
557
-
558
- const res = await (platform === 'android' ? new AndroidManage().startApp(appId, deviceId) : new iOSManage().startApp(appId, deviceId))
559
- const response: StartAppResponse = {
560
- device: res.device,
561
- appStarted: res.appStarted,
562
- launchTimeMs: res.launchTimeMs
563
- }
564
- return wrapResponse(response)
565
- }
566
-
567
- if (name === "terminate_app") {
568
- const { platform, appId, deviceId } = args as any
569
- const res = await (platform === 'android' ? new AndroidManage().terminateApp(appId, deviceId) : new iOSManage().terminateApp(appId, deviceId))
570
- const response: TerminateAppResponse = { device: res.device, appTerminated: res.appTerminated }
571
- return wrapResponse(response)
572
- }
573
-
574
- if (name === "restart_app") {
575
- const { platform, appId, deviceId } = args as any
576
- const res = await (platform === 'android' ? new AndroidManage().restartApp(appId, deviceId) : new iOSManage().restartApp(appId, deviceId))
577
- const response: RestartAppResponse = { device: res.device, appRestarted: res.appRestarted, launchTimeMs: res.launchTimeMs }
578
- return wrapResponse(response)
579
- }
580
-
581
- if (name === "reset_app_data") {
582
- const { platform, appId, deviceId } = args as any
583
- const res = await (platform === 'android' ? new AndroidManage().resetAppData(appId, deviceId) : new iOSManage().resetAppData(appId, deviceId))
584
- const response: ResetAppDataResponse = { device: res.device, dataCleared: res.dataCleared }
585
- return wrapResponse(response)
586
- }
587
-
588
- if (name === "install_app") {
589
- const { platform, projectType, appPath, deviceId } = args as any
590
- const res = await ToolsManage.installAppHandler({ platform, appPath, deviceId, projectType })
591
- const response: InstallAppResponse = {
592
- device: res.device,
593
- installed: res.installed,
594
- output: (res as any).output,
595
- error: (res as any).error
596
- }
597
- return wrapResponse(response)
598
- }
599
-
600
- if (name === "build_app") {
601
- const { platform, projectType, projectPath, variant } = args as any
602
- const res = await ToolsManage.buildAppHandler({ platform, projectPath, variant, projectType })
603
- return wrapResponse(res)
604
- }
6
+ const server = createServer()
605
7
 
606
- if (name === 'build_and_install') {
607
- const { platform, projectType, projectPath, deviceId, timeout } = args as any
608
- const res = await ToolsManage.buildAndInstallHandler({ platform, projectPath, deviceId, timeout, projectType })
609
- // res: { ndjson, result }
610
- return {
611
- content: [
612
- { type: 'text', text: res.ndjson },
613
- { type: 'text', text: JSON.stringify(res.result, null, 2) }
614
- ]
615
- }
616
- }
617
-
618
-
619
- if (name === "get_logs") {
620
- const { platform, appId, deviceId, pid, tag, level, contains, since_seconds, limit, lines } = args as any
621
- const res = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, pid, tag, level, contains, since_seconds, limit, lines })
622
- const filtered = !!(pid || tag || level || contains || since_seconds || appId)
623
- return {
624
- content: [
625
- { type: 'text', text: JSON.stringify({ device: res.device, result: { count: res.logCount, filtered, crashLines: (res.crashLines || []), source: res.source, meta: res.meta || {} } }, null, 2) },
626
- { type: 'text', text: JSON.stringify({ logs: res.logs }, null, 2) }
627
- ]
628
- }
629
- }
630
-
631
- if (name === "list_devices") {
632
- const { platform, appId } = (args || {}) as any
633
- const res = await ToolsManage.listDevicesHandler({ platform, appId })
634
- return wrapResponse(res)
635
- }
636
-
637
- if (name === "get_system_status") {
638
- const result = await getSystemStatus()
639
- return wrapResponse(result)
640
- }
641
-
642
-
643
- if (name === "capture_screenshot") {
644
- const { platform, deviceId } = args as any
645
- const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId })
646
- const mime = (res as any).screenshot_mime || 'image/png'
647
- const content: any[] = [
648
- { type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: (res as any).resolution, mimeType: mime } }, null, 2) },
649
- { type: 'image', data: (res as any).screenshot, mimeType: mime }
650
- ]
651
- // If a jpeg fallback is available, include a small note and the fallback as an additional image block for compatibility
652
- if ((res as any).screenshot_fallback) {
653
- content.push({ type: 'text', text: JSON.stringify({ note: 'JPEG fallback included for compatibility', mimeType: (res as any).screenshot_fallback_mime || 'image/jpeg' }) })
654
- content.push({ type: 'image', data: (res as any).screenshot_fallback, mimeType: (res as any).screenshot_fallback_mime || 'image/jpeg' })
655
- }
656
- return { content }
657
- }
658
-
659
- if (name === "capture_debug_snapshot") {
660
- const { reason, includeLogs, logLines, platform, appId, deviceId, sessionId } = args as any
661
- const res = await ToolsObserve.captureDebugSnapshotHandler({ reason, includeLogs, logLines, platform, appId, deviceId, sessionId })
662
- return wrapResponse(res)
663
- }
664
-
665
- if (name === "get_ui_tree") {
666
- const { platform, deviceId } = args as any
667
- const res = await ToolsObserve.getUITreeHandler({ platform, deviceId })
668
- return wrapResponse(res)
669
- }
670
-
671
- if (name === "get_current_screen") {
672
- const { deviceId } = (args || {}) as any
673
- const res = await ToolsObserve.getCurrentScreenHandler({ deviceId })
674
- return wrapResponse(res)
675
- }
676
-
677
- if (name === "get_screen_fingerprint") {
678
- const { platform, deviceId } = (args || {}) as any
679
- const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId })
680
- return wrapResponse(res)
681
- }
682
-
683
- if (name === "wait_for_screen_change") {
684
- const { platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId } = (args || {}) as any
685
- const res = await ToolsInteract.waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId })
686
- return wrapResponse(res)
687
- }
688
-
689
-
690
- if (name === "wait_for_ui") {
691
- const { selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry, platform, deviceId } = (args || {}) as any
692
- const res = await ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform, deviceId })
693
- return wrapResponse(res)
694
- }
695
-
696
- if (name === "find_element") {
697
- const { query, exact = false, timeoutMs = 3000, platform, deviceId } = (args || {}) as any
698
- const res = await ToolsInteract.findElementHandler({ query, exact, timeoutMs, platform, deviceId })
699
- return wrapResponse(res)
700
- }
701
-
702
- if (name === "tap") {
703
- const { platform, x, y, deviceId } = (args || {}) as any
704
- const res = await ToolsInteract.tapHandler({ platform, x, y, deviceId })
705
- return wrapResponse(res)
706
- }
707
-
708
- if (name === "swipe") {
709
- const { platform = 'android', x1, y1, x2, y2, duration, deviceId } = (args || {}) as any
710
- const res = await ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId })
711
- return wrapResponse(res)
712
- }
713
-
714
- if (name === "scroll_to_element") {
715
- const { platform, selector, direction, maxScrolls, scrollAmount, deviceId } = (args || {}) as any
716
- const res = await ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId })
717
- return wrapResponse(res)
718
- }
719
-
720
- if (name === "type_text") {
721
- const { text, deviceId } = (args || {}) as any
722
- const res = await ToolsInteract.typeTextHandler({ text, deviceId })
723
- return wrapResponse(res)
724
- }
725
-
726
- if (name === "press_back") {
727
- const { deviceId } = (args || {}) as any
728
- const res = await ToolsInteract.pressBackHandler({ deviceId })
729
- return wrapResponse(res)
730
- }
731
-
732
- if (name === 'start_log_stream') {
733
- const { platform, packageName, level, sessionId, deviceId } = args as any
734
- const res = await ToolsObserve.startLogStreamHandler({ platform, packageName, level, sessionId, deviceId })
735
- return wrapResponse(res)
736
- }
737
-
738
- if (name === 'read_log_stream') {
739
- const { platform, sessionId, limit, since } = args as any
740
- const res = await ToolsObserve.readLogStreamHandler({ platform, sessionId, limit, since })
741
- return wrapResponse(res)
742
- }
743
-
744
- if (name === 'stop_log_stream') {
745
- const { platform, sessionId } = (args || {}) as any
746
- const res = await ToolsObserve.stopLogStreamHandler({ platform, sessionId })
747
- return wrapResponse(res)
748
- }
749
- } catch (error) {
750
- return {
751
- content: [{ type: "text", text: `Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}` }]
752
- }
753
- }
754
-
755
- throw new Error(`Unknown tool: ${name}`)
756
- })
8
+ getSystemStatus().then((res) => {
9
+ console.debug('[startup] system status summary:', { adb: res.adbAvailable, ios: res.iosAvailable, devices: res.devices, iosDevices: res.iosDevices })
10
+ }).catch((e) => console.warn('[startup] healthcheck failed:', e instanceof Error ? e.message : String(e)))
757
11
 
758
12
  const transport = new StdioServerTransport()
759
13
 
@@ -762,5 +16,5 @@ async function main() {
762
16
  }
763
17
 
764
18
  main().catch((error) => {
765
- console.error("Server failed to start:", error)
766
- })
19
+ console.error('Server failed to start:', error)
20
+ })