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.
- package/.eslintignore +5 -0
- package/.eslintrc.cjs +18 -0
- package/.github/workflows/.gitkeep +0 -0
- package/.github/workflows/ci.yml +63 -0
- package/README.md +4 -17
- package/dist/android/interact.js +26 -4
- package/dist/android/observe.js +3 -3
- package/dist/android/utils.js +59 -104
- package/dist/ios/interact.js +3 -3
- package/dist/ios/observe.js +4 -4
- package/dist/ios/utils.js +8 -8
- package/dist/server.js +34 -42
- package/dist/tools/install.js +1 -1
- package/dist/tools/interact.js +89 -0
- package/dist/tools/logs.js +2 -2
- package/dist/tools/observe.js +126 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/java.js +76 -0
- package/docs/CHANGELOG.md +21 -6
- package/eslint.config.cjs +36 -0
- package/eslint.config.js +60 -0
- package/package.json +8 -2
- package/src/android/interact.ts +24 -5
- package/src/android/observe.ts +3 -3
- package/src/android/utils.ts +65 -93
- package/src/ios/interact.ts +3 -4
- package/src/ios/observe.ts +4 -4
- package/src/ios/utils.ts +8 -8
- package/src/server.ts +37 -58
- package/src/tools/interact.ts +84 -0
- package/src/tools/observe.ts +132 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/java.ts +69 -0
- package/test/integration/install.integration.ts +3 -3
- package/test/integration/run-install-android.ts +1 -1
- package/test/integration/run-install-ios.ts +1 -1
- package/test/integration/smoke-test.ts +1 -1
- package/test/integration/test-dist.ts +1 -1
- package/test/integration/test-ui-tree.ts +1 -1
- package/test/integration/wait_for_element_real.ts +1 -1
- package/test/unit/detect-java.test.ts +22 -0
- package/test/unit/install.test.ts +0 -6
- package/src/tools/app.ts +0 -46
- package/src/tools/devices.ts +0 -6
- package/src/tools/install.ts +0 -43
- package/src/tools/logs.ts +0 -62
- package/src/tools/screenshot.ts +0 -18
- 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 {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import { iOSInteract } from
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
|
|
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
|
|
400
|
+
const res = await (platform === 'android' ? new AndroidInteract().startApp(appId, deviceId) : new iOSInteract().startApp(appId, deviceId))
|
|
422
401
|
const response: StartAppResponse = {
|
|
423
|
-
device:
|
|
424
|
-
appStarted:
|
|
425
|
-
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
|
|
433
|
-
const response: TerminateAppResponse = { device:
|
|
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
|
|
440
|
-
const response: RestartAppResponse = { device:
|
|
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
|
|
447
|
-
const response: ResetAppDataResponse = { device:
|
|
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
|
|
432
|
+
const res = await ToolsInteract.installAppHandler({ platform, appPath, deviceId })
|
|
454
433
|
const response: InstallAppResponse = {
|
|
455
|
-
device:
|
|
456
|
-
installed:
|
|
457
|
-
output: (
|
|
458
|
-
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
15
|
+
} catch {
|
|
16
16
|
console.error('Install failed:', err instanceof Error ? err.message : String(err))
|
|
17
17
|
process.exit(2)
|
|
18
18
|
}
|
|
@@ -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
|
|
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
|