mobile-debug-mcp 0.9.0 → 0.10.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 (48) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.cjs +18 -0
  3. package/.github/workflows/.gitkeep +0 -0
  4. package/.github/workflows/ci.yml +63 -0
  5. package/README.md +4 -17
  6. package/dist/android/interact.js +26 -4
  7. package/dist/android/observe.js +3 -3
  8. package/dist/android/utils.js +59 -104
  9. package/dist/ios/interact.js +3 -3
  10. package/dist/ios/observe.js +4 -4
  11. package/dist/ios/utils.js +8 -8
  12. package/dist/server.js +34 -42
  13. package/dist/tools/install.js +1 -1
  14. package/dist/tools/interact.js +89 -0
  15. package/dist/tools/logs.js +2 -2
  16. package/dist/tools/observe.js +126 -0
  17. package/dist/utils/index.js +1 -0
  18. package/dist/utils/java.js +76 -0
  19. package/docs/CHANGELOG.md +21 -6
  20. package/eslint.config.cjs +36 -0
  21. package/eslint.config.js +60 -0
  22. package/package.json +8 -2
  23. package/src/android/interact.ts +24 -5
  24. package/src/android/observe.ts +3 -3
  25. package/src/android/utils.ts +65 -93
  26. package/src/ios/interact.ts +3 -4
  27. package/src/ios/observe.ts +4 -4
  28. package/src/ios/utils.ts +8 -8
  29. package/src/server.ts +37 -58
  30. package/src/tools/interact.ts +84 -0
  31. package/src/tools/observe.ts +132 -0
  32. package/src/utils/index.ts +1 -0
  33. package/src/utils/java.ts +69 -0
  34. package/test/integration/install.integration.ts +3 -3
  35. package/test/integration/run-install-android.ts +1 -1
  36. package/test/integration/run-install-ios.ts +1 -1
  37. package/test/integration/smoke-test.ts +1 -1
  38. package/test/integration/test-dist.ts +1 -1
  39. package/test/integration/test-ui-tree.ts +1 -1
  40. package/test/integration/wait_for_element_real.ts +1 -1
  41. package/test/unit/detect-java.test.ts +22 -0
  42. package/test/unit/install.test.ts +0 -6
  43. package/src/tools/app.ts +0 -46
  44. package/src/tools/devices.ts +0 -6
  45. package/src/tools/install.ts +0 -43
  46. package/src/tools/logs.ts +0 -62
  47. package/src/tools/screenshot.ts +0 -18
  48. package/src/tools/ui.ts +0 -62
