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/dist/server.js
CHANGED
|
@@ -2,20 +2,12 @@
|
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { iOSInteract } from
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { getLogsHandler, startLogStreamHandler, readLogStreamHandler, stopLogStreamHandler } from './tools/logs.js';
|
|
12
|
-
import { listDevicesHandler } from './tools/devices.js';
|
|
13
|
-
import { captureScreenshotHandler } from './tools/screenshot.js';
|
|
14
|
-
import { getUITreeHandler, getCurrentScreenHandler, waitForElementHandler, tapHandler, swipeHandler, typeTextHandler, pressBackHandler } from './tools/ui.js';
|
|
15
|
-
const androidObserve = new AndroidObserve();
|
|
16
|
-
const androidInteract = new AndroidInteract();
|
|
17
|
-
const iosObserve = new iOSObserve();
|
|
18
|
-
const iosInteract = new iOSInteract();
|
|
5
|
+
import { ToolsInteract } from './tools/interact.js';
|
|
6
|
+
import { ToolsObserve } from './tools/observe.js';
|
|
7
|
+
import { AndroidInteract } from './android/interact.js';
|
|
8
|
+
import { iOSInteract } from './ios/interact.js';
|
|
9
|
+
import { AndroidObserve } from './android/observe.js';
|
|
10
|
+
import { iOSObserve } from './ios/observe.js';
|
|
19
11
|
const server = new Server({
|
|
20
12
|
name: "mobile-debug-mcp",
|
|
21
13
|
version: "0.7.0"
|
|
@@ -383,46 +375,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
383
375
|
try {
|
|
384
376
|
if (name === "start_app") {
|
|
385
377
|
const { platform, appId, deviceId } = args;
|
|
386
|
-
const
|
|
378
|
+
const res = await (platform === 'android' ? new AndroidInteract().startApp(appId, deviceId) : new iOSInteract().startApp(appId, deviceId));
|
|
387
379
|
const response = {
|
|
388
|
-
device:
|
|
389
|
-
appStarted:
|
|
390
|
-
launchTimeMs:
|
|
380
|
+
device: res.device,
|
|
381
|
+
appStarted: res.appStarted,
|
|
382
|
+
launchTimeMs: res.launchTimeMs
|
|
391
383
|
};
|
|
392
384
|
return wrapResponse(response);
|
|
393
385
|
}
|
|
394
386
|
if (name === "terminate_app") {
|
|
395
387
|
const { platform, appId, deviceId } = args;
|
|
396
|
-
const
|
|
397
|
-
const response = { device:
|
|
388
|
+
const res = await (platform === 'android' ? new AndroidInteract().terminateApp(appId, deviceId) : new iOSInteract().terminateApp(appId, deviceId));
|
|
389
|
+
const response = { device: res.device, appTerminated: res.appTerminated };
|
|
398
390
|
return wrapResponse(response);
|
|
399
391
|
}
|
|
400
392
|
if (name === "restart_app") {
|
|
401
393
|
const { platform, appId, deviceId } = args;
|
|
402
|
-
const
|
|
403
|
-
const response = { device:
|
|
394
|
+
const res = await (platform === 'android' ? new AndroidInteract().restartApp(appId, deviceId) : new iOSInteract().restartApp(appId, deviceId));
|
|
395
|
+
const response = { device: res.device, appRestarted: res.appRestarted, launchTimeMs: res.launchTimeMs };
|
|
404
396
|
return wrapResponse(response);
|
|
405
397
|
}
|
|
406
398
|
if (name === "reset_app_data") {
|
|
407
399
|
const { platform, appId, deviceId } = args;
|
|
408
|
-
const
|
|
409
|
-
const response = { device:
|
|
400
|
+
const res = await (platform === 'android' ? new AndroidInteract().resetAppData(appId, deviceId) : new iOSInteract().resetAppData(appId, deviceId));
|
|
401
|
+
const response = { device: res.device, dataCleared: res.dataCleared };
|
|
410
402
|
return wrapResponse(response);
|
|
411
403
|
}
|
|
412
404
|
if (name === "install_app") {
|
|
413
405
|
const { platform, appPath, deviceId } = args;
|
|
414
|
-
const
|
|
406
|
+
const res = await ToolsInteract.installAppHandler({ platform, appPath, deviceId });
|
|
415
407
|
const response = {
|
|
416
|
-
device:
|
|
417
|
-
installed:
|
|
418
|
-
output:
|
|
419
|
-
error:
|
|
408
|
+
device: res.device,
|
|
409
|
+
installed: res.installed,
|
|
410
|
+
output: res.output,
|
|
411
|
+
error: res.error
|
|
420
412
|
};
|
|
421
413
|
return wrapResponse(response);
|
|
422
414
|
}
|
|
423
415
|
if (name === "get_logs") {
|
|
424
416
|
const { platform, appId, deviceId, lines } = args;
|
|
425
|
-
const res = await getLogsHandler({ platform, appId, deviceId, lines });
|
|
417
|
+
const res = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, lines });
|
|
426
418
|
return {
|
|
427
419
|
content: [
|
|
428
420
|
{ type: 'text', text: JSON.stringify({ device: res.device, result: { lines: res.logs.length, crashLines: res.crashLines.length > 0 ? res.crashLines : undefined } }, null, 2) },
|
|
@@ -432,12 +424,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
432
424
|
}
|
|
433
425
|
if (name === "list_devices") {
|
|
434
426
|
const { platform, appId } = (args || {});
|
|
435
|
-
const res = await listDevicesHandler({ platform, appId });
|
|
427
|
+
const res = await ToolsObserve.listDevicesHandler({ platform, appId });
|
|
436
428
|
return wrapResponse(res);
|
|
437
429
|
}
|
|
438
430
|
if (name === "capture_screenshot") {
|
|
439
431
|
const { platform, deviceId } = args;
|
|
440
|
-
const res = await captureScreenshotHandler({ platform, deviceId });
|
|
432
|
+
const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId });
|
|
441
433
|
return {
|
|
442
434
|
content: [
|
|
443
435
|
{ type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: res.resolution } }, null, 2) },
|
|
@@ -447,52 +439,52 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
447
439
|
}
|
|
448
440
|
if (name === "get_ui_tree") {
|
|
449
441
|
const { platform, deviceId } = args;
|
|
450
|
-
const res = await
|
|
442
|
+
const res = await (platform === 'android' ? new AndroidObserve().getUITree(deviceId) : new iOSObserve().getUITree(deviceId));
|
|
451
443
|
return wrapResponse(res);
|
|
452
444
|
}
|
|
453
445
|
if (name === "get_current_screen") {
|
|
454
446
|
const { deviceId } = (args || {});
|
|
455
|
-
const res = await
|
|
447
|
+
const res = await new AndroidObserve().getCurrentScreen(deviceId);
|
|
456
448
|
return wrapResponse(res);
|
|
457
449
|
}
|
|
458
450
|
if (name === "wait_for_element") {
|
|
459
451
|
const { platform, text, timeout, deviceId } = (args || {});
|
|
460
|
-
const res = await
|
|
452
|
+
const res = await (platform === 'android' ? new AndroidInteract().waitForElement(text, timeout, deviceId) : new iOSInteract().waitForElement(text, timeout, deviceId));
|
|
461
453
|
return wrapResponse(res);
|
|
462
454
|
}
|
|
463
455
|
if (name === "tap") {
|
|
464
456
|
const { platform, x, y, deviceId } = (args || {});
|
|
465
|
-
const res = await
|
|
457
|
+
const res = await (platform === 'android' ? new AndroidInteract().tap(x, y, deviceId) : new iOSInteract().tap(x, y, deviceId));
|
|
466
458
|
return wrapResponse(res);
|
|
467
459
|
}
|
|
468
460
|
if (name === "swipe") {
|
|
469
461
|
const { x1, y1, x2, y2, duration, deviceId } = (args || {});
|
|
470
|
-
const res = await
|
|
462
|
+
const res = await new AndroidInteract().swipe(x1, y1, x2, y2, duration, deviceId);
|
|
471
463
|
return wrapResponse(res);
|
|
472
464
|
}
|
|
473
465
|
if (name === "type_text") {
|
|
474
466
|
const { text, deviceId } = (args || {});
|
|
475
|
-
const res = await
|
|
467
|
+
const res = await new AndroidInteract().typeText(text, deviceId);
|
|
476
468
|
return wrapResponse(res);
|
|
477
469
|
}
|
|
478
470
|
if (name === "press_back") {
|
|
479
471
|
const { deviceId } = (args || {});
|
|
480
|
-
const res = await
|
|
472
|
+
const res = await new AndroidInteract().pressBack(deviceId);
|
|
481
473
|
return wrapResponse(res);
|
|
482
474
|
}
|
|
483
475
|
if (name === 'start_log_stream') {
|
|
484
476
|
const { platform, packageName, level, sessionId, deviceId } = args;
|
|
485
|
-
const res = await startLogStreamHandler({ platform, packageName, level, sessionId, deviceId });
|
|
477
|
+
const res = await ToolsObserve.startLogStreamHandler({ platform, packageName, level, sessionId, deviceId });
|
|
486
478
|
return wrapResponse(res);
|
|
487
479
|
}
|
|
488
480
|
if (name === 'read_log_stream') {
|
|
489
481
|
const { platform, sessionId, limit, since } = args;
|
|
490
|
-
const res = await readLogStreamHandler({ platform, sessionId, limit, since });
|
|
482
|
+
const res = await ToolsObserve.readLogStreamHandler({ platform, sessionId, limit, since });
|
|
491
483
|
return wrapResponse(res);
|
|
492
484
|
}
|
|
493
485
|
if (name === 'stop_log_stream') {
|
|
494
486
|
const { platform, sessionId } = (args || {});
|
|
495
|
-
const res = await stopLogStreamHandler({ platform, sessionId });
|
|
487
|
+
const res = await ToolsObserve.stopLogStreamHandler({ platform, sessionId });
|
|
496
488
|
return wrapResponse(res);
|
|
497
489
|
}
|
|
498
490
|
}
|
package/dist/tools/install.js
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
export class ToolsInteract {
|
|
7
|
+
static async installAppHandler({ platform, appPath, deviceId }) {
|
|
8
|
+
let chosenPlatform = platform;
|
|
9
|
+
try {
|
|
10
|
+
const stat = await fs.stat(appPath).catch(() => null);
|
|
11
|
+
if (stat && stat.isDirectory()) {
|
|
12
|
+
const files = (await fs.readdir(appPath).catch(() => []));
|
|
13
|
+
if (files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) {
|
|
14
|
+
chosenPlatform = 'ios';
|
|
15
|
+
}
|
|
16
|
+
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)))) {
|
|
17
|
+
chosenPlatform = 'android';
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
chosenPlatform = 'android';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else if (typeof appPath === 'string') {
|
|
24
|
+
const ext = path.extname(appPath).toLowerCase();
|
|
25
|
+
if (ext === '.apk')
|
|
26
|
+
chosenPlatform = 'android';
|
|
27
|
+
else if (ext === '.ipa' || ext === '.app')
|
|
28
|
+
chosenPlatform = 'ios';
|
|
29
|
+
else
|
|
30
|
+
chosenPlatform = 'android';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
chosenPlatform = 'android';
|
|
35
|
+
}
|
|
36
|
+
if (chosenPlatform === 'android') {
|
|
37
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
38
|
+
const androidInteract = new AndroidInteract();
|
|
39
|
+
const result = await androidInteract.installApp(appPath, resolved.id);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
44
|
+
const iosInteract = new iOSInteract();
|
|
45
|
+
const result = await iosInteract.installApp(appPath, resolved.id);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
static async startAppHandler({ platform, appId, deviceId }) {
|
|
50
|
+
if (platform === 'android') {
|
|
51
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
52
|
+
return await new AndroidInteract().startApp(appId, resolved.id);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
56
|
+
return await new iOSInteract().startApp(appId, resolved.id);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
static async terminateAppHandler({ platform, appId, deviceId }) {
|
|
60
|
+
if (platform === 'android') {
|
|
61
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
62
|
+
return await new AndroidInteract().terminateApp(appId, resolved.id);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
66
|
+
return await new iOSInteract().terminateApp(appId, resolved.id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
static async restartAppHandler({ platform, appId, deviceId }) {
|
|
70
|
+
if (platform === 'android') {
|
|
71
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
72
|
+
return await new AndroidInteract().restartApp(appId, resolved.id);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
76
|
+
return await new iOSInteract().restartApp(appId, resolved.id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
static async resetAppDataHandler({ platform, appId, deviceId }) {
|
|
80
|
+
if (platform === 'android') {
|
|
81
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
82
|
+
return await new AndroidInteract().resetAppData(appId, resolved.id);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
86
|
+
return await new iOSInteract().resetAppData(appId, resolved.id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
package/dist/tools/logs.js
CHANGED
|
@@ -23,7 +23,7 @@ export async function getLogsHandler({ platform, appId, deviceId, lines }) {
|
|
|
23
23
|
const crashLines = logs.filter(l => l.includes('FATAL EXCEPTION'));
|
|
24
24
|
return { device: deviceInfo, logs, crashLines };
|
|
25
25
|
}
|
|
26
|
-
catch
|
|
26
|
+
catch {
|
|
27
27
|
return { device: deviceInfo, logs: [], crashLines: [] };
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -37,7 +37,7 @@ export async function startLogStreamHandler({ platform, packageName, level, sess
|
|
|
37
37
|
}
|
|
38
38
|
else {
|
|
39
39
|
const resolved = await resolveTargetDevice({ platform: 'ios', appId: packageName, deviceId });
|
|
40
|
-
return await startIOSLogStream(packageName,
|
|
40
|
+
return await startIOSLogStream(packageName, resolved.id, sid);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
export async function readLogStreamHandler({ platform, sessionId, limit, since }) {
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
export class ToolsObserve {
|
|
7
|
+
static async getLogsHandler({ platform, appId, deviceId, lines }) {
|
|
8
|
+
if (platform === 'android') {
|
|
9
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
10
|
+
const response = await new AndroidObserve().getLogs(appId, lines ?? 200, resolved.id);
|
|
11
|
+
const logs = Array.isArray(response.logs) ? response.logs : [];
|
|
12
|
+
const crashLines = logs.filter(line => line.includes('FATAL EXCEPTION'));
|
|
13
|
+
return { device: response.device, logs, crashLines };
|
|
14
|
+
}
|
|
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
|
+
static async startLogStreamHandler({ platform, packageName, level, sessionId, deviceId }) {
|
|
24
|
+
const effectivePlatform = platform || 'android';
|
|
25
|
+
const sid = sessionId || 'default';
|
|
26
|
+
if (effectivePlatform === 'android') {
|
|
27
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId: packageName, deviceId });
|
|
28
|
+
// AndroidObserve uses utils for log stream control; delegate to android/utils functions where appropriate
|
|
29
|
+
const { startAndroidLogStream } = await import('../android/utils.js');
|
|
30
|
+
return await startAndroidLogStream(packageName, level || 'error', resolved.id, sid);
|
|
31
|
+
}
|
|
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
|
+
static async readLogStreamHandler({ platform, sessionId, limit, since }) {
|
|
40
|
+
const effectivePlatform = platform || 'android';
|
|
41
|
+
const sid = sessionId || 'default';
|
|
42
|
+
if (effectivePlatform === 'android') {
|
|
43
|
+
const { readLogStreamLines } = await import('../android/utils.js');
|
|
44
|
+
return await readLogStreamLines(sid, limit ?? 100, since);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const { readIOSLogStreamLines } = await import('../ios/utils.js');
|
|
48
|
+
return await readIOSLogStreamLines(sid, limit ?? 100, since);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
static async stopLogStreamHandler({ platform, sessionId }) {
|
|
52
|
+
const effectivePlatform = platform || 'android';
|
|
53
|
+
const sid = sessionId || 'default';
|
|
54
|
+
if (effectivePlatform === 'android') {
|
|
55
|
+
const { stopAndroidLogStream } = await import('../android/utils.js');
|
|
56
|
+
return await stopAndroidLogStream(sid);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const { stopIOSLogStream } = await import('../ios/utils.js');
|
|
60
|
+
return await stopIOSLogStream(sid);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
static async getUITreeHandler({ platform, deviceId }) {
|
|
64
|
+
if (platform === 'android') {
|
|
65
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
66
|
+
return await new AndroidObserve().getUITree(resolved.id);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
70
|
+
return await new iOSObserve().getUITree(resolved.id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
static async getCurrentScreenHandler({ deviceId }) {
|
|
74
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
75
|
+
return await new AndroidObserve().getCurrentScreen(resolved.id);
|
|
76
|
+
}
|
|
77
|
+
static async waitForElementHandler({ platform, text, timeout, deviceId }) {
|
|
78
|
+
const effectiveTimeout = timeout ?? 10000;
|
|
79
|
+
if (platform === 'android') {
|
|
80
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
81
|
+
return await new AndroidInteract().waitForElement(text, effectiveTimeout, resolved.id);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
85
|
+
return await new iOSInteract().waitForElement(text, effectiveTimeout, resolved.id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
static async tapHandler({ platform, x, y, deviceId }) {
|
|
89
|
+
const effectivePlatform = platform || 'android';
|
|
90
|
+
if (effectivePlatform === 'android') {
|
|
91
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
92
|
+
return await new AndroidInteract().tap(x, y, resolved.id);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
96
|
+
return await new iOSInteract().tap(x, y, resolved.id);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
static async swipeHandler({ x1, y1, x2, y2, duration, deviceId }) {
|
|
100
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
101
|
+
return await new AndroidInteract().swipe(x1, y1, x2, y2, duration, resolved.id);
|
|
102
|
+
}
|
|
103
|
+
static async typeTextHandler({ text, deviceId }) {
|
|
104
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
105
|
+
return await new AndroidInteract().typeText(text, resolved.id);
|
|
106
|
+
}
|
|
107
|
+
static async pressBackHandler({ deviceId }) {
|
|
108
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
109
|
+
return await new AndroidInteract().pressBack(resolved.id);
|
|
110
|
+
}
|
|
111
|
+
static async listDevicesHandler({ platform, appId }) {
|
|
112
|
+
const devices = await listDevices(platform, appId);
|
|
113
|
+
return { devices };
|
|
114
|
+
}
|
|
115
|
+
static async captureScreenshotHandler({ platform, deviceId }) {
|
|
116
|
+
const effectivePlatform = platform || 'android';
|
|
117
|
+
if (effectivePlatform === 'android') {
|
|
118
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
119
|
+
return await new AndroidObserve().captureScreen(resolved.id);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
123
|
+
return await new iOSObserve().captureScreenshot(resolved.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { detectJavaHome } from './java.js';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export async function detectJavaHome() {
|
|
5
|
+
try {
|
|
6
|
+
// If JAVA_HOME is set, validate it's Java 17
|
|
7
|
+
if (process.env.JAVA_HOME) {
|
|
8
|
+
try {
|
|
9
|
+
const javaBin = path.join(process.env.JAVA_HOME, 'bin', 'java');
|
|
10
|
+
const v = execSync(`"${javaBin}" -version`, { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
|
|
11
|
+
if (/\b17\b/.test(v) || /17\./.test(v))
|
|
12
|
+
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
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
console.debug('[java.detect] Failed to validate existing JAVA_HOME, searching for JDK17');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// macOS explicit path
|
|
20
|
+
const explicit = '/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home';
|
|
21
|
+
if (existsSync(explicit))
|
|
22
|
+
return explicit;
|
|
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))
|
|
36
|
+
return p;
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// macOS /usr/libexec/java_home
|
|
42
|
+
try {
|
|
43
|
+
const out = execSync('/usr/libexec/java_home -v 17', { stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
44
|
+
if (out)
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
// macOS common JDK locations
|
|
49
|
+
try {
|
|
50
|
+
const homes = execSync('ls -1 /Library/Java/JavaVirtualMachines || true', { stdio: ['ignore', 'pipe', 'inherit'] }).toString().split(/\r?\n/).filter(Boolean);
|
|
51
|
+
for (const h of homes) {
|
|
52
|
+
if (h.toLowerCase().includes('17') || h.toLowerCase().includes('jdk-17')) {
|
|
53
|
+
const candidate = `/Library/Java/JavaVirtualMachines/${h}/Contents/Home`;
|
|
54
|
+
return candidate;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
// Linux locations
|
|
60
|
+
const linuxCandidates = [
|
|
61
|
+
'/usr/lib/jvm/java-17-openjdk-amd64',
|
|
62
|
+
'/usr/lib/jvm/java-17-openjdk',
|
|
63
|
+
'/usr/lib/jvm/zulu17',
|
|
64
|
+
'/usr/lib/jvm/temurin-17-jdk'
|
|
65
|
+
];
|
|
66
|
+
for (const p of linuxCandidates) {
|
|
67
|
+
try {
|
|
68
|
+
if (existsSync(p))
|
|
69
|
+
return p;
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,14 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [0.
|
|
5
|
+
## [0.10.0]
|
|
6
6
|
|
|
7
7
|
### Added / Changed
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
8
|
+
- Tools refactor: consolidated handlers into ToolsInteract and ToolsObserve classes to centralise tool wiring and simplify platform delegation.
|
|
9
|
+
- install_app now builds project directories (Gradle/xcodebuild) and supports streamed installs with robust fallbacks (adb push + pm install).
|
|
10
|
+
- Added log streaming utilities and improved log parsing/crash detection heuristics.
|
|
11
|
+
- CI: added lint and unit tests for handler parity; updated README links to docs and changelog.
|
|
12
|
+
- Docs: Created docs/TOOLS.md with comprehensive tool definitions and examples.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## [0.9.0]
|
|
16
|
+
|
|
17
|
+
### Added / Changed
|
|
18
|
+
- install_app now builds apps when given a project directory and then installs the produced artifact (Android: Gradle wrapper assembleDebug; iOS: xcodebuild where applicable). When a workspace (.xcworkspace) is present, the iOS build uses `-workspace` instead of `-project` to support CocoaPods and multi-project setups.
|
|
19
|
+
- Build orchestration uses a scoped JAVA_HOME (detectJavaHome) and prefers JDK 17 when available; Gradle invocations avoid mutating global env and pass java home via `-Dorg.gradle.java.home`.
|
|
20
|
+
- Streaming ADB support: added `spawnAdb()` (streams stdout/stderr and returns exit code) alongside `execAdb()` (returns buffered stdout). This enables live install output and robust fallbacks.
|
|
21
|
+
- Resilient install flow: streamed `adb install` is attempted first; on failure MCP falls back to `adb push` + `pm install -r` to improve reliability on devices that don't support streamed install or when install times out.
|
|
22
|
+
- Centralised timeout logic: extracted `getAdbTimeout(args, customTimeout)` to standardise timeout selection (precedence: custom timeout > MCP_ADB_TIMEOUT/ADB_TIMEOUT env > per-command defaults — install: 120s, logcat: 10s, uiautomator dump: 20s).
|
|
23
|
+
- Improved types: `execAdb` / `spawnAdb` now accept `SpawnOptionsWithTimeout` (typed extension of Node's SpawnOptions with an optional timeout property).
|
|
24
|
+
- Linting and CI: added ESLint (unused-imports plugin), added `npm run lint` / `npm run lint:fix` scripts, and updated CI to run lint in the unit job. ESLint config converted to the flat `eslint.config.js` format.
|
|
25
|
+
- Tests: unit tests updated to exercise real build/install flows using fake `adb` and `gradlew` wrappers; added detectJavaHome smoke tests. Integration workflows remain manual and require device/emulator access.
|
|
26
|
+
- Misc: improved logging, more informative error messages, and several internal cleanups (removed redundant try/catch, consolidated helper functions).
|
|
27
|
+
|
|
13
28
|
|
|
14
29
|
## [0.8.0]
|
|
15
30
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module.exports = [
|
|
2
|
+
// Files/directories to ignore
|
|
3
|
+
{
|
|
4
|
+
ignores: [
|
|
5
|
+
'dist/',
|
|
6
|
+
'node_modules/',
|
|
7
|
+
'.git/',
|
|
8
|
+
'.vscode/',
|
|
9
|
+
'coverage/',
|
|
10
|
+
'.env'
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
// Apply rules to JS/TS source and tests
|
|
14
|
+
{
|
|
15
|
+
files: ['src/**/*.ts', 'test/**/*.ts', 'src/**/*.js', 'test/**/*.js'],
|
|
16
|
+
languageOptions: {
|
|
17
|
+
parser: require.resolve('@typescript-eslint/parser'),
|
|
18
|
+
parserOptions: {
|
|
19
|
+
ecmaVersion: 2020,
|
|
20
|
+
sourceType: 'module',
|
|
21
|
+
project: './tsconfig.json'
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
plugins: {
|
|
25
|
+
'@typescript-eslint': require('@typescript-eslint/eslint-plugin'),
|
|
26
|
+
'unused-imports': require('eslint-plugin-unused-imports')
|
|
27
|
+
},
|
|
28
|
+
rules: {
|
|
29
|
+
// Use plugin to error on unused imports and provide autofix where possible
|
|
30
|
+
'unused-imports/no-unused-imports': 'error',
|
|
31
|
+
'unused-imports/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
|
|
32
|
+
// Disable the default TS rule to avoid duplicate warnings
|
|
33
|
+
'@typescript-eslint/no-unused-vars': 'off'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import tsParser from '@typescript-eslint/parser'
|
|
2
|
+
import tsPlugin from '@typescript-eslint/eslint-plugin'
|
|
3
|
+
import unusedImports from 'eslint-plugin-unused-imports'
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
// Files/directories to ignore
|
|
7
|
+
{
|
|
8
|
+
ignores: [
|
|
9
|
+
'dist/',
|
|
10
|
+
'node_modules/',
|
|
11
|
+
'.git/',
|
|
12
|
+
'.vscode/',
|
|
13
|
+
'coverage/',
|
|
14
|
+
'.env'
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
// Apply rules to JS/TS source
|
|
18
|
+
{
|
|
19
|
+
files: ['src/**/*.ts', 'src/**/*.js'],
|
|
20
|
+
languageOptions: {
|
|
21
|
+
parser: tsParser,
|
|
22
|
+
parserOptions: {
|
|
23
|
+
ecmaVersion: 2020,
|
|
24
|
+
sourceType: 'module',
|
|
25
|
+
project: './tsconfig.json'
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
plugins: {
|
|
29
|
+
'@typescript-eslint': tsPlugin,
|
|
30
|
+
'unused-imports': unusedImports
|
|
31
|
+
},
|
|
32
|
+
rules: {
|
|
33
|
+
// Use plugin to error on unused imports and provide autofix where possible
|
|
34
|
+
'unused-imports/no-unused-imports': 'error',
|
|
35
|
+
'unused-imports/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
|
|
36
|
+
// Disable the default TS rule to avoid duplicate warnings
|
|
37
|
+
'@typescript-eslint/no-unused-vars': 'off'
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
// Apply lighter rules to test files (no project reference to avoid TS project parsing)
|
|
41
|
+
{
|
|
42
|
+
files: ['test/**/*.ts', 'test/**/*.js'],
|
|
43
|
+
languageOptions: {
|
|
44
|
+
parser: tsParser,
|
|
45
|
+
parserOptions: {
|
|
46
|
+
ecmaVersion: 2020,
|
|
47
|
+
sourceType: 'module'
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
plugins: {
|
|
51
|
+
'@typescript-eslint': tsPlugin,
|
|
52
|
+
'unused-imports': unusedImports
|
|
53
|
+
},
|
|
54
|
+
rules: {
|
|
55
|
+
'unused-imports/no-unused-imports': 'error',
|
|
56
|
+
'unused-imports/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
|
|
57
|
+
'@typescript-eslint/no-unused-vars': 'off'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]
|