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.
Files changed (51) hide show
  1. package/README.md +18 -443
  2. package/dist/android/interact.js +96 -1
  3. package/dist/android/utils.js +404 -12
  4. package/dist/ios/interact.js +105 -0
  5. package/dist/ios/utils.js +154 -0
  6. package/dist/resolve-device.js +70 -0
  7. package/dist/server.js +126 -194
  8. package/dist/tools/app.js +45 -0
  9. package/dist/tools/devices.js +5 -0
  10. package/dist/tools/install.js +47 -0
  11. package/dist/tools/logs.js +62 -0
  12. package/dist/tools/screenshot.js +17 -0
  13. package/dist/tools/ui.js +57 -0
  14. package/docs/CHANGELOG.md +19 -0
  15. package/docs/TOOLS.md +272 -0
  16. package/package.json +6 -2
  17. package/src/android/interact.ts +100 -1
  18. package/src/android/utils.ts +395 -10
  19. package/src/ios/interact.ts +102 -0
  20. package/src/ios/utils.ts +157 -0
  21. package/src/resolve-device.ts +80 -0
  22. package/src/server.ts +149 -276
  23. package/src/tools/app.ts +46 -0
  24. package/src/tools/devices.ts +6 -0
  25. package/src/tools/install.ts +43 -0
  26. package/src/tools/logs.ts +62 -0
  27. package/src/tools/screenshot.ts +18 -0
  28. package/src/tools/ui.ts +62 -0
  29. package/src/types.ts +7 -0
  30. package/test/integration/index.ts +8 -0
  31. package/test/integration/install.integration.ts +64 -0
  32. package/test/integration/logstream-real.ts +35 -0
  33. package/test/integration/run-install-android.ts +21 -0
  34. package/test/integration/run-install-ios.ts +21 -0
  35. package/test/integration/run-real-test.ts +19 -0
  36. package/{smoke-test.ts → test/integration/smoke-test.ts} +17 -25
  37. package/test/integration/test-dist.mjs +41 -0
  38. package/test/integration/test-dist.ts +41 -0
  39. package/{test-ui-tree.ts → test/integration/test-ui-tree.ts} +2 -2
  40. package/test/integration/wait_for_element_real.ts +80 -0
  41. package/test/unit/index.ts +7 -0
  42. package/test/unit/install.test.ts +82 -0
  43. package/test/unit/logparse.test.ts +41 -0
  44. package/test/unit/logstream.test.ts +46 -0
  45. package/test/unit/wait_for_element_mock.ts +104 -0
  46. package/tsconfig.json +1 -1
  47. package/smoke-test.js +0 -102
  48. package/test/run-real-test.js +0 -24
  49. package/test/wait_for_element_mock.js +0 -113
  50. package/test/wait_for_element_real.js +0 -67
  51. package/test-ui-tree.js +0 -68
package/README.md CHANGED
@@ -1,473 +1,48 @@
1
1
  # Mobile Debug MCP
2
2
 
3
- **Mobile Debug MCP** is a minimal, secure MCP server for AI-assisted mobile development. It allows you to **launch Android or iOS apps**, **read their logs**, and **inspect UI** from an MCP-compatible AI client.
3
+ A minimal, secure MCP server for AI-assisted mobile development. Build, install, and inspect Android/iOS apps from an MCP-compatible client.
4
4
 
5
- This server is designed with security in mind, using strict argument handling to prevent shell injection, and reliability, with robust process management to avoid hanging operations.
5
+ This README was shortened to keep high-level info only. Detailed tool definitions moved to docs/TOOLS.md.
6
6
 