package/src/server.ts CHANGED
@@ -8,40 +8,19 @@ import {
8
8
 
9
9
  import {
10
10
  StartAppResponse,
11
- DeviceInfo,
12
11
  TerminateAppResponse,
13
12
  RestartAppResponse,
14
13
  ResetAppDataResponse,
15
- GetUITreeResponse,
16
- GetCurrentScreenResponse,
17
- WaitForElementResponse,
18
- TapResponse,
19
- SwipeResponse,
20
- TypeTextResponse,
21
- PressBackResponse,
22
14
  InstallAppResponse
23
15
  } from "./types.js"
24
16
 
25
- import { AndroidObserve } from "./android/observe.js"
26
- import { AndroidInteract } from "./android/interact.js"
27
- import { iOSObserve } from "./ios/observe.js"
28
- import { iOSInteract } from "./ios/interact.js"
29
- import { resolveTargetDevice, listDevices } from "./resolve-device.js"
30
- import { startAndroidLogStream, readLogStreamLines, stopAndroidLogStream } from "./android/utils.js"
31
- import { startIOSLogStream, readIOSLogStreamLines, stopIOSLogStream } from "./ios/utils.js"
32
- import { promises as fs } from 'fs'
33
- import path from 'path'
34
- import { installAppHandler } from './tools/install.js'
35
- import { startAppHandler, terminateAppHandler, restartAppHandler, resetAppDataHandler } from './tools/app.js'
36
- import { getLogsHandler, startLogStreamHandler, readLogStreamHandler, stopLogStreamHandler } from './tools/logs.js'
37
- import { listDevicesHandler } from './tools/devices.js'
38
- import { captureScreenshotHandler } from './tools/screenshot.js'
39
- import { getUITreeHandler, getCurrentScreenHandler, waitForElementHandler, tapHandler, swipeHandler, typeTextHandler, pressBackHandler } from './tools/ui.js'
40
-
41
- const androidObserve = new AndroidObserve()
42
- const androidInteract = new AndroidInteract()
43
- const iosObserve = new iOSObserve()
44
- const iosInteract = new iOSInteract()
17
+ import { ToolsInteract } from './tools/interact.js'
18
+ import { ToolsObserve } from './tools/observe.js'
19
+ import { AndroidInteract } from './android/interact.js'
20
+ import { iOSInteract } from './ios/interact.js'
21
+ import { AndroidObserve } from './android/observe.js'
22
+ import { iOSObserve } from './ios/observe.js'
23
+
45
24
 
46
25
  const server = new Server(
47
26
  {
@@ -418,44 +397,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
418
397
  try {
419
398
  if (name === "start_app") {
420
399
  const { platform, appId, deviceId } = args as any
421
- const result = await startAppHandler({ platform, appId, deviceId })
400
+ const res = await (platform === 'android' ? new AndroidInteract().startApp(appId, deviceId) : new iOSInteract().startApp(appId, deviceId))
422
401
  const response: StartAppResponse = {
423
- device: result.device,
424
- appStarted: result.appStarted,
425
- launchTimeMs: result.launchTimeMs
402
+ device: res.device,
403
+ appStarted: res.appStarted,
404
+ launchTimeMs: res.launchTimeMs
426
405
  }
427
406
  return wrapResponse(response)
428
407
  }
429
408
 
430
409
  if (name === "terminate_app") {
431
410
  const { platform, appId, deviceId } = args as any
432
- const result = await terminateAppHandler({ platform, appId, deviceId })
433
- const response: TerminateAppResponse = { device: result.device, appTerminated: result.appTerminated }
411
+ const res = await (platform === 'android' ? new AndroidInteract().terminateApp(appId, deviceId) : new iOSInteract().terminateApp(appId, deviceId))
412
+ const response: TerminateAppResponse = { device: res.device, appTerminated: res.appTerminated }
434
413
  return wrapResponse(response)
435
414
  }
436
415
 
437
416
  if (name === "restart_app") {
438
417
  const { platform, appId, deviceId } = args as any
439
- const result = await restartAppHandler({ platform, appId, deviceId })
440
- const response: RestartAppResponse = { device: result.device, appRestarted: result.appRestarted, launchTimeMs: result.launchTimeMs }
418
+ const res = await (platform === 'android' ? new AndroidInteract().restartApp(appId, deviceId) : new iOSInteract().restartApp(appId, deviceId))
419
+ const response: RestartAppResponse = { device: res.device, appRestarted: res.appRestarted, launchTimeMs: res.launchTimeMs }
441
420
  return wrapResponse(response)
442
421
  }
443
422
 
444
423
  if (name === "reset_app_data") {
445
424
  const { platform, appId, deviceId } = args as any
446
- const result = await resetAppDataHandler({ platform, appId, deviceId })
447
- const response: ResetAppDataResponse = { device: result.device, dataCleared: result.dataCleared }
425
+ const res = await (platform === 'android' ? new AndroidInteract().resetAppData(appId, deviceId) : new iOSInteract().resetAppData(appId, deviceId))
426
+ const response: ResetAppDataResponse = { device: res.device, dataCleared: res.dataCleared }
448
427
  return wrapResponse(response)
449
428
  }
450
429
 
451
430
  if (name === "install_app") {
452
431
  const { platform, appPath, deviceId } = args as any
453
- const result = await installAppHandler({ platform, appPath, deviceId })
432
+ const res = await ToolsInteract.installAppHandler({ platform, appPath, deviceId })
454
433
  const response: InstallAppResponse = {
455
- device: result.device,
456
- installed: result.installed,
457
- output: (result as any).output,
458
- error: (result as any).error
434
+ device: res.device,
435
+ installed: res.installed,
436
+ output: (res as any).output,
437
+ error: (res as any).error
459
438
  }
460
439
  return wrapResponse(response)
461
440
  }
@@ -463,7 +442,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
463
442
 
464
443
  if (name === "get_logs") {
465
444
  const { platform, appId, deviceId, lines } = args as any
466
- const res = await getLogsHandler({ platform, appId, deviceId, lines })
445
+ const res = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, lines })
467
446
  return {
468
447
  content: [
469
448
  { type: 'text', text: JSON.stringify({ device: res.device, result: { lines: res.logs.length, crashLines: res.crashLines.length > 0 ? res.crashLines : undefined } }, null, 2) },
@@ -474,79 +453,79 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
474
453
 
475
454
  if (name === "list_devices") {
476
455
  const { platform, appId } = (args || {}) as any
477
- const res = await listDevicesHandler({ platform, appId })
456
+ const res = await ToolsObserve.listDevicesHandler({ platform, appId })
478
457
  return wrapResponse(res)
479
458
  }
480
459
 
481
460
 
482
461
  if (name === "capture_screenshot") {
483
462
  const { platform, deviceId } = args as any
484
- const res = await captureScreenshotHandler({ platform, deviceId })
463
+ const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId })
485
464
  return {
486
465
  content: [
487
- { type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: res.resolution } }, null, 2) },
488
- { type: 'image', data: res.screenshot, mimeType: 'image/png' }
466
+ { type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: (res as any).resolution } }, null, 2) },
467
+ { type: 'image', data: (res as any).screenshot, mimeType: 'image/png' }
489
468
  ]
