mobile-debug-mcp 0.5.0 → 0.7.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 CHANGED
@@ -26,6 +26,7 @@ This server is designed with security in mind, using strict argument handling to
26
26
  - Node.js >= 18
27
27
  - Android SDK (`adb` in PATH) for Android support
28
28
  - Xcode command-line tools (`xcrun simctl`) for iOS support
29
+ - **iOS Device Bridge (`idb`)** for iOS UI tree support
29
30
  - Booted iOS simulator for iOS testing
30
31
 
31
32
  ---
@@ -34,7 +35,18 @@ This server is designed with security in mind, using strict argument handling to
34
35
 
35
36
  You can install and use **Mobile Debug MCP** in one of two ways:
36
37
 
37
- ### 1. Clone the repository for local development
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
38
50
 
39
51
  ```bash
40
52
  git clone https://github.com/YOUR_USERNAME/mobile-debug-mcp.git
@@ -45,7 +57,7 @@ npm run build
45
57
 
46
58
  This option is suitable if you want to modify or contribute to the code.
47
59
 
48
- ### 2. Install via npm for standard use
60
+ ### 3. Install via npm for standard use
49
61
 
50
62
  ```bash
51
63
  npm install -g mobile-debug-mcp
@@ -55,27 +67,7 @@ This option installs the package globally for easy use without cloning the repo.
55
67
 
56
68
  ---
57
69
 
58
- ## Testing
59
- 33.
60
- 34. The repository includes a smoke test script to verify end-to-end functionality on real devices or simulators.
61
- 35.
62
- 36. ```bash
63
- 37. # Run smoke test for Android
64
- 38. npx tsx smoke-test.ts android com.example.package
65
- 39.
66
- 40. # Run smoke test for iOS
67
- 41. npx tsx smoke-test.ts ios com.example.bundleid
68
- 42. ```
69
- 43.
70
- 44. The smoke test performs the following sequence:
71
- 45. 1. Starts the app
72
- 46. 2. Captures a screenshot
73
- 47. 3. Fetches logs
74
- 48. 4. Terminates the app
75
- 49.
76
- 50. ---
77
- 51.
78
- 52. ## MCP Server Configuration
70
+ ## MCP Server Configuration
79
71
 
80
72
  Example WebUI MCP config using `npx --yes` and environment variables:
81
73
 
@@ -110,7 +102,7 @@ All tools accept a JSON input payload and return a structured JSON response. **E
110
102
  Launch a mobile app.
111
103
 
112
104
  **Input:**
