mobile-debug-mcp 0.7.0 → 0.9.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/README.md +18 -443
- package/dist/android/interact.js +96 -1
- package/dist/android/utils.js +404 -12
- package/dist/ios/interact.js +105 -0
- package/dist/ios/utils.js +154 -0
- package/dist/resolve-device.js +70 -0
- package/dist/server.js +126 -194
- package/dist/tools/app.js +45 -0
- package/dist/tools/devices.js +5 -0
- package/dist/tools/install.js +47 -0
- package/dist/tools/logs.js +62 -0
- package/dist/tools/screenshot.js +17 -0
- package/dist/tools/ui.js +57 -0
- package/docs/CHANGELOG.md +19 -0
- package/docs/TOOLS.md +272 -0
- package/package.json +6 -2
- package/src/android/interact.ts +100 -1
- package/src/android/utils.ts +395 -10
- package/src/ios/interact.ts +102 -0
- package/src/ios/utils.ts +157 -0
- package/src/resolve-device.ts +80 -0
- package/src/server.ts +149 -276
- package/src/tools/app.ts +46 -0
- package/src/tools/devices.ts +6 -0
- package/src/tools/install.ts +43 -0
- package/src/tools/logs.ts +62 -0
- package/src/tools/screenshot.ts +18 -0
- package/src/tools/ui.ts +62 -0
- package/src/types.ts +7 -0
- package/test/integration/index.ts +8 -0
- package/test/integration/install.integration.ts +64 -0
- package/test/integration/logstream-real.ts +35 -0
- package/test/integration/run-install-android.ts +21 -0
- package/test/integration/run-install-ios.ts +21 -0
- package/test/integration/run-real-test.ts +19 -0
- package/{smoke-test.ts → test/integration/smoke-test.ts} +17 -25
- package/test/integration/test-dist.mjs +41 -0
- package/test/integration/test-dist.ts +41 -0
- package/{test-ui-tree.ts → test/integration/test-ui-tree.ts} +2 -2
- package/test/integration/wait_for_element_real.ts +80 -0
- package/test/unit/index.ts +7 -0
- package/test/unit/install.test.ts +82 -0
- package/test/unit/logparse.test.ts +41 -0
- package/test/unit/logstream.test.ts +46 -0
- package/test/unit/wait_for_element_mock.ts +104 -0
- package/tsconfig.json +1 -1
- package/smoke-test.js +0 -102
- package/test/run-real-test.js +0 -24
- package/test/wait_for_element_mock.js +0 -113
- package/test/wait_for_element_real.js +0 -67
- package/test-ui-tree.js +0 -68
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
const androidInteract = new AndroidInteract();
|
|
7
|
+
const iosInteract = new iOSInteract();
|
|
8
|
+
export async function installAppHandler({ platform, appPath, deviceId }) {
|
|
9
|
+
let chosenPlatform = platform;
|
|
10
|
+
try {
|
|
11
|
+
const stat = await fs.stat(appPath).catch(() => null);
|
|
12
|
+
if (stat && stat.isDirectory()) {
|
|
13
|
+
const files = (await fs.readdir(appPath).catch(() => []));
|
|
14
|
+
if (files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) {
|
|
15
|
+
chosenPlatform = 'ios';
|
|
16
|
+
}
|
|
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
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
chosenPlatform = 'android';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else if (typeof appPath === 'string') {
|
|
25
|
+
const ext = path.extname(appPath).toLowerCase();
|
|
26
|
+
if (ext === '.apk')
|
|
27
|
+
chosenPlatform = 'android';
|
|
28
|
+
else if (ext === '.ipa' || ext === '.app')
|
|
29
|
+
chosenPlatform = 'ios';
|
|
30
|
+
else
|
|
31
|
+
chosenPlatform = 'android';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
chosenPlatform = 'android';
|
|
36
|
+
}
|
|
37
|
+
if (chosenPlatform === 'android') {
|
|
38
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
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 result = await iosInteract.installApp(appPath, resolved.id);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { resolveTargetDevice } from '../resolve-device.js';
|
|
2
|
+
import { AndroidObserve } from '../android/observe.js';
|
|
3
|
+
import { iOSObserve } from '../ios/observe.js';
|
|
4
|
+
import { startAndroidLogStream, readLogStreamLines, stopAndroidLogStream } from '../android/utils.js';
|
|
5
|
+
import { startIOSLogStream, readIOSLogStreamLines, stopIOSLogStream } from '../ios/utils.js';
|
|
6
|
+
const androidObserve = new AndroidObserve();
|
|
7
|
+
export async function getLogsHandler({ platform, appId, deviceId, lines }) {
|
|
8
|
+
if (platform === 'android') {
|
|
9
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
10
|
+
const deviceInfo = resolved;
|
|
11
|
+
const response = await 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: deviceInfo, logs, crashLines };
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
18
|
+
const deviceInfo = resolved;
|
|
19
|
+
try {
|
|
20
|
+
const iosObs = new iOSObserve();
|
|
21
|
+
const resp = await iosObs.getLogs(appId, resolved.id);
|
|
22
|
+
const logs = Array.isArray(resp.logs) ? resp.logs : [];
|
|
23
|
+
const crashLines = logs.filter(l => l.includes('FATAL EXCEPTION'));
|
|
24
|
+
return { device: deviceInfo, logs, crashLines };
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
return { device: deviceInfo, logs: [], crashLines: [] };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function startLogStreamHandler({ platform, packageName, level, sessionId, deviceId }) {
|
|
32
|
+
const effectivePlatform = platform || 'android';
|
|
33
|
+
const sid = sessionId || 'default';
|
|
34
|
+
if (effectivePlatform === 'android') {
|
|
35
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId: packageName, deviceId });
|
|
36
|
+
return await startAndroidLogStream(packageName, level || 'error', resolved.id, sid);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId: packageName, deviceId });
|
|
40
|
+
return await startIOSLogStream(packageName, level || 'error', resolved.id, sid);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function readLogStreamHandler({ platform, sessionId, limit, since }) {
|
|
44
|
+
const effectivePlatform = platform || 'android';
|
|
45
|
+
const sid = sessionId || 'default';
|
|
46
|
+
if (effectivePlatform === 'android') {
|
|
47
|
+
return await readLogStreamLines(sid, limit ?? 100, since);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return await readIOSLogStreamLines(sid, limit ?? 100, since);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function stopLogStreamHandler({ platform, sessionId }) {
|
|
54
|
+
const effectivePlatform = platform || 'android';
|
|
55
|
+
const sid = sessionId || 'default';
|
|
56
|
+
if (effectivePlatform === 'android') {
|
|
57
|
+
return await stopAndroidLogStream(sid);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
return await stopIOSLogStream(sid);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { resolveTargetDevice } from '../resolve-device.js';
|
|
2
|
+
import { AndroidObserve } from '../android/observe.js';
|
|
3
|
+
import { iOSObserve } from '../ios/observe.js';
|
|
4
|
+
const androidObserve = new AndroidObserve();
|
|
5
|
+
const iosObserve = new iOSObserve();
|
|
6
|
+
export async function captureScreenshotHandler({ platform, deviceId }) {
|
|
7
|
+
if (platform === 'android') {
|
|
8
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
9
|
+
const result = await androidObserve.captureScreen(resolved.id);
|
|
10
|
+
return { device: resolved, resolution: result.resolution, screenshot: result.screenshot };
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
14
|
+
const result = await iosObserve.captureScreenshot(resolved.id);
|
|
15
|
+
return { device: resolved, resolution: result.resolution, screenshot: result.screenshot };
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/tools/ui.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { resolveTargetDevice } 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
|
+
const androidObserve = new AndroidObserve();
|
|
7
|
+
const iosObserve = new iOSObserve();
|
|
8
|
+
const androidInteract = new AndroidInteract();
|
|
9
|
+
const iosInteract = new iOSInteract();
|
|
10
|
+
export async function getUITreeHandler({ platform, deviceId }) {
|
|
11
|
+
if (platform === 'android') {
|
|
12
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
13
|
+
return await androidObserve.getUITree(resolved.id);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
17
|
+
return await iosObserve.getUITree(resolved.id);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function getCurrentScreenHandler({ deviceId }) {
|
|
21
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
22
|
+
return await androidObserve.getCurrentScreen(resolved.id);
|
|
23
|
+
}
|
|
24
|
+
export async function waitForElementHandler({ platform, text, timeout, deviceId }) {
|
|
25
|
+
const effectiveTimeout = timeout ?? 10000;
|
|
26
|
+
if (platform === 'android') {
|
|
27
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
28
|
+
return await androidInteract.waitForElement(text, effectiveTimeout, resolved.id);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
32
|
+
return await iosInteract.waitForElement(text, effectiveTimeout, resolved.id);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function tapHandler({ platform, x, y, deviceId }) {
|
|
36
|
+
const effectivePlatform = platform || 'android';
|
|
37
|
+
if (effectivePlatform === 'android') {
|
|
38
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
39
|
+
return await androidInteract.tap(x, y, resolved.id);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
43
|
+
return await iosInteract.tap(x, y, resolved.id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function swipeHandler({ x1, y1, x2, y2, duration, deviceId }) {
|
|
47
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
48
|
+
return await androidInteract.swipe(x1, y1, x2, y2, duration, resolved.id);
|
|
49
|
+
}
|
|
50
|
+
export async function typeTextHandler({ text, deviceId }) {
|
|
51
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
52
|
+
return await androidInteract.typeText(text, resolved.id);
|
|
53
|
+
}
|
|
54
|
+
export async function pressBackHandler({ deviceId }) {
|
|
55
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
56
|
+
return await androidInteract.pressBack(resolved.id);
|
|
57
|
+
}
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.9.0] - 2026-03-14
|
|
6
|
+
|
|
7
|
+
### Added / Changed
|
|
8
|
+
- install_app now builds apps when given a project directory and then installs the produced artifact (Android: Gradle wrapper assembleDebug; iOS: xcodebuild where applicable).
|
|
9
|
+
- Auto-detects and prefers JDK 17 (Android Studio JBR or system JDK). Any JAVA_HOME overrides are scoped to the spawned build process, avoiding global system changes.
|
|
10
|
+
- Respects ADB_PATH and falls back to PATH if unset. Set ADB_PATH to an explicit adb binary to avoid PATH discovery issues.
|
|
11
|
+
- Increased default adb timeout to 120s for installs; timeout can be configured via MCP_ADB_TIMEOUT or ADB_TIMEOUT environment variables.
|
|
12
|
+
- Improved logging and error messages for build/install steps. Unit and integration tests updated and converted to TypeScript.
|
|
13
|
+
|
|
14
|
+
## [0.8.0]
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **`list_devices` tool**: enumerate connected Android devices and iOS simulators. Returns device metadata (id, platform, osVersion, model, simulator, appInstalled).
|
|
18
|
+
- **`install_app` tool**: install an APK (.apk) on Android or an app bundle (.app/.ipa) on iOS simulators/devices. Uses `adb install -r` for Android and `simctl`/`idb` for iOS.
|
|
19
|
+
- **`start_log_stream`, `read_log_stream`, `stop_log_stream` tools**: stream Android logcat filtered by application PID, poll parsed entries, support incremental reads (limit/since) and basic crash detection metadata (crash_detected, exception, sample).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Device-selection: server handlers now use a central resolver to pick a sensible default device when `deviceId` is omitted. This reduces duplication and makes behavior deterministic when multiple devices are attached.
|
|
23
|
+
|
|
5
24
|
## [0.7.0]
|
|
6
25
|
|
|
7
26
|
### Added
|
package/docs/TOOLS.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Tools
|
|
2
|
+
|
|
3
|
+
This document contains detailed definitions for each MCP tool implemented by Mobile Debug MCP. These were extracted from the README to keep the top-level README concise.
|
|
4
|
+
|
|
5
|
+
Each tool returns a JSON metadata block and, where applicable, additional content blocks (e.g., image data or raw logs). Where an example previously used `jsonc` fences with inline comments, the examples below use `json` fences and external explanatory notes to avoid highlighting issues.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## list_devices
|
|
10
|
+
Enumerate connected Android devices and iOS simulators.
|
|
11
|
+
|
|
12
|
+
Input (optional):
|
|
13
|
+
```json
|
|
14
|
+
{ "platform": "android" | "ios" }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Response:
|
|
18
|
+
```json
|
|
19
|
+
{ "devices": [ { "id": "emulator-5554", "platform": "android", "osVersion": "11", "model": "sdk_gphone64_arm64", "simulator": true, "appInstalled": false } ] }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Notes:
|
|
23
|
+
- Use `list_devices` to inspect connected targets and their metadata. When multiple devices are attached, pass `deviceId` to other tools to target a specific device.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## install_app
|
|
28
|
+
Install an app onto a connected device or simulator (APK for Android, .app/.ipa for iOS).
|
|
29
|
+
|
|
30
|
+
Input:
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"platform": "android" | "ios",
|
|
34
|
+
"appPath": "/path/to/app.apk_or_app.app_or_ipa",
|
|
35
|
+
"deviceId": "emulator-5554"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Response:
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"device": { /* device info */ },
|
|
43
|
+
"installed": true,
|
|
44
|
+
"output": "Platform-specific installer output (adb/simctl/idb)",
|
|
45
|
+
"error": "Optional error message if installation failed"
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Notes:
|
|
50
|
+
- Android: the tool attempts to locate and install the APK. If `appPath` points at a project directory, the tool will attempt to run the Gradle wrapper (`./gradlew assembleDebug`) and locate the built APK under `build/outputs/apk/`.
|
|
51
|
+
- The installer respects `ADB_PATH` (preferred) falling back to `adb` on PATH. To avoid PATH discovery issues, set `ADB_PATH` to the full adb binary path.
|
|
52
|
+
- The default ADB command timeout was increased to 120s to handle larger streamed installs. Configure via `MCP_ADB_TIMEOUT` or `ADB_TIMEOUT` env vars.
|
|
53
|
+
- iOS: prefers `xcrun simctl install` for simulators and falls back to `idb install` for devices when available.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## start_app
|
|
58
|
+
Launch a mobile app.
|
|
59
|
+
|
|
60
|
+
Input:
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"platform": "android" | "ios",
|
|
64
|
+
"appId": "com.example.app",
|
|
65
|
+
"deviceId": "emulator-5554"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Response:
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"device": { /* device info */ },
|
|
73
|
+
"appStarted": true,
|
|
74
|
+
"launchTimeMs": 123
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Notes:
|
|
79
|
+
- Android: uses `adb shell monkey -p <package> -c android.intent.category.LAUNCHER 1` to trigger a launch.
|
|
80
|
+
- iOS: uses `xcrun simctl launch` for simulators or `idb` for devices when available.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## get_logs
|
|
85
|
+
Fetch recent logs from the app or device.
|
|
86
|
+
|
|
87
|
+
Input:
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"platform": "android" | "ios",
|
|
91
|
+
"appId": "com.example.app",
|
|
92
|
+
"deviceId": "emulator-5554",
|
|
93
|
+
"lines": 200
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Response:
|
|
98
|
+
- The tool returns two content blocks for Android: a JSON metadata block and a plain text log output block. The JSON metadata includes parsed results (counts, crash summaries) and the raw log is provided for inspection.
|
|
99
|
+
|
|
100
|
+
Notes:
|
|
101
|
+
- Android log parsing is heuristic and includes basic crash detection (searching for "FATAL EXCEPTION" and exception names).
|
|
102
|
+
- Use `lines` to control how many log lines are returned from `adb logcat`.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## capture_screenshot
|
|
107
|
+
Capture a screenshot of the current device screen.
|
|
108
|
+
|
|
109
|
+
Input:
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"platform": "android" | "ios",
|
|
113
|
+
"deviceId": "emulator-5554"
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Response:
|
|
118
|
+
- JSON metadata block with resolution and device info, followed by an image/png block containing base64-encoded PNG bytes.
|
|
119
|
+
|
|
120
|
+
Notes:
|
|
121
|
+
- Android: uses `adb exec-out screencap -p` and returns PNG bytes.
|
|
122
|
+
- iOS: uses `xcrun simctl io booted screenshot` or `idb` capture when available.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## terminate_app
|
|
127
|
+
Terminate a running app.
|
|
128
|
+
|
|
129
|
+
Input:
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"platform": "android" | "ios",
|
|
133
|
+
"appId": "com.example.app",
|
|
134
|
+
"deviceId": "emulator-5554"
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Response:
|
|
139
|
+
```json
|
|
140
|
+
{ "device": { /* device info */ }, "appTerminated": true }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Notes:
|
|
144
|
+
- Android: uses `adb shell am force-stop <package>`.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## restart_app
|
|
149
|
+
Restart an app (terminate then launch).
|
|
150
|
+
|
|
151
|
+
Input/Response: combination of terminate + start as above. Response includes launch timing metadata.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## reset_app_data
|
|
156
|
+
Clear app storage (reset to fresh install state).
|
|
157
|
+
|
|
158
|
+
Input:
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"platform": "android" | "ios",
|
|
162
|
+
"appId": "com.example.app",
|
|
163
|
+
"deviceId": "emulator-5554"
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Response:
|
|
168
|
+
```json
|
|
169
|
+
{ "device": { /* device info */ }, "dataCleared": true }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Notes:
|
|
173
|
+
- Android: uses `adb shell pm clear <package>` and returns whether the operation succeeded.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## start_log_stream / read_log_stream / stop_log_stream
|
|
178
|
+
Start a live log stream (background) for an Android app and poll the accumulated entries.
|
|
179
|
+
|
|
180
|
+
- start_log_stream starts a background `adb logcat` filtered by the app PID and writes parsed NDJSON to a per-session file. Returns immediately with session details.
|
|
181
|
+
- read_log_stream retrieves recent parsed entries and includes crash detection metadata.
|
|
182
|
+
- stop_log_stream terminates the background process and closes the stream.
|
|
183
|
+
|
|
184
|
+
Input (start_log_stream):
|
|
185
|
+
```json
|
|
186
|
+
{ "packageName": "com.example.app", "level": "error" | "warn" | "info" | "debug", "sessionId": "optional-session-id" }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Response (read_log_stream):
|
|
190
|
+
```json
|
|
191
|
+
{ "entries": [ /* parsed entries */ ], "crash_summary": { "crash_detected": true/false, "exception": "..." } }
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Notes:
|
|
195
|
+
- The `since` parameter for read_log_stream accepts ISO timestamps or epoch ms. Use it for incremental polling.
|
|
196
|
+
- Crash detection is heuristic-based and intended as a quick signal for agents.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## get_ui_tree
|
|
201
|
+
Get the current UI hierarchy from the device.
|
|
202
|
+
|
|
203
|
+
Input:
|
|
204
|
+
```json
|
|
205
|
+
{ "platform": "android" | "ios", "deviceId": "emulator-5554" }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Response:
|
|
209
|
+
- Structured JSON containing screen metadata and an array of UI elements with properties: text, contentDescription, type, resourceId, clickable, enabled, visible, bounds, center, depth, parentId, children.
|
|
210
|
+
|
|
211
|
+
Notes:
|
|
212
|
+
- Android: uses `uiautomator dump` or `adb exec-out uiautomator` fallback methods. Times out on slow responses; use provided timeouts.
|
|
213
|
+
- iOS: uses `idb` or accessibility APIs when available.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## get_current_screen
|
|
218
|
+
Get the currently visible activity on Android.
|
|
219
|
+
|
|
220
|
+
Input:
|
|
221
|
+
```json
|
|
222
|
+
{ "deviceId": "emulator-5554" }
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Response:
|
|
226
|
+
```json
|
|
227
|
+
{ "device": { /* device info */ }, "package": "com.example.app", "activity": "com.example.app.LoginActivity", "shortActivity": "LoginActivity" }
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Notes:
|
|
231
|
+
- Uses `dumpsys activity activities` and robust parsing to support multiple Android versions.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## wait_for_element
|
|
236
|
+
Wait until a UI element with matching text appears on screen or timeout is reached.
|
|
237
|
+
|
|
238
|
+
Input:
|
|
239
|
+
```json
|
|
240
|
+
{ "platform": "android" | "ios", "text": "Home", "timeout": 5000, "deviceId": "emulator-5554" }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Response:
|
|
244
|
+
```json
|
|
245
|
+
{ "device": { /* device info */ }, "found": true/false, "element": { /* element */ } }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Notes:
|
|
249
|
+
- Polls get_ui_tree until timeout or element found. Returns an `error` field if system failures occur.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## tap / swipe / type_text / press_back
|
|
254
|
+
|
|
255
|
+
- tap: `adb shell input tap x y` (Android) or `idb` events for iOS.
|
|
256
|
+
- swipe: `adb shell input swipe x1 y1 x2 y2 duration`.
|
|
257
|
+
- type_text: `adb shell input text` (spaces encoded as %s) — may fail for special characters.
|
|
258
|
+
- press_back: `adb shell input keyevent 4`.
|
|
259
|
+
|
|
260
|
+
Inputs and responses follow the device+success pattern used across other tools.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Notes on environment and timeout behavior
|
|
265
|
+
|
|
266
|
+
- The tools prefer explicit env vars: `ADB_PATH` and `XCRUN_PATH` to locate platform binaries. If unset, tools fall back to PATH lookup.
|
|
267
|
+
- For Android builds, the install tool auto-detects a suitable Java 17 installation (Android Studio JBR or system JDK 17). Any JAVA_HOME overrides are scoped to the spawned Gradle process.
|
|
268
|
+
- Default ADB timeout is now 120s for long operations; override via `MCP_ADB_TIMEOUT` or `ADB_TIMEOUT`.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
If you need the tools split into per-tool markdown files (e.g., docs/tools/install.md), say so and I will split them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-debug-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"start": "node ./dist/server.js",
|
|
12
|
-
"prepare": "npm run build"
|
|
12
|
+
"prepare": "npm run build",
|
|
13
|
+
"test:unit": "tsx test/unit/index.ts",
|
|
14
|
+
"test:integration": "tsx test/integration/index.ts",
|
|
15
|
+
"test": "npm run test:unit && npm run test:integration"
|
|
13
16
|
},
|
|
14
17
|
"engines": {
|
|
15
18
|
"node": ">=18"
|
|
@@ -21,6 +24,7 @@
|
|
|
21
24
|
},
|
|
22
25
|
"devDependencies": {
|
|
23
26
|
"@types/node": "^25.4.0",
|
|
27
|
+
"tsx": "^4.21.0",
|
|
24
28
|
"typescript": "^5.9.3"
|
|
25
29
|
}
|
|
26
30
|
}
|
package/src/android/interact.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse, WaitForElementResponse, TapResponse, SwipeResponse, TypeTextResponse, PressBackResponse } from "../types.js"
|
|
2
|
-
import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "./utils.js"
|
|
2
|
+
import { execAdb, getAndroidDeviceMetadata, getDeviceInfo, detectJavaHome } from "./utils.js"
|
|
3
3
|
import { AndroidObserve } from "./observe.js"
|
|
4
|
+
import { promises as fs } from "fs"
|
|
5
|
+
import { spawn, execSync } from "child_process"
|
|
6
|
+
import path from "path"
|
|
7
|
+
import { existsSync } from "fs"
|
|
8
|
+
|
|
4
9
|
|
|
5
10
|
export class AndroidInteract {
|
|
6
11
|
private observe = new AndroidObserve();
|
|
@@ -87,6 +92,100 @@ export class AndroidInteract {
|
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
|
|
95
|
+
async installApp(apkPath: string, deviceId?: string): Promise<import("../types.js").InstallAppResponse> {
|
|
96
|
+
const metadata = await getAndroidDeviceMetadata("", deviceId)
|
|
97
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
98
|
+
|
|
99
|
+
// Helper to recursively find first APK under a directory
|
|
100
|
+
async function findApk(dir: string): Promise<string | undefined> {
|
|
101
|
+
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => [])
|
|
102
|
+
for (const e of entries) {
|
|
103
|
+
const full = path.join(dir, e.name)
|
|
104
|
+
if (e.isDirectory()) {
|
|
105
|
+
const found = await findApk(full)
|
|
106
|
+
if (found) return found
|
|
107
|
+
} else if (e.isFile() && full.endsWith('.apk')) {
|
|
108
|
+
return full
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return undefined
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
let apkToInstall = apkPath
|
|
116
|
+
|
|
117
|
+
// If a directory is provided, attempt to build via Gradle
|
|
118
|
+
const stat = await fs.stat(apkPath).catch(() => null)
|
|
119
|
+
if (stat && stat.isDirectory()) {
|
|
120
|
+
const gradlewPath = path.join(apkPath, 'gradlew')
|
|
121
|
+
const gradleCmd = existsSync(gradlewPath) ? './gradlew' : 'gradle'
|
|
122
|
+
|
|
123
|
+
await new Promise<void>(async (resolve, reject) => {
|
|
124
|
+
// Auto-detect and set JAVA_HOME (prefer JDK 17) so builds don't require manual environment setup
|
|
125
|
+
const detectedJavaHome = await detectJavaHome().catch(() => undefined)
|
|
126
|
+
const env = Object.assign({}, process.env)
|
|
127
|
+
if (detectedJavaHome) {
|
|
128
|
+
// Override existing JAVA_HOME if detection found a preferably compatible JDK (e.g., JDK 17).
|
|
129
|
+
if (env.JAVA_HOME !== detectedJavaHome) {
|
|
130
|
+
env.JAVA_HOME = detectedJavaHome
|
|
131
|
+
// Also ensure the JDK bin is on PATH so tools like jlink/javac are resolved from the detected JDK
|
|
132
|
+
env.PATH = `${path.join(detectedJavaHome, 'bin')}${path.delimiter}${env.PATH || ''}`
|
|
133
|
+
console.debug('[android] Overriding JAVA_HOME with detected path:', detectedJavaHome)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Sanitize environment so user shell init scripts are less likely to override our JAVA_HOME.
|
|
138
|
+
try {
|
|
139
|
+
// Remove obvious shell profile hints; avoid touching SDKMAN symlinks or on-disk state.
|
|
140
|
+
delete env.SHELL
|
|
141
|
+
} catch (e) {}
|
|
142
|
+
|
|
143
|
+
// If we detected a compatible JDK, instruct Gradle to use it and avoid daemon reuse
|
|
144
|
+
// Prepare gradle invocation
|
|
145
|
+
const gradleArgs = ['assembleDebug']
|
|
146
|
+
if (detectedJavaHome) {
|
|
147
|
+
gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`)
|
|
148
|
+
gradleArgs.push('--no-daemon')
|
|
149
|
+
env.GRADLE_JAVA_HOME = detectedJavaHome
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Prefer invoking the wrapper directly without a shell to avoid user profile shims (sdkman) re-setting JAVA_HOME
|
|
153
|
+
const wrapperPath = path.join(apkPath, 'gradlew')
|
|
154
|
+
const useWrapper = existsSync(wrapperPath)
|
|
155
|
+
const execCmd = useWrapper ? wrapperPath : gradleCmd
|
|
156
|
+
const spawnOpts: any = { cwd: apkPath, env }
|
|
157
|
+
// When using wrapper, ensure it's executable and invoke directly (no shell)
|
|
158
|
+
if (useWrapper) {
|
|
159
|
+
// Ensure the wrapper is executable; swallow errors from chmod (best-effort).
|
|
160
|
+
await fs.chmod(wrapperPath, 0o755).catch(() => {})
|
|
161
|
+
spawnOpts.shell = false
|
|
162
|
+
} else {
|
|
163
|
+
// if using system 'gradle' allow shell to resolve platform PATH
|
|
164
|
+
spawnOpts.shell = true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const proc = spawn(execCmd, gradleArgs, spawnOpts)
|
|
168
|
+
let stderr = ''
|
|
169
|
+
proc.stderr?.on('data', d => stderr += d.toString())
|
|
170
|
+
proc.on('close', code => {
|
|
171
|
+
if (code === 0) resolve()
|
|
172
|
+
else reject(new Error(stderr || `Gradle build failed with code ${code}`))
|
|
173
|
+
})
|
|
174
|
+
proc.on('error', err => reject(err))
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const built = await findApk(apkPath)
|
|
178
|
+
if (!built) throw new Error('Could not locate built APK after running Gradle')
|
|
179
|
+
apkToInstall = built
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const output = await execAdb(['install', '-r', apkToInstall], deviceId)
|
|
183
|
+
return { device: deviceInfo, installed: true, output }
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return { device: deviceInfo, installed: false, error: e instanceof Error ? e.message : String(e) }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
90
189
|
async startApp(appId: string, deviceId?: string): Promise<StartAppResponse> {
|
|
91
190
|
const metadata = await getAndroidDeviceMetadata(appId, deviceId)
|
|
92
191
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|