490
469
  }
491
470
  }
492
471
 
493
472
  if (name === "get_ui_tree") {
494
473
  const { platform, deviceId } = args as any
495
- const res = await getUITreeHandler({ platform, deviceId })
474
+ const res = await (platform === 'android' ? new AndroidObserve().getUITree(deviceId) : new iOSObserve().getUITree(deviceId))
496
475
  return wrapResponse(res)
497
476
  }
498
477
 
499
478
  if (name === "get_current_screen") {
500
479
  const { deviceId } = (args || {}) as any
501
- const res = await getCurrentScreenHandler({ deviceId })
480
+ const res = await new AndroidObserve().getCurrentScreen(deviceId)
502
481
  return wrapResponse(res)
503
482
  }
504
483
 
505
484
  if (name === "wait_for_element") {
506
485
  const { platform, text, timeout, deviceId } = (args || {}) as any
507
- const res = await waitForElementHandler({ platform, text, timeout, deviceId })
486
+ const res = await (platform === 'android' ? new AndroidInteract().waitForElement(text, timeout, deviceId) : new iOSInteract().waitForElement(text, timeout, deviceId))
508
487
  return wrapResponse(res)
509
488
  }
510
489
 
511
490
  if (name === "tap") {
512
491
  const { platform, x, y, deviceId } = (args || {}) as any
513
- const res = await tapHandler({ platform, x, y, deviceId })
492
+ const res = await (platform === 'android' ? new AndroidInteract().tap(x, y, deviceId) : new iOSInteract().tap(x, y, deviceId))
514
493
  return wrapResponse(res)
515
494
  }
516
495
 
517
496
  if (name === "swipe") {
518
497
  const { x1, y1, x2, y2, duration, deviceId } = (args || {}) as any
519
- const res = await swipeHandler({ x1, y1, x2, y2, duration, deviceId })
498
+ const res = await new AndroidInteract().swipe(x1, y1, x2, y2, duration, deviceId)
520
499
  return wrapResponse(res)
521
500
  }
522
501
 
523
502
  if (name === "type_text") {
524
503
  const { text, deviceId } = (args || {}) as any
525
- const res = await typeTextHandler({ text, deviceId })
504
+ const res = await new AndroidInteract().typeText(text, deviceId)
526
505
  return wrapResponse(res)
527
506
  }