7
- > **Note:** iOS support is currently an untested Work In Progress (WIP). Please use with caution and report any issues.
8
-
9
- ---
10
-
11
- ## Features
12
-
13
- - Launch Android apps via package name.
14
- - Launch iOS apps via bundle ID on a booted simulator.
15
- - Fetch recent logs from Android or iOS apps.
16
- - Terminate and restart apps.
17
- - Clear app data for fresh installs.
18
- - Capture screenshots.
19
- - Cross-platform support (Android + iOS).
20
- - Minimal, focused design for fast debugging loops.
21
-
22
- ---
23
-
24
- ## Requirements
25
-
26
- - Node.js >= 18
27
- - Android SDK (`adb` in PATH) for Android support
28
- - Xcode command-line tools (`xcrun simctl`) for iOS support
29
- - **iOS Device Bridge (`idb`)** for iOS UI tree support
30
- - Booted iOS simulator for iOS testing
31
-
32
- ---
33
-
34
- ## Installation
35
-
36
- You can install and use **Mobile Debug MCP** in one of two ways:
37
-
38
- ### 1. Install Dependencies
39
-
40
- **iOS Prerequisite (`idb`):**
41
- To use the `get_ui_tree` tool on iOS, you must install Facebook's `idb`:
42
-
43
- ```bash
44
- brew tap facebook/fb
45
- brew install idb-companion
46
- pip3 install fb-idb
47
- ```
48
-
49
- ### 2. Clone the repository for local development
7
+ ## Quick start
50
8
 
51
9
  ```bash
52
10
  git clone https://github.com/YOUR_USERNAME/mobile-debug-mcp.git
53
11
  cd mobile-debug-mcp
54
12
  npm install
55
13
  npm run build
14
+ npm start
56
15
  ```
57
16
 
58
- This option is suitable if you want to modify or contribute to the code.
59
-
60
- ### 3. Install via npm for standard use
61
-
62
- ```bash
63
- npm install -g mobile-debug-mcp
64
- ```
65
-
66
- This option installs the package globally for easy use without cloning the repo.
67
-
68
- ---
17
+ ## Requirements
69
18
 
70
- ## MCP Server Configuration
19
+ - Node.js >= 18
20
+ - Android SDK (adb) for Android support
21
+ - Xcode command-line tools for iOS support
22
+ - Optional: idb for enhanced iOS device support
71
23
 
72
- Example WebUI MCP config using `npx --yes` and environment variables:
24
+ ## Configuration example
73
25
 
74
26
  ```json
75
27
  {
76
28
  "mcpServers": {
77
29
  "mobile-debug": {
78
30
  "command": "npx",
79
- "args": [
80
- "--yes",
81
- "mobile-debug-mcp",
82
- "server"
83
- ],
84
- "env": {
85
- "ADB_PATH": "/path/to/adb",
86
- "XCRUN_PATH": "/usr/bin/xcrun"
87
- }
31
+ "args": ["--yes","mobile-debug-mcp","server"],
32
+ "env": { "ADB_PATH": "/path/to/adb", "XCRUN_PATH": "/usr/bin/xcrun" }
88
33
  }
89
34
  }
90
35
  }
91
36
  ```
92
37
 
