mobile-debug-mcp 0.6.0 → 0.8.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 +230 -2
- package/dist/android/interact.js +87 -0
- package/dist/android/utils.js +311 -4
- package/dist/ios/interact.js +109 -1
- package/dist/ios/utils.js +154 -0
- package/dist/resolve-device.js +70 -0
- package/dist/server.js +367 -20
- package/docs/CHANGELOG.md +17 -2
- package/package.json +6 -2
- package/src/android/interact.ts +98 -1
- package/src/android/utils.ts +314 -4
- package/src/ios/interact.ts +116 -2
- package/src/ios/utils.ts +157 -0
- package/src/resolve-device.ts +80 -0
- package/src/server.ts +425 -21
- package/src/types.ts +44 -0
- package/test/integration/index.ts +8 -0
- package/test/integration/logstream-real.ts +35 -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-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 +6 -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-ui-tree.js +0 -68
package/dist/server.js
CHANGED
|
@@ -6,13 +6,16 @@ import { AndroidObserve } from "./android/observe.js";
|
|
|
6
6
|
import { AndroidInteract } from "./android/interact.js";
|
|
7
7
|
import { iOSObserve } from "./ios/observe.js";
|
|
8
8
|
import { iOSInteract } from "./ios/interact.js";
|
|
9
|
+
import { resolveTargetDevice, listDevices } from "./resolve-device.js";
|
|
10
|
+
import { startAndroidLogStream, readLogStreamLines, stopAndroidLogStream } from "./android/utils.js";
|
|
11
|
+
import { startIOSLogStream, readIOSLogStreamLines, stopIOSLogStream } from "./ios/utils.js";
|
|
9
12
|
const androidObserve = new AndroidObserve();
|
|
10
13
|
const androidInteract = new AndroidInteract();
|
|
11
14
|
const iosObserve = new iOSObserve();
|
|
12
15
|
const iosInteract = new iOSInteract();
|
|
13
16
|
const server = new Server({
|
|
14
17
|
name: "mobile-debug-mcp",
|
|
15
|
-
version: "0.
|
|
18
|
+
version: "0.7.0"
|
|
16
19
|
}, {
|
|
17
20
|
capabilities: {
|
|
18
21
|
tools: {}
|
|
@@ -116,6 +119,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
116
119
|
required: ["platform", "appId"]
|
|
117
120
|
}
|
|
118
121
|
},
|
|
122
|
+
{
|
|
123
|
+
name: "install_app",
|
|
124
|
+
description: "Install an app on Android (apk) or iOS simulator/device (ipa/.app).",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
platform: { type: "string", enum: ["android", "ios"] },
|
|
129
|
+
appPath: { type: "string", description: "Path to APK (Android) or .app/.ipa (iOS) on the host machine" },
|
|
130
|
+
deviceId: { type: "string", description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected." }
|
|
131
|
+
},
|
|
132
|
+
required: ["platform", "appPath"]
|
|
133
|
+
}
|
|
134
|
+
},
|
|
119
135
|
{
|
|
120
136
|
name: "get_logs",
|
|
121
137
|
description: "Get recent logs from Android or iOS simulator. Returns device metadata and the log output.",
|
|
@@ -142,6 +158,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
142
158
|
required: ["platform"]
|
|
143
159
|
}
|
|
144
160
|
},
|
|
161
|
+
{
|
|
162
|
+
name: "list_devices",
|
|
163
|
+
description: "List connected devices and their metadata (android + ios).",
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
platform: { type: "string", enum: ["android", "ios"] }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
145
171
|
{
|
|
146
172
|
name: "capture_screenshot",
|
|
147
173
|
description: "Capture a screenshot from an Android device or iOS simulator. Returns device metadata and the screenshot image.",
|
|
@@ -160,6 +186,41 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
160
186
|
required: ["platform"]
|
|
161
187
|
}
|
|
162
188
|
},
|
|
189
|
+
{
|
|
190
|
+
name: "start_log_stream",
|
|
191
|
+
description: "Start streaming logs for a target application on Android or iOS. For Android this uses adb logcat --pid=<pid>; for iOS it streams `xcrun simctl spawn <device> log stream` with a predicate.",
|
|
192
|
+
inputSchema: {
|
|
193
|
+
type: "object",
|
|
194
|
+
properties: {
|
|
195
|
+
platform: { type: "string", enum: ["android", "ios"], default: "android" },
|
|
196
|
+
packageName: { type: "string", description: "Android package name or iOS bundle id" },
|
|
197
|
+
level: { type: "string", enum: ["error", "warn", "info", "debug"], default: "error" },
|
|
198
|
+
deviceId: { type: "string", description: "Device Serial (Android) or UDID (iOS). Defaults to connected/booted device." },
|
|
199
|
+
sessionId: { type: "string", description: "Session identifier for the log stream" }
|
|
200
|
+
},
|
|
201
|
+
required: ["packageName"]
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "read_log_stream",
|
|
206
|
+
description: "Read accumulated log stream entries for the active session.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {
|
|
210
|
+
sessionId: { type: "string" }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "stop_log_stream",
|
|
216
|
+
description: "Stop an active log stream for the session.",
|
|
217
|
+
inputSchema: {
|
|
218
|
+
type: "object",
|
|
219
|
+
properties: {
|
|
220
|
+
sessionId: { type: "string" }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
},
|
|
163
224
|
{
|
|
164
225
|
name: "get_ui_tree",
|
|
165
226
|
description: "Get the current UI hierarchy from an Android device or iOS simulator. Returns a structured JSON representation of the screen content.",
|
|
@@ -191,6 +252,126 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
191
252
|
}
|
|
192
253
|
}
|
|
193
254
|
}
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "wait_for_element",
|
|
258
|
+
description: "Wait until a UI element with matching text appears on screen or timeout is reached.",
|
|
259
|
+
inputSchema: {
|
|
260
|
+
type: "object",
|
|
261
|
+
properties: {
|
|
262
|
+
platform: {
|
|
263
|
+
type: "string",
|
|
264
|
+
enum: ["android", "ios"],
|
|
265
|
+
description: "Platform to check"
|
|
266
|
+
},
|
|
267
|
+
text: {
|
|
268
|
+
type: "string",
|
|
269
|
+
description: "Text content of the element to wait for"
|
|
270
|
+
},
|
|
271
|
+
timeout: {
|
|
272
|
+
type: "number",
|
|
273
|
+
description: "Max wait time in ms (default 10000)",
|
|
274
|
+
default: 10000
|
|
275
|
+
},
|
|
276
|
+
deviceId: {
|
|
277
|
+
type: "string",
|
|
278
|
+
description: "Device Serial/UDID. Defaults to connected/booted device."
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
required: ["platform", "text"]
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: "tap",
|
|
286
|
+
description: "Simulate a finger tap on the device screen at specific coordinates.",
|
|
287
|
+
inputSchema: {
|
|
288
|
+
type: "object",
|
|
289
|
+
properties: {
|
|
290
|
+
platform: {
|
|
291
|
+
type: "string",
|
|
292
|
+
enum: ["android", "ios"],
|
|
293
|
+
description: "Platform to tap on"
|
|
294
|
+
},
|
|
295
|
+
x: {
|
|
296
|
+
type: "number",
|
|
297
|
+
description: "X coordinate"
|
|
298
|
+
},
|
|
299
|
+
y: {
|
|
300
|
+
type: "number",
|
|
301
|
+
description: "Y coordinate"
|
|
302
|
+
},
|
|
303
|
+
deviceId: {
|
|
304
|
+
type: "string",
|
|
305
|
+
description: "Device Serial/UDID. Defaults to connected/booted device."
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
required: ["x", "y"]
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: "swipe",
|
|
313
|
+
description: "Simulate a swipe gesture on an Android device.",
|
|
314
|
+
inputSchema: {
|
|
315
|
+
type: "object",
|
|
316
|
+
properties: {
|
|
317
|
+
platform: {
|
|
318
|
+
type: "string",
|
|
319
|
+
enum: ["android"],
|
|
320
|
+
description: "Platform to swipe on (currently only android supported)"
|
|
321
|
+
},
|
|
322
|
+
x1: { type: "number", description: "Start X coordinate" },
|
|
323
|
+
y1: { type: "number", description: "Start Y coordinate" },
|
|
324
|
+
x2: { type: "number", description: "End X coordinate" },
|
|
325
|
+
y2: { type: "number", description: "End Y coordinate" },
|
|
326
|
+
duration: { type: "number", description: "Duration in ms" },
|
|
327
|
+
deviceId: {
|
|
328
|
+
type: "string",
|
|
329
|
+
description: "Device Serial/UDID. Defaults to connected/booted device."
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
required: ["x1", "y1", "x2", "y2", "duration"]
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "type_text",
|
|
337
|
+
description: "Type text into the currently focused input field on an Android device.",
|
|
338
|
+
inputSchema: {
|
|
339
|
+
type: "object",
|
|
340
|
+
properties: {
|
|
341
|
+
platform: {
|
|
342
|
+
type: "string",
|
|
343
|
+
enum: ["android"],
|
|
344
|
+
description: "Platform to type on (currently only android supported)"
|
|
345
|
+
},
|
|
346
|
+
text: {
|
|
347
|
+
type: "string",
|
|
348
|
+
description: "The text to type"
|
|
349
|
+
},
|
|
350
|
+
deviceId: {
|
|
351
|
+
type: "string",
|
|
352
|
+
description: "Device Serial/UDID. Defaults to connected/booted device."
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
required: ["text"]
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: "press_back",
|
|
360
|
+
description: "Simulate pressing the Android Back button.",
|
|
361
|
+
inputSchema: {
|
|
362
|
+
type: "object",
|
|
363
|
+
properties: {
|
|
364
|
+
platform: {
|
|
365
|
+
type: "string",
|
|
366
|
+
enum: ["android"],
|
|
367
|
+
description: "Platform (currently only android supported)"
|
|
368
|
+
},
|
|
369
|
+
deviceId: {
|
|
370
|
+
type: "string",
|
|
371
|
+
description: "Device Serial/UDID. Defaults to connected/booted device."
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
194
375
|
}
|
|
195
376
|
]
|
|
196
377
|
}));
|
|
@@ -203,13 +384,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
203
384
|
let launchTimeMs;
|
|
204
385
|
let deviceInfo;
|
|
205
386
|
if (platform === "android") {
|
|
206
|
-
const
|
|
387
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
388
|
+
const result = await androidInteract.startApp(appId, resolved.id);
|
|
207
389
|
appStarted = result.appStarted;
|
|
208
390
|
launchTimeMs = result.launchTimeMs;
|
|
209
391
|
deviceInfo = result.device;
|
|
210
392
|
}
|
|
211
393
|
else {
|
|
212
|
-
const
|
|
394
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
395
|
+
const result = await iosInteract.startApp(appId, resolved.id);
|
|
213
396
|
appStarted = result.appStarted;
|
|
214
397
|
launchTimeMs = result.launchTimeMs;
|
|
215
398
|
deviceInfo = result.device;
|
|
@@ -226,12 +409,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
226
409
|
let appTerminated;
|
|
227
410
|
let deviceInfo;
|
|
228
411
|
if (platform === "android") {
|
|
229
|
-
const
|
|
412
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
413
|
+
const result = await androidInteract.terminateApp(appId, resolved.id);
|
|
230
414
|
appTerminated = result.appTerminated;
|
|
231
415
|
deviceInfo = result.device;
|
|
232
416
|
}
|
|
233
417
|
else {
|
|
234
|
-
const
|
|
418
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
419
|
+
const result = await iosInteract.terminateApp(appId, resolved.id);
|
|
235
420
|
appTerminated = result.appTerminated;
|
|
236
421
|
deviceInfo = result.device;
|
|
237
422
|
}
|
|
@@ -247,13 +432,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
247
432
|
let launchTimeMs;
|
|
248
433
|
let deviceInfo;
|
|
249
434
|
if (platform === "android") {
|
|
250
|
-
const
|
|
435
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
436
|
+
const result = await androidInteract.restartApp(appId, resolved.id);
|
|
251
437
|
appRestarted = result.appRestarted;
|
|
252
438
|
launchTimeMs = result.launchTimeMs;
|
|
253
439
|
deviceInfo = result.device;
|
|
254
440
|
}
|
|
255
441
|
else {
|
|
256
|
-
const
|
|
442
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
443
|
+
const result = await iosInteract.restartApp(appId, resolved.id);
|
|
257
444
|
appRestarted = result.appRestarted;
|
|
258
445
|
launchTimeMs = result.launchTimeMs;
|
|
259
446
|
deviceInfo = result.device;
|
|
@@ -270,12 +457,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
270
457
|
let dataCleared;
|
|
271
458
|
let deviceInfo;
|
|
272
459
|
if (platform === "android") {
|
|
273
|
-
const
|
|
460
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
461
|
+
const result = await androidInteract.resetAppData(appId, resolved.id);
|
|
274
462
|
dataCleared = result.dataCleared;
|
|
275
463
|
deviceInfo = result.device;
|
|
276
464
|
}
|
|
277
465
|
else {
|
|
278
|
-
const
|
|
466
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
467
|
+
const result = await iosInteract.resetAppData(appId, resolved.id);
|
|
279
468
|
dataCleared = result.dataCleared;
|
|
280
469
|
deviceInfo = result.device;
|
|
281
470
|
}
|
|
@@ -285,18 +474,51 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
285
474
|
};
|
|
286
475
|
return wrapResponse(response);
|
|
287
476
|
}
|
|
477
|
+
if (name === "install_app") {
|
|
478
|
+
const { platform, appPath, deviceId } = args;
|
|
479
|
+
let installed;
|
|
480
|
+
let output;
|
|
481
|
+
let deviceInfo;
|
|
482
|
+
let errorMsg;
|
|
483
|
+
if (platform === "android") {
|
|
484
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
485
|
+
const result = await androidInteract.installApp(appPath, resolved.id);
|
|
486
|
+
installed = result.installed;
|
|
487
|
+
output = result.output;
|
|
488
|
+
deviceInfo = result.device;
|
|
489
|
+
errorMsg = result.error;
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
493
|
+
const result = await iosInteract.installApp(appPath, resolved.id);
|
|
494
|
+
installed = result.installed;
|
|
495
|
+
output = result.output;
|
|
496
|
+
deviceInfo = result.device;
|
|
497
|
+
errorMsg = result.error;
|
|
498
|
+
}
|
|
499
|
+
const response = {
|
|
500
|
+
device: deviceInfo,
|
|
501
|
+
installed,
|
|
502
|
+
output,
|
|
503
|
+
error: errorMsg
|
|
504
|
+
};
|
|
505
|
+
return wrapResponse(response);
|
|
506
|
+
}
|
|
288
507
|
if (name === "get_logs") {
|
|
289
508
|
const { platform, appId, deviceId, lines } = args;
|
|
290
509
|
let logs;
|
|
291
510
|
let deviceInfo;
|
|
292
511
|
if (platform === "android") {
|
|
293
|
-
|
|
294
|
-
const
|
|
512
|
+
// Resolve an explicit target device when multiple are attached
|
|
513
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
|
|
514
|
+
deviceInfo = resolved;
|
|
515
|
+
const response = await androidObserve.getLogs(appId, lines ?? 200, resolved.id);
|
|
295
516
|
logs = Array.isArray(response.logs) ? response.logs : [];
|
|
296
517
|
}
|
|
297
518
|
else {
|
|
298
|
-
|
|
299
|
-
|
|
519
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
|
|
520
|
+
deviceInfo = resolved;
|
|
521
|
+
const response = await iosObserve.getLogs(appId, resolved.id);
|
|
300
522
|
logs = Array.isArray(response.logs) ? response.logs : [];
|
|
301
523
|
}
|
|
302
524
|
// Filter crash lines (e.g. lines containing 'FATAL EXCEPTION') for internal or AI use
|
|
@@ -321,20 +543,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
321
543
|
]
|
|
322
544
|
};
|
|
323
545
|
}
|
|
546
|
+
if (name === "list_devices") {
|
|
547
|
+
const { platform, appId } = (args || {});
|
|
548
|
+
const devices = await listDevices(platform, appId);
|
|
549
|
+
return wrapResponse({ devices });
|
|
550
|
+
}
|
|
324
551
|
if (name === "capture_screenshot") {
|
|
325
552
|
const { platform, deviceId } = args;
|
|
326
553
|
let screenshot;
|
|
327
554
|
let resolution;
|
|
328
555
|
let deviceInfo;
|
|
329
556
|
if (platform === "android") {
|
|
330
|
-
|
|
331
|
-
|
|
557
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
558
|
+
deviceInfo = resolved;
|
|
559
|
+
const result = await androidObserve.captureScreen(resolved.id);
|
|
332
560
|
screenshot = result.screenshot;
|
|
333
561
|
resolution = result.resolution;
|
|
334
562
|
}
|
|
335
563
|
else {
|
|
336
|
-
|
|
337
|
-
|
|
564
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
565
|
+
deviceInfo = resolved;
|
|
566
|
+
const result = await iosObserve.captureScreenshot(resolved.id);
|
|
338
567
|
screenshot = result.screenshot;
|
|
339
568
|
resolution = result.resolution;
|
|
340
569
|
}
|
|
@@ -361,10 +590,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
361
590
|
const { platform, deviceId } = args;
|
|
362
591
|
let result;
|
|
363
592
|
if (platform === "android") {
|
|
364
|
-
|
|
593
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
594
|
+
result = await androidObserve.getUITree(resolved.id);
|
|
365
595
|
}
|
|
366
596
|
else if (platform === "ios") {
|
|
367
|
-
|
|
597
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
598
|
+
result = await iosObserve.getUITree(resolved.id);
|
|
368
599
|
}
|
|
369
600
|
else {
|
|
370
601
|
throw new Error(`Platform ${platform} not supported for get_ui_tree`);
|
|
@@ -373,9 +604,125 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
373
604
|
}
|
|
374
605
|
if (name === "get_current_screen") {
|
|
375
606
|
const { deviceId } = (args || {});
|
|
376
|
-
const
|
|
607
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
608
|
+
const result = await androidObserve.getCurrentScreen(resolved.id);
|
|
609
|
+
return wrapResponse(result);
|
|
610
|
+
}
|
|
611
|
+
if (name === "wait_for_element") {
|
|
612
|
+
const { platform, text, timeout, deviceId } = (args || {});
|
|
613
|
+
const effectiveTimeout = timeout ?? 10000;
|
|
614
|
+
let result;
|
|
615
|
+
if (platform === "android") {
|
|
616
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
617
|
+
result = await androidInteract.waitForElement(text, effectiveTimeout, resolved.id);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
621
|
+
result = await iosInteract.waitForElement(text, effectiveTimeout, resolved.id);
|
|
622
|
+
}
|
|
623
|
+
return wrapResponse(result);
|
|
624
|
+
}
|
|
625
|
+
if (name === "tap") {
|
|
626
|
+
const { platform, x, y, deviceId } = (args || {});
|
|
627
|
+
const effectivePlatform = platform || "android";
|
|
628
|
+
// Basic validation
|
|
629
|
+
if (typeof x !== 'number' || typeof y !== 'number') {
|
|
630
|
+
throw new Error("x and y coordinates are required and must be numbers");
|
|
631
|
+
}
|
|
632
|
+
let result;
|
|
633
|
+
if (effectivePlatform === "android") {
|
|
634
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
635
|
+
result = await androidInteract.tap(x, y, resolved.id);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId });
|
|
639
|
+
result = await iosInteract.tap(x, y, resolved.id);
|
|
640
|
+
}
|
|
641
|
+
return wrapResponse(result);
|
|
642
|
+
}
|
|
643
|
+
if (name === "swipe") {
|
|
644
|
+
const { platform, x1, y1, x2, y2, duration, deviceId } = (args || {});
|
|
645
|
+
const effectivePlatform = platform || "android";
|
|
646
|
+
if (typeof x1 !== 'number' || typeof y1 !== 'number' || typeof x2 !== 'number' || typeof y2 !== 'number' || typeof duration !== 'number') {
|
|
647
|
+
throw new Error("x1, y1, x2, y2, and duration are required and must be numbers");
|
|
648
|
+
}
|
|
649
|
+
let result;
|
|
650
|
+
if (effectivePlatform === "android") {
|
|
651
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
652
|
+
result = await androidInteract.swipe(x1, y1, x2, y2, duration, resolved.id);
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
throw new Error(`Platform ${effectivePlatform} not supported for swipe`);
|
|
656
|
+
}
|
|
657
|
+
return wrapResponse(result);
|
|
658
|
+
}
|
|
659
|
+
if (name === "type_text") {
|
|
660
|
+
const { platform, text, deviceId } = (args || {});
|
|
661
|
+
const effectivePlatform = platform || "android";
|
|
662
|
+
if (typeof text !== 'string') {
|
|
663
|
+
throw new Error("text is required and must be a string");
|
|
664
|
+
}
|
|
665
|
+
let result;
|
|
666
|
+
if (effectivePlatform === "android") {
|
|
667
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
668
|
+
result = await androidInteract.typeText(text, resolved.id);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
throw new Error(`Platform ${effectivePlatform} not supported for type_text`);
|
|
672
|
+
}
|
|
377
673
|
return wrapResponse(result);
|
|
378
674
|
}
|
|
675
|
+
if (name === "press_back") {
|
|
676
|
+
const { platform, deviceId } = (args || {});
|
|
677
|
+
const effectivePlatform = platform || "android";
|
|
678
|
+
if (effectivePlatform !== "android") {
|
|
679
|
+
throw new Error(`Platform ${effectivePlatform} not supported for press_back`);
|
|
680
|
+
}
|
|
681
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
682
|
+
const result = await androidInteract.pressBack(resolved.id);
|
|
683
|
+
return wrapResponse(result);
|
|
684
|
+
}
|
|
685
|
+
if (name === 'start_log_stream') {
|
|
686
|
+
const { platform, packageName, level, sessionId: argSession, deviceId } = args;
|
|
687
|
+
const sessionId = argSession || 'default';
|
|
688
|
+
const effectivePlatform = platform || 'android';
|
|
689
|
+
if (effectivePlatform === 'android') {
|
|
690
|
+
const resolved = await resolveTargetDevice({ platform: 'android', appId: packageName, deviceId });
|
|
691
|
+
const res = await startAndroidLogStream(packageName, level || 'error', resolved.id, sessionId);
|
|
692
|
+
return wrapResponse(res);
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', appId: packageName, deviceId });
|
|
696
|
+
const res = await startIOSLogStream(packageName, level || 'error', resolved.id, sessionId);
|
|
697
|
+
return wrapResponse(res);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (name === 'read_log_stream') {
|
|
701
|
+
const { platform, sessionId: argSession, limit, since } = (args || {});
|
|
702
|
+
const sid = argSession || 'default';
|
|
703
|
+
const effectivePlatform = platform || 'android';
|
|
704
|
+
if (effectivePlatform === 'android') {
|
|
705
|
+
const { entries, crash_summary } = await readLogStreamLines(sid, limit ?? 100, since);
|
|
706
|
+
return wrapResponse({ entries, crash_summary });
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
const { entries, crash_summary } = await readIOSLogStreamLines(sid, limit ?? 100, since);
|
|
710
|
+
return wrapResponse({ entries, crash_summary });
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (name === 'stop_log_stream') {
|
|
714
|
+
const { platform, sessionId: argSession } = (args || {});
|
|
715
|
+
const sid = argSession || 'default';
|
|
716
|
+
const effectivePlatform = platform || 'android';
|
|
717
|
+
if (effectivePlatform === 'android') {
|
|
718
|
+
const res = await stopAndroidLogStream(sid);
|
|
719
|
+
return wrapResponse(res);
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
const res = await stopIOSLogStream(sid);
|
|
723
|
+
return wrapResponse(res);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
379
726
|
}
|
|
380
727
|
catch (error) {
|
|
381
728
|
return {
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,27 @@
|
|
|
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.8.0]
|
|
6
6
|
|
|
7
7
|
### Added
|
|
8
|
+
- **`list_devices` tool**: enumerate connected Android devices and iOS simulators. Returns device metadata (id, platform, osVersion, model, simulator, appInstalled).
|
|
9
|
+
- **`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.
|
|
10
|
+
- **`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).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
15
|
+
## [0.7.0]
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`wait_for_element` tool**: Added ability to wait for a specific UI element to appear on screen. Polls `get_ui_tree` until timeout. Useful for waiting on app transitions or loading states.
|
|
8
19
|
- **`get_current_screen` tool**: Added ability to determine the currently visible activity on an Android device using `dumpsys activity activities`. Includes robust regex parsing to handle various Android versions.
|
|
20
|
+
- **`tap` tool**: Added ability to tap at specific screen coordinates on Android and iOS devices.
|
|
21
|
+
- **`swipe` tool**: Added ability to simulate swipe gestures (scroll, drag) on Android devices.
|
|
22
|
+
- **`type_text` tool**: Added ability to type text into focused input fields on Android devices.
|
|
23
|
+
- **`press_back` tool**: Added ability to simulate the Android Back button.
|
|
9
24
|
|
|
10
|
-
## [0.4.0]
|
|
25
|
+
## [0.4.0]
|
|
11
26
|
|
|
12
27
|
### Added
|
|
13
28
|
- **`terminate_app` tool**: Added ability to terminate apps on Android and iOS.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-debug-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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
|
}
|