113
- ```json
105
+ ```jsonc
114
106
  {
115
107
  "platform": "android" | "ios",
116
108
  "appId": "com.example.app", // Android package or iOS bundle ID (Required)
@@ -131,7 +123,7 @@ Launch a mobile app.
131
123
  Fetch recent logs from the app or device.
132
124
 
133
125
  **Input:**
134
- ```json
126
+ ```jsonc
135
127
  {
136
128
  "platform": "android" | "ios",
137
129
  "appId": "com.example.app", // Optional: filter logs by app
@@ -155,7 +147,7 @@ Returns two content blocks:
155
147
  Capture a screenshot of the current device screen.
156
148
 
157
149
  **Input:**
158
- ```json
150
+ ```jsonc
159
151
  {
160
152
  "platform": "android" | "ios",
161
153
  "deviceId": "emulator-5554" // Optional: target specific device
@@ -177,7 +169,7 @@ Returns two content blocks:
177
169
  Terminate a running app.
178
170
 
179
171
  **Input:**
180
- ```json
172
+ ```jsonc
181
173
  {
182
174
  "platform": "android" | "ios",
183
175
  "appId": "com.example.app", // Android package or iOS bundle ID (Required)
@@ -197,7 +189,7 @@ Terminate a running app.
197
189
  Restart an app (terminate then launch).
198
190
 
199
191
  **Input:**
200
- ```json
192
+ ```jsonc
201
193
  {
202
194
  "platform": "android" | "ios",
203
195
  "appId": "com.example.app", // Android package or iOS bundle ID (Required)
@@ -218,7 +210,7 @@ Restart an app (terminate then launch).
218
210
  Clear app storage (reset to fresh install state).
219
211
 
220
212
  **Input:**
221
- ```json
213
+ ```jsonc
222
214
  {
223
215
  "platform": "android" | "ios",
224
216
  "appId": "com.example.app", // Android package or iOS bundle ID (Required)
@@ -234,6 +226,206 @@ Clear app storage (reset to fresh install state).
234
226
  }
235
227
  ```
236
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
+
237
429
  ---
238
430
 
239
431
  ## Recommended Workflow
@@ -242,8 +434,9 @@ Clear app storage (reset to fresh install state).
242
434
  2. Use `start_app` to launch the app.
243
435
  3. Use `get_logs` to read the latest logs.
244
436
  4. Use `capture_screenshot` to visually inspect the app if needed.
245
- 5. Use `reset_app_data` to clear state if debugging fresh install scenarios.
246
- 6. Use `restart_app` to quickly reboot the app during development cycles.
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.
247
440
 
248
441
  ---
249
442
 
@@ -255,6 +448,26 @@ Clear app storage (reset to fresh install state).
255
448
 
256
449
  ---
257
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
+ ```
462
+
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
468
+
469
+ ---
470
+
258
471
  ## License
259
472
 
260
473
  MIT License
@@ -0,0 +1,106 @@
1
+ import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "./utils.js";
2
+ import { AndroidObserve } from "./observe.js";
3
+ export class AndroidInteract {
4
+ observe = new AndroidObserve();
5
+ async waitForElement(text, timeout, deviceId) {
6
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
7
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
8
+ const startTime = Date.now();
9
+ while (Date.now() - startTime < timeout) {
10
+ try {
11
+ const tree = await this.observe.getUITree(deviceId);
12
+ if (tree.error) {
13
+ return { device: deviceInfo, found: false, error: tree.error };
14
+ }
15
+ const element = tree.elements.find(e => e.text === text);
16
+ if (element) {
17
+ return { device: deviceInfo, found: true, element };
18
+ }
19
+ }
20
+ catch (e) {
21
+ // Ignore errors during polling and retry
22
+ console.error("Error polling UI tree:", e);
23
+ }
24
+ const elapsed = Date.now() - startTime;
25
+ const remaining = timeout - elapsed;
26
+ if (remaining <= 0)
27
+ break;
28
+ await new Promise(resolve => setTimeout(resolve, Math.min(500, remaining)));
29
+ }
30
+ return { device: deviceInfo, found: false };
31
+ }
32
+ async tap(x, y, deviceId) {
33
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
34
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
35
+ try {
36
+ await execAdb(['shell', 'input', 'tap', x.toString(), y.toString()], deviceId);
37
+ return { device: deviceInfo, success: true, x, y };
38
+ }
39
+ catch (e) {
40
+ return { device: deviceInfo, success: false, x, y, error: e instanceof Error ? e.message : String(e) };
41
+ }
42
+ }
43
+ async swipe(x1, y1, x2, y2, duration, deviceId) {
44
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
45
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
46
+ try {
47
+ await execAdb(['shell', 'input', 'swipe', x1.toString(), y1.toString(), x2.toString(), y2.toString(), duration.toString()], deviceId);
48
+ return { device: deviceInfo, success: true, start: [x1, y1], end: [x2, y2], duration };
49
+ }
50
+ catch (e) {
51
+ return { device: deviceInfo, success: false, start: [x1, y1], end: [x2, y2], duration, error: e instanceof Error ? e.message : String(e) };
52
+ }
53
+ }
54
+ async typeText(text, deviceId) {
55
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
56
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
57
+ try {
58
+ // Encode spaces as %s to ensure proper input handling by adb shell input text
59
+ const encodedText = text.replace(/\s/g, '%s');
60
+ // Note: 'input text' might fail with some characters or if keyboard isn't ready, but it's the standard ADB way.
61
+ await execAdb(['shell', 'input', 'text', encodedText], deviceId);
62
+ return { device: deviceInfo, success: true, text };
63
+ }
64
+ catch (e) {
65
+ return { device: deviceInfo, success: false, text, error: e instanceof Error ? e.message : String(e) };
66
+ }
67
+ }
68
+ async pressBack(deviceId) {
69
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
70
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
71
+ try {
72
+ await execAdb(['shell', 'input', 'keyevent', '4'], deviceId);
73
+ return { device: deviceInfo, success: true };
74
+ }
75
+ catch (e) {
76
+ return { device: deviceInfo, success: false, error: e instanceof Error ? e.message : String(e) };
77
+ }
78
+ }
79
+ async startApp(appId, deviceId) {
80
+ const metadata = await getAndroidDeviceMetadata(appId, deviceId);
81
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
82
+ await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
83
+ return { device: deviceInfo, appStarted: true, launchTimeMs: 1000 };
84
+ }
85
+ async terminateApp(appId, deviceId) {
86
+ const metadata = await getAndroidDeviceMetadata(appId, deviceId);
87
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
88
+ await execAdb(['shell', 'am', 'force-stop', appId], deviceId);
89
+ return { device: deviceInfo, appTerminated: true };
90
+ }
91
+ async restartApp(appId, deviceId) {
92
+ await this.terminateApp(appId, deviceId);
93
+ const startResult = await this.startApp(appId, deviceId);
94
+ return {
95
+ device: startResult.device,
96
+ appRestarted: startResult.appStarted,
97
+ launchTimeMs: startResult.launchTimeMs
98
+ };
99
+ }
100
+ async resetAppData(appId, deviceId) {
101
+ const metadata = await getAndroidDeviceMetadata(appId, deviceId);
102
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
103
+ const output = await execAdb(['shell', 'pm', 'clear', appId], deviceId);
104
+ return { device: deviceInfo, dataCleared: output === 'Success' };
105
+ }
106
+ }