93
- > Make sure to set `ADB_PATH` (Android) and `XCRUN_PATH` (iOS) if the tools are not in your system PATH.
94
-
95
- ---
96
-
97
- ## Tools
98
-
99
- All tools accept a JSON input payload and return a structured JSON response. **Every response includes a `device` object** (with information about the selected device/simulator used for the operation), plus the tool-specific output.
100
-
101
- ### start_app
102
- Launch a mobile app.
103
-
104
- **Input:**
105
- ```jsonc
106
- {
107
- "platform": "android" | "ios",
108
- "appId": "com.example.app", // Android package or iOS bundle ID (Required)
109
- "deviceId": "emulator-5554" // Optional: target specific device/simulator
110
- }
111
- ```
112
-
113
- **Response:**
114
- ```json
115
- {
116
- "device": { /* device info */ },
117
- "appStarted": true,
118
- "launchTimeMs": 123
119
- }
120
- ```
121
-
122
- ### get_logs
123
- Fetch recent logs from the app or device.
124
-
125
- **Input:**
126
- ```jsonc
127
- {
128
- "platform": "android" | "ios",
129
- "appId": "com.example.app", // Optional: filter logs by app
130
- "deviceId": "emulator-5554", // Optional: target specific device
131
- "lines": 200 // Optional: number of lines (Android only)
132
- }
133
- ```
134
-
135
- **Response:**
136
- Returns two content blocks:
137
- 1. JSON metadata:
138
- ```json
139
- {
140
- "device": { /* device info */ },
141
- "result": { "lines": 50, "crashLines": [...] }
142
- }
143
- ```
144
- 2. Plain text log output.
145
-
146
- ### capture_screenshot
147
- Capture a screenshot of the current device screen.
148
-
149
- **Input:**
150
- ```jsonc
151
- {
152
- "platform": "android" | "ios",
153
- "deviceId": "emulator-5554" // Optional: target specific device
154
- }
155
- ```
156
-
157
- **Response:**
158
- Returns two content blocks:
159
- 1. JSON metadata:
160
- ```json
161
- {
162
- "device": { /* device info */ },
163
- "result": { "resolution": { "width": 1080, "height": 1920 } }
164
- }
165
- ```
166
- 2. Image content (image/png) containing the raw PNG data.
167
-
168
- ### terminate_app
169
- Terminate a running app.
170
-
171
- **Input:**
172
- ```jsonc
173
- {
174
- "platform": "android" | "ios",
175
- "appId": "com.example.app", // Android package or iOS bundle ID (Required)
176
- "deviceId": "emulator-5554" // Optional
177
- }
178
- ```
179
-
180
- **Response:**
181
- ```json
182
- {
183
- "device": { /* device info */ },
184
- "appTerminated": true
185
- }
186
- ```
187
-
188
- ### restart_app
189
- Restart an app (terminate then launch).
190
-
191
- **Input:**
192
- ```jsonc
193
- {
194
- "platform": "android" | "ios",
195
- "appId": "com.example.app", // Android package or iOS bundle ID (Required)
196
- "deviceId": "emulator-5554" // Optional
197
- }
198
- ```
199
-
200
- **Response:**
201
- ```json
202
- {
203
- "device": { /* device info */ },
204
- "appRestarted": true,
205
- "launchTimeMs": 123
206
- }
207
- ```
208
-
209
- ### reset_app_data
210
- Clear app storage (reset to fresh install state).
211
-
212
- **Input:**
213
- ```jsonc
214
- {
215
- "platform": "android" | "ios",
216
- "appId": "com.example.app", // Android package or iOS bundle ID (Required)
217
- "deviceId": "emulator-5554" // Optional
218
- }
219
- ```
220
-
221
- **Response:**
222
- ```json
223
- {
224
- "device": { /* device info */ },
225
- "dataCleared": true
226
- }
227
- ```
228
-
229
- ### get_ui_tree
230
- Get the current UI hierarchy from the device. Returns a structured JSON representation of the screen content.
231
-
232
- **Input:**
233
- ```jsonc
234
- {
235
- "platform": "android" | "ios",
236
- "deviceId": "emulator-5554" // Optional
237
- }
238
- ```
239
-
240
- **Response:**
241
- ```json
242
- {
243
- "device": { /* device info */ },
244
- "screen": "",
245
- "resolution": { "width": 1080, "height": 1920 },
246
- "elements": [
247
- {
248
- "text": "Login",
249
- "contentDescription": null,
250
- "type": "android.widget.Button",
251
- "resourceId": "com.example:id/login_button",
252
- "clickable": true,
253
- "enabled": true,
254
- "visible": true,
255
- "bounds": [120,400,280,450],
256
- "center": [200, 425],
257
- "depth": 1,
258
- "parentId": 0,
259
- "children": []
260
- }
261
- ]
262
- }
263
- ```
264
-
265
- ### get_current_screen
266
- Get the currently visible activity on an Android device.
267
-
268
- **Input:**
269
- ```jsonc
270
- {
271
- "deviceId": "emulator-5554" // Optional: target specific device
272
- }
273
- ```
274
-
275
- **Response:**
276
- ```json
277
- {
278
- "device": { /* device info */ },
279
- "package": "com.example.app",
280
- "activity": "com.example.app.LoginActivity",
281
- "shortActivity": "LoginActivity"
282
- }
283
- ```
284
-
285
- ### wait_for_element
286
- Wait until a UI element with matching text appears on screen or timeout is reached. Useful for handling loading states or transitions.
287
-
288
- **Input:**
289
- ```jsonc
290
- {
291
- "platform": "android" | "ios",
292
- "text": "Home", // Text to wait for
293
- "timeout": 5000, // Max wait time in ms (default 10000)
294
- "deviceId": "emulator-5554" // Optional
295
- }
296
- ```
297
-
298
- **Response:**
299
- ```json
300
- {
301
- "device": { /* device info */ },
302
- "found": true,
303
- "element": { /* UIElement object if found */ }
304
- }
305
- ```
306
-
307
- If the element is not found within the timeout, `found` will be `false`. If a system error occurs (e.g., ADB failure), an `error` field will be present.
308
-
309
- ```json
310
- {
311
- "device": { /* device info */ },
312
- "found": false,
313
- "error": "Optional error message"
314
- }
315
- ```
316
-
317
- ### tap
318
- Simulate a finger tap on the device screen at specific coordinates.
319
-
320
- Platform support and constraints:
321
- - Android: Implemented via `adb shell input tap` and works when `adb` is available in PATH or configured via `ADB_PATH`.
322
- - iOS: Requires Facebook's `idb` tooling. The iOS implementation uses `idb` to deliver UI events and is simulator-oriented (works reliably on a booted simulator). Physical device support depends on `idb` capabilities and a running `idb_companion` on the target device; it may not work in all environments.
323
-
324
- Prerequisites for iOS (if you intend to use tap on iOS):
325
- ```bash
326
- brew tap facebook/fb
327
- brew install idb-companion
328
- pip3 install fb-idb
329
- ```
330
- Ensure `idb` and `idb_companion` are in your PATH. If you use non-standard tool locations, set `XCRUN_PATH` and/or `ADB_PATH` environment variables as appropriate.
331
-
332
- Behavior notes:
333
- - The tool is a primitive input: it only sends a tap at the provided coordinates. It does not inspect or interpret the UI.
334
- - If `idb` is missing or the simulator/device is not available, the tool will return an error explaining the failure.
335
-
336
- **Input:**
337
- ```jsonc
338
- {
339
- "platform": "android" | "ios", // Optional, defaults to "android"
340
- "x": 200, // X coordinate (Required)
341
- "y": 400, // Y coordinate (Required)
342
- "deviceId": "emulator-5554" // Optional
343
- }
344
- ```
345
-
346
- **Response:**
347
- ```json
348
- {
349
- "device": { /* device info */ },
350
- "success": true,
351
- "x": 200,
352
- "y": 400
353
- }
354
- ```
355
-
356
- If the tap fails (e.g., missing `adb`/`idb`, device not found), `success` will be `false` and an `error` field will be present.
357
-
358
- ### swipe
359
- Simulate a swipe gesture on an Android device.
360
-
361
- **Input:**
362
- ```jsonc
363
- {
364
- "platform": "android", // Optional, defaults to "android"
365
- "x1": 500, // Start X (Required)
366
- "y1": 1500, // Start Y (Required)
367
- "x2": 500, // End X (Required)
368
- "y2": 500, // End Y (Required)
369
- "duration": 300, // Duration in ms (Required)
370
- "deviceId": "emulator-5554" // Optional
371
- }
372
- ```
373
-
374
- **Response:**
375
- ```json
376
- {
377
- "device": { /* device info */ },
378
- "success": true,
379
- "start": [500, 1500],
380
- "end": [500, 500],
381
- "duration": 300
382
- }
383
- ```
384
-
385
- If the swipe fails, `success` will be `false` and an `error` field will be present.
386
-
387
- ### type_text
388
- Type text into the currently focused input field on an Android device.
389
-
390
- **Input:**
391
- ```jsonc
392
- {
393
- "platform": "android", // Optional, defaults to "android"
394
- "text": "hello world", // Text to type (Required)
395
- "deviceId": "emulator-5554" // Optional
396
- }
397
- ```
398
-
399
- **Response:**
400
- ```json
401
- {
402
- "device": { /* device info */ },
403
- "success": true,
404
- "text": "hello world"
405
- }
406
- ```
407
-
408
- If the command fails, `success` will be `false` and an `error` field will be present.
409
-
410
- ### press_back
411
- Simulate pressing the Android Back button.
412
-
413
- **Input:**
414
- ```jsonc
415
- {
416
- "platform": "android", // Optional
417
- "deviceId": "emulator-5554" // Optional
418
- }
419
- ```
420
-
421
- **Response:**
422
- ```json
423
- {
424
- "device": { /* device info */ },
425
- "success": true
426
- }
427
- ```
428
-
429
- ---
430
-
431
- ## Recommended Workflow
432
-
433
- 1. Ensure Android device or iOS simulator is running.
434
- 2. Use `start_app` to launch the app.
435
- 3. Use `get_logs` to read the latest logs.
436
- 4. Use `capture_screenshot` to visually inspect the app if needed.
437
- 5. Use `wait_for_element` to ensure the app is in the expected state before proceeding (e.g., after login).
438
- 6. Use `reset_app_data` to clear state if debugging fresh install scenarios.
439
- 7. Use `restart_app` to quickly reboot the app during development cycles.
440
-
441
- ---
442
-
443
- ## Notes
444
-
445
- - Ensure `adb` and `xcrun` are in your PATH or set `ADB_PATH` / `XCRUN_PATH` accordingly.
446
- - For iOS, the simulator must be booted before using tools.
447
- - The `capture_screenshot` tool returns a multi-block response: a JSON text block with metadata, followed by an image block containing the base64-encoded PNG data.
448
-
449
- ---
450
-
451
- ## Testing
452
-
453
- The repository includes a smoke test script to verify end-to-end functionality on real devices or simulators.
454
-
455
- ```bash
456
- # Run smoke test for Android
457
- npx tsx smoke-test.ts android com.example.package
458
-
459
- # Run smoke test for iOS
460
- npx tsx smoke-test.ts ios com.example.bundleid
461
- ```
38
+ > Note: Avoid using `jsonc` fences with inline comments in README code blocks to prevent syntax-highlighting issues on some renderers.
462
39
 