528
507
 
529
508
  if (name === "press_back") {
530
509
  const { deviceId } = (args || {}) as any
531
- const res = await pressBackHandler({ deviceId })
510
+ const res = await new AndroidInteract().pressBack(deviceId)
532
511
  return wrapResponse(res)
533
512
  }
534
513
 
535
514
  if (name === 'start_log_stream') {
536
515
  const { platform, packageName, level, sessionId, deviceId } = args as any
537
- const res = await startLogStreamHandler({ platform, packageName, level, sessionId, deviceId })
516
+ const res = await ToolsObserve.startLogStreamHandler({ platform, packageName, level, sessionId, deviceId })
538
517
  return wrapResponse(res)
539
518
  }
540
519
 
541
520
  if (name === 'read_log_stream') {
542
521
  const { platform, sessionId, limit, since } = args as any
543
- const res = await readLogStreamHandler({ platform, sessionId, limit, since })
522
+ const res = await ToolsObserve.readLogStreamHandler({ platform, sessionId, limit, since })
544
523
  return wrapResponse(res)
545
524
  }
546
525
 
547
526
  if (name === 'stop_log_stream') {
548
527
  const { platform, sessionId } = (args || {}) as any
549
- const res = await stopLogStreamHandler({ platform, sessionId })
528
+ const res = await ToolsObserve.stopLogStreamHandler({ platform, sessionId })
550
529
  return wrapResponse(res)
551
530
  }
552
531
  } catch (error) {
@@ -0,0 +1,84 @@
1
+ import { promises as fs } from 'fs'
2
+ import path from 'path'
3
+ import { resolveTargetDevice } from '../resolve-device.js'
4
+ import { AndroidInteract } from '../android/interact.js'
5
+ import { iOSInteract } from '../ios/interact.js'
6
+
7
+ export class ToolsInteract {
8
+ static async installAppHandler({ platform, appPath, deviceId }: { platform?: 'android' | 'ios', appPath: string, deviceId?: string }) {
9
+ let chosenPlatform: 'android' | 'ios' | undefined = platform
10
+
11
+ try {
12
+ const stat = await fs.stat(appPath).catch(() => null)
13
+ if (stat && stat.isDirectory()) {
14
+ const files = (await fs.readdir(appPath).catch(() => [])) as string[]
15
+ if (files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) {
16
+ chosenPlatform = 'ios'
17
+ } else if (files.includes('gradlew') || files.includes('build.gradle') || files.includes('settings.gradle') || (files.includes('app') && (await fs.stat(path.join(appPath, 'app')).catch(() => null)))) {
18
+ chosenPlatform = 'android'
19
+ } else {
20
+ chosenPlatform = 'android'
21
+ }
22
+ } else if (typeof appPath === 'string') {
23
+ const ext = path.extname(appPath).toLowerCase()
24
+ if (ext === '.apk') chosenPlatform = 'android'
25
+ else if (ext === '.ipa' || ext === '.app') chosenPlatform = 'ios'
26
+ else chosenPlatform = 'android'
27
+ }
28
+ } catch {
29
+ chosenPlatform = 'android'
30
+ }
31
+
32
+ if (chosenPlatform === 'android') {
33
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
34
+ const androidInteract = new AndroidInteract()
35
+ const result = await androidInteract.installApp(appPath, resolved.id)
36
+ return result
37
+ } else {
38
+ const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
39
+ const iosInteract = new iOSInteract()
40
+ const result = await iosInteract.installApp(appPath, resolved.id)
41
+ return result
42
+ }
43
+ }
44
+
45
+ static async startAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
46
+ if (platform === 'android') {
47
+ const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
48
+ return await new AndroidInteract().startApp(appId, resolved.id)
49
+ } else {
50
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
51
+ return await new iOSInteract().startApp(appId, resolved.id)
52
+ }
53
+ }
54
+
55
+ static async terminateAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
56
+ if (platform === 'android') {
57
+ const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
58
+ return await new AndroidInteract().terminateApp(appId, resolved.id)
59
+ } else {
60
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
61
+ return await new iOSInteract().terminateApp(appId, resolved.id)
62
+ }
63
+ }
64
+
65
+ static async restartAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
66
+ if (platform === 'android') {
67
+ const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
68
+ return await new AndroidInteract().restartApp(appId, resolved.id)
69
+ } else {
70
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
71
+ return await new iOSInteract().restartApp(appId, resolved.id)
72
+ }
73
+ }
74
+
75
+ static async resetAppDataHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
76
+ if (platform === 'android') {
77
+ const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
78
+ return await new AndroidInteract().resetAppData(appId, resolved.id)
79
+ } else {
80
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
81
+ return await new iOSInteract().resetAppData(appId, resolved.id)
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,132 @@
1
+ import { resolveTargetDevice, listDevices } from '../resolve-device.js'
2
+ import { AndroidObserve } from '../android/observe.js'
3
+ import { iOSObserve } from '../ios/observe.js'
4
+ import { AndroidInteract } from '../android/interact.js'
5
+ import { iOSInteract } from '../ios/interact.js'
6
+
7
+ export class ToolsObserve {
8
+ static async getLogsHandler({ platform, appId, deviceId, lines }: { platform: 'android' | 'ios', appId?: string, deviceId?: string, lines?: number }) {
9
+ if (platform === 'android') {
10
+ const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
11
+ const response = await new AndroidObserve().getLogs(appId, lines ?? 200, resolved.id)
12
+ const logs = Array.isArray(response.logs) ? response.logs : []
13
+ const crashLines = logs.filter(line => line.includes('FATAL EXCEPTION'))
14
+ return { device: response.device, logs, crashLines }
15
+ } else {
16
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
17
+ const resp = await new iOSObserve().getLogs(appId, resolved.id)
18
+ const logs = Array.isArray(resp.logs) ? resp.logs : []
19
+ const crashLines = logs.filter(l => l.includes('FATAL EXCEPTION'))
20
+ return { device: resp.device, logs, crashLines }
21
+ }
22
+ }
23
+
24
+ static async startLogStreamHandler({ platform, packageName, level, sessionId, deviceId }: { platform?: 'android' | 'ios', packageName: string, level?: 'error' | 'warn' | 'info' | 'debug', sessionId?: string, deviceId?: string }) {
25
+ const effectivePlatform = platform || 'android'
26
+ const sid = sessionId || 'default'
27
+ if (effectivePlatform === 'android') {
28
+ const resolved = await resolveTargetDevice({ platform: 'android', appId: packageName, deviceId })
29
+ // AndroidObserve uses utils for log stream control; delegate to android/utils functions where appropriate
30
+ const { startAndroidLogStream } = await import('../android/utils.js')
31
+ return await startAndroidLogStream(packageName, level || 'error', resolved.id, sid)
32
+ } else {
33
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId: packageName, deviceId })
34
+ // iOSObserve implements startIOSLogStream via ios/utils; use its helper
35
+ const { startIOSLogStream } = await import('../ios/utils.js')
36
+ return await startIOSLogStream(packageName, resolved.id, sid)
37
+ }
38
+ }
39
+
40
+ static async readLogStreamHandler({ platform, sessionId, limit, since }: { platform?: 'android' | 'ios', sessionId?: string, limit?: number, since?: string }) {
41
+ const effectivePlatform = platform || 'android'
42
+ const sid = sessionId || 'default'
43
+ if (effectivePlatform === 'android') {
44
+ const { readLogStreamLines } = await import('../android/utils.js')
45
+ return await readLogStreamLines(sid, limit ?? 100, since)
46
+ } else {
47
+ const { readIOSLogStreamLines } = await import('../ios/utils.js')
48
+ return await readIOSLogStreamLines(sid, limit ?? 100, since)
49
+ }
50
+ }
51
+
52
+ static async stopLogStreamHandler({ platform, sessionId }: { platform?: 'android' | 'ios', sessionId?: string }) {
53
+ const effectivePlatform = platform || 'android'
54
+ const sid = sessionId || 'default'
55
+ if (effectivePlatform === 'android') {
56
+ const { stopAndroidLogStream } = await import('../android/utils.js')
57
+ return await stopAndroidLogStream(sid)
58
+ } else {
59
+ const { stopIOSLogStream } = await import('../ios/utils.js')
60
+ return await stopIOSLogStream(sid)
61
+ }
62
+ }
63
+
64
+ static async getUITreeHandler({ platform, deviceId }: { platform: 'android' | 'ios', deviceId?: string }) {
65
+ if (platform === 'android') {
66
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
67
+ return await new AndroidObserve().getUITree(resolved.id)
68
+ } else {
69
+ const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
70
+ return await new iOSObserve().getUITree(resolved.id)
71
+ }
72
+ }
73
+
74
+ static async getCurrentScreenHandler({ deviceId }: { deviceId?: string }) {
75
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
76
+ return await new AndroidObserve().getCurrentScreen(resolved.id)
77
+ }
78
+
79
+ static async waitForElementHandler({ platform, text, timeout, deviceId }: { platform: 'android' | 'ios', text: string, timeout?: number, deviceId?: string }) {
80
+ const effectiveTimeout = timeout ?? 10000
81
+ if (platform === 'android') {
82
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
83
+ return await new AndroidInteract().waitForElement(text, effectiveTimeout, resolved.id)
84
+ } else {
85
+ const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
86
+ return await new iOSInteract().waitForElement(text, effectiveTimeout, resolved.id)
87
+ }
88
+ }
89
+
90
+ static async tapHandler({ platform, x, y, deviceId }: { platform?: 'android' | 'ios', x: number, y: number, deviceId?: string }) {
91
+ const effectivePlatform = platform || 'android'
92
+ if (effectivePlatform === 'android') {
93
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
94
+ return await new AndroidInteract().tap(x, y, resolved.id)
95
+ } else {
96
+ const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
97
+ return await new iOSInteract().tap(x, y, resolved.id)
98
+ }
99
+ }
100
+
101
+ static async swipeHandler({ x1, y1, x2, y2, duration, deviceId }: { x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string }) {
102
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
103
+ return await new AndroidInteract().swipe(x1, y1, x2, y2, duration, resolved.id)
104
+ }
105
+
106
+ static async typeTextHandler({ text, deviceId }: { text: string, deviceId?: string }) {
107
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
108
+ return await new AndroidInteract().typeText(text, resolved.id)
109
+ }
110
+
111
+ static async pressBackHandler({ deviceId }: { deviceId?: string }) {
112
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
113
+ return await new AndroidInteract().pressBack(resolved.id)
114
+ }
115
+
116
+ static async listDevicesHandler({ platform, appId }: { platform?: 'android' | 'ios', appId?: string }) {
117
+ const devices = await listDevices(platform as any, appId)
118
+ return { devices }
119
+ }
120
+
121
+ static async captureScreenshotHandler({ platform, deviceId }: { platform?: 'android' | 'ios', deviceId?: string }) {
122
+ const effectivePlatform = platform || 'android'
123
+ if (effectivePlatform === 'android') {
124
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
125
+ return await new AndroidObserve().captureScreen(resolved.id)
126
+ } else {
127
+ const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
128
+ return await new iOSObserve().captureScreenshot(resolved.id)
129
+ }
130
+ }
131
+ }
132
+
@@ -0,0 +1 @@
1
+ export { detectJavaHome } from './java.js'
@@ -0,0 +1,69 @@
1
+ import { execSync } from 'child_process'
2
+ import { existsSync } from 'fs'
3
+ import path from 'path'
4
+
5
+ export async function detectJavaHome(): Promise<string | undefined> {
6
+ try {
7
+ // If JAVA_HOME is set, validate it's Java 17
8
+ if (process.env.JAVA_HOME) {
9
+ try {
10
+ const javaBin = path.join(process.env.JAVA_HOME, 'bin', 'java')
11
+ const v = execSync(`"${javaBin}" -version`, { stdio: ['ignore', 'pipe', 'pipe'] }).toString()
12
+ if (/\b17\b/.test(v) || /17\./.test(v)) return process.env.JAVA_HOME
13
+ console.debug('[java.detect] Existing JAVA_HOME does not appear to be Java 17, will search for JDK17')
14
+ } catch {
15
+ console.debug('[java.detect] Failed to validate existing JAVA_HOME, searching for JDK17')
16
+ }
17
+ }
18
+
19
+ // macOS explicit path
20
+ const explicit = '/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home'
21
+ if (existsSync(explicit)) return explicit
22
+
23
+ // Android Studio JBR candidates
24
+ const jbrCandidates = [
25
+ '/Applications/Android Studio.app/Contents/jbr',
26
+ '/Applications/Android Studio Preview.app/Contents/jbr',
27
+ '/Applications/Android Studio Preview 2022.3.app/Contents/jbr',
28
+ '/Applications/Android Studio Preview 2023.1.app/Contents/jbr'
29
+ ]
30
+ for (const p of jbrCandidates) {
31
+ const javaBin = path.join(p, 'bin', 'java')
32
+ if (existsSync(javaBin)) {
33
+ try {
34
+ const v = execSync(`"${javaBin}" -version`, { stdio: ['ignore', 'pipe', 'pipe'] }).toString()
35
+ if (/\b17\b/.test(v) || /17\./.test(v)) return p
36
+ } catch {}
37
+ }
38
+ }
39
+
40
+ // macOS /usr/libexec/java_home
41
+ try {
42
+ const out = execSync('/usr/libexec/java_home -v 17', { stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim()
43
+ if (out) return out
44
+ } catch {}
45
+
46
+ // macOS common JDK locations
47
+ try {
48
+ const homes = execSync('ls -1 /Library/Java/JavaVirtualMachines || true', { stdio: ['ignore', 'pipe', 'inherit'] }).toString().split(/\r?\n/).filter(Boolean)
49
+ for (const h of homes) {
50
+ if (h.toLowerCase().includes('17') || h.toLowerCase().includes('jdk-17')) {
51
+ const candidate = `/Library/Java/JavaVirtualMachines/${h}/Contents/Home`
52
+ return candidate
53
+ }
54
+ }
55
+ } catch {}
56
+
57
+ // Linux locations
58
+ const linuxCandidates = [
59
+ '/usr/lib/jvm/java-17-openjdk-amd64',
60
+ '/usr/lib/jvm/java-17-openjdk',
61
+ '/usr/lib/jvm/zulu17',
62
+ '/usr/lib/jvm/temurin-17-jdk'
63
+ ]
64
+ for (const p of linuxCandidates) {
65
+ try { if (existsSync(p)) return p } catch {}
66
+ }
67
+ } catch {}
68
+ return undefined
69
+ }
@@ -17,14 +17,14 @@ function isAndroidDir(p: string) {
17
17
  try {
18
18
  const listing = fs.readdirSync(p)
19
19
  return listing.includes('gradlew') || listing.some((f: string) => f.endsWith('.gradle') || f === 'app' || f === 'settings.gradle')
20
- } catch (e) { return false }
20
+ } catch { return false }
21
21
  }
22
22
 
23
23
  function isIosDir(p: string) {
24
24
  try {
25
25
  const listing = fs.readdirSync(p)
26
26
  return listing.some((f: string) => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))
27
- } catch (e) { return false }
27
+ } catch { return false }
28
28
  }