463
- The smoke test performs the following sequence:
464
- 1. Starts the app
465
- 2. Captures a screenshot
466
- 3. Fetches logs
467
- 4. Terminates the app
40
+ ## Docs
468
41
 
469
- ---
42
+ - Tools: docs/TOOLS.md (full input/response examples)
43
+ - Changelog: docs/CHANGELOG.md
44
+ - Tests: test/
470
45
 
471
46
  ## License
472
47
 
473
- MIT License
48
+ MIT
@@ -1,5 +1,9 @@
1
- import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "./utils.js";
1
+ import { execAdb, getAndroidDeviceMetadata, getDeviceInfo, detectJavaHome } from "./utils.js";
2
2
  import { AndroidObserve } from "./observe.js";
3
+ import { promises as fs } from "fs";
4
+ import { spawn } from "child_process";
5
+ import path from "path";
6
+ import { existsSync } from "fs";
3
7
  export class AndroidInteract {
4
8
  observe = new AndroidObserve();
5
9
  async waitForElement(text, timeout, deviceId) {
@@ -76,6 +80,97 @@ export class AndroidInteract {
76
80
  return { device: deviceInfo, success: false, error: e instanceof Error ? e.message : String(e) };
77
81
  }
78
82
  }
83
+ async installApp(apkPath, deviceId) {
84
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
85
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
86
+ // Helper to recursively find first APK under a directory
87
+ async function findApk(dir) {
88
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
89
+ for (const e of entries) {
90
+ const full = path.join(dir, e.name);
91
+ if (e.isDirectory()) {
92
+ const found = await findApk(full);
93
+ if (found)
94
+ return found;
95
+ }
96
+ else if (e.isFile() && full.endsWith('.apk')) {
97
+ return full;
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+ try {
103
+ let apkToInstall = apkPath;
104
+ // If a directory is provided, attempt to build via Gradle
105
+ const stat = await fs.stat(apkPath).catch(() => null);
106
+ if (stat && stat.isDirectory()) {
107
+ const gradlewPath = path.join(apkPath, 'gradlew');
108
+ const gradleCmd = existsSync(gradlewPath) ? './gradlew' : 'gradle';
109
+ await new Promise(async (resolve, reject) => {
110
+ // Auto-detect and set JAVA_HOME (prefer JDK 17) so builds don't require manual environment setup
111
+ const detectedJavaHome = await detectJavaHome().catch(() => undefined);
112
+ const env = Object.assign({}, process.env);
113
+ if (detectedJavaHome) {
114
+ // Override existing JAVA_HOME if detection found a preferably compatible JDK (e.g., JDK 17).
115
+ if (env.JAVA_HOME !== detectedJavaHome) {
116
+ env.JAVA_HOME = detectedJavaHome;
117
+ // Also ensure the JDK bin is on PATH so tools like jlink/javac are resolved from the detected JDK
118
+ env.PATH = `${path.join(detectedJavaHome, 'bin')}${path.delimiter}${env.PATH || ''}`;
119
+ console.debug('[android] Overriding JAVA_HOME with detected path:', detectedJavaHome);
120
+ }
121
+ }
122
+ // Sanitize environment so user shell init scripts are less likely to override our JAVA_HOME.
123
+ try {
124
+ // Remove obvious shell profile hints; avoid touching SDKMAN symlinks or on-disk state.
125
+ delete env.SHELL;
126
+ }
127
+ catch (e) { }
128
+ // If we detected a compatible JDK, instruct Gradle to use it and avoid daemon reuse
129
+ // Prepare gradle invocation
130
+ const gradleArgs = ['assembleDebug'];
131
+ if (detectedJavaHome) {
132
+ gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`);
133
+ gradleArgs.push('--no-daemon');
134
+ env.GRADLE_JAVA_HOME = detectedJavaHome;
135
+ }
136
+ // Prefer invoking the wrapper directly without a shell to avoid user profile shims (sdkman) re-setting JAVA_HOME
137
+ const wrapperPath = path.join(apkPath, 'gradlew');
138
+ const useWrapper = existsSync(wrapperPath);
139
+ const execCmd = useWrapper ? wrapperPath : gradleCmd;
140
+ const spawnOpts = { cwd: apkPath, env };
141
+ // When using wrapper, ensure it's executable and invoke directly (no shell)
142
+ if (useWrapper) {
143
+ // Ensure the wrapper is executable; swallow errors from chmod (best-effort).
144
+ await fs.chmod(wrapperPath, 0o755).catch(() => { });
145
+ spawnOpts.shell = false;
146
+ }
147
+ else {
148
+ // if using system 'gradle' allow shell to resolve platform PATH
149
+ spawnOpts.shell = true;
150
+ }
151
+ const proc = spawn(execCmd, gradleArgs, spawnOpts);
152
+ let stderr = '';
153
+ proc.stderr?.on('data', d => stderr += d.toString());
154
+ proc.on('close', code => {
155
+ if (code === 0)
156
+ resolve();
157
+ else
158
+ reject(new Error(stderr || `Gradle build failed with code ${code}`));
159
+ });
160
+ proc.on('error', err => reject(err));
161
+ });
162
+ const built = await findApk(apkPath);
163
+ if (!built)
164
+ throw new Error('Could not locate built APK after running Gradle');
165
+ apkToInstall = built;
166
+ }
167
+ const output = await execAdb(['install', '-r', apkToInstall], deviceId);
168
+ return { device: deviceInfo, installed: true, output };
169
+ }
170
+ catch (e) {
171
+ return { device: deviceInfo, installed: false, error: e instanceof Error ? e.message : String(e) };
172
+ }
173
+ }
79
174
  async startApp(appId, deviceId) {
80
175
  const metadata = await getAndroidDeviceMetadata(appId, deviceId);
81
176
  const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);