29
29
 
30
30
  let runner: string | undefined
@@ -56,7 +56,7 @@ proc.on('close', (code) => {
56
56
  console.error('Integration reported not installed:', obj)
57
57
  process.exit(4)
58
58
  }
59
- } catch (e) {
59
+ } catch {
60
60
  console.error('Failed to parse runner output')
61
61
  console.error(stdout)
62
62
  process.exit(5)
@@ -12,7 +12,7 @@ async function main() {
12
12
  try {
13
13
  const res = await inter.installApp(appPath, deviceId)
14
14
  console.log(JSON.stringify(res, null, 2))
15
- } catch (err) {
15
+ } catch {
16
16
  console.error('Install failed:', err instanceof Error ? err.message : String(err))
17
17
  process.exit(2)
18
18
  }
@@ -12,7 +12,7 @@ async function main() {
12
12
  try {
13
13
  const res = await inter.installApp(appPath, deviceId || 'booted')
14
14
  console.log(JSON.stringify(res, null, 2))
15
- } catch (err) {
15
+ } catch {
16
16
  console.error('Install failed:', err instanceof Error ? err.message : String(err))
17
17
  process.exit(2)
18
18
  }
@@ -105,7 +105,7 @@ async function main() {
105
105
 
106
106
  console.log(`\n✨ Smoke test COMPLETED SUCCESSFULLY! ✨\n`);
107
107
 
108
- } catch (error) {
108
+ } catch {
109
109
  console.error(`\n❌ Smoke test FAILED:`, error);
110
110
  process.exit(1);
111
111
  }
@@ -32,7 +32,7 @@ async function main() {
32
32
  } else {
33
33
  console.log('No screenshot returned')
34
34
  }
35
- } catch (err) {
35
+ } catch {
36
36
  console.error('Smoke test script failed:', err)
37
37
  process.exit(1)
38
38
  }
@@ -67,7 +67,7 @@ async function main() {
67
67
  console.log(`- Elements with text: ${withText}`);
68
68
  }
69
69
 
70
- } catch (error) {
70
+ } catch {
71
71
  console.error("\n❌ Test Failed:", error);
72
72
  process.exit(1);
73
73
  }
@@ -72,7 +72,7 @@ async function runRealTest() {
72
72
  console.log(`Calls: ${calls} ${calls === 3 ? "PASS" : "FAIL"}`);
73
73
  console.log(`Elapsed time (should be >= 1000ms): ${elapsed3} ${elapsed3 >= 1000 ? "PASS" : "FAIL"}`);
74
74
 
75
- } catch (error) {
75
+ } catch {
76
76
  console.error("Test failed with error:", error);
77
77
  }
78
78
  }
@@ -0,0 +1,22 @@
1
+ import assert from 'assert'
2
+ import { detectJavaHome } from '../../src/utils/java.js'
3
+
4
+ // These tests are lightweight smoke tests; they don't rely on actual JDK17 installs,
5
+ // but exercise the failure modes and ensure the function returns undefined or a string.
6
+
7
+ export async function run() {
8
+ const res = await detectJavaHome()
9
+ // It's acceptable for local dev env to not have JDK17; just ensure call returns (string|undefined)
10
+ assert.ok(typeof res === 'string' || typeof res === 'undefined')
11
+
12
+ // Basic mocking: if JAVA_HOME points to a fake path, detection should return undefined
13
+ const orig = process.env.JAVA_HOME
14
+ process.env.JAVA_HOME = '/non/existent/java/home'
15
+ const res2 = await detectJavaHome()
16
+ assert.ok(typeof res2 === 'undefined')
17
+ process.env.JAVA_HOME = orig
18
+
19
+ console.log('detectJavaHome tests passed')
20
+ }
21
+
22
+ run().catch((e) => { console.error(e); process.exit(1) })
@@ -2,7 +2,6 @@ import assert from 'assert'
2
2
  import fs from 'fs/promises'
3
3
  import os from 'os'
4
4
  import path from 'path'
5
- import { createRequire } from 'module'
6
5
 
7
6
  // This test mocks child_process.spawn and simulates a Gradle build producing an APK
8
7
  // and an adb install. It does not patch AndroidInteract.installApp itself so the
@@ -15,11 +14,6 @@ async function makeTempFile(ext: string) {
15
14
  return { dir, file }
16
15
  }
17
16
 
18
- async function makeTempDirWith(name: string) {
19
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-test-'))
20
- await fs.writeFile(path.join(dir, name), '')
21
- return dir
22
- }
23
17
 
24
18
  export async function run() {
25
19
  // Create a fake adb executable in a temporary bin dir and prepend to PATH so