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/dist/server.js CHANGED
@@ -2,11 +2,17 @@
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
- import { startAndroidApp, getAndroidLogs, captureAndroidScreen, getAndroidDeviceMetadata, terminateAndroidApp, restartAndroidApp, resetAndroidAppData } from "./android.js";
6
- import { startIOSApp, getIOSLogs, captureIOSScreenshot, getIOSDeviceMetadata, terminateIOSApp, restartIOSApp, resetIOSAppData } from "./ios.js";
5
+ import { AndroidObserve } from "./android/observe.js";
6
+ import { AndroidInteract } from "./android/interact.js";
7
+ import { iOSObserve } from "./ios/observe.js";
8
+ import { iOSInteract } from "./ios/interact.js";
9
+ const androidObserve = new AndroidObserve();
10
+ const androidInteract = new AndroidInteract();
11
+ const iosObserve = new iOSObserve();
12
+ const iosInteract = new iOSInteract();
7
13
  const server = new Server({
8
14
  name: "mobile-debug-mcp",
9
- version: "0.4.0"
15
+ version: "0.7.0"
10
16
  }, {
11
17
  capabilities: {
12
18
  tools: {}
@@ -153,6 +159,158 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
153
159
  },
154
160
  required: ["platform"]
155
161
  }
162
+ },
163
+ {
164
+ name: "get_ui_tree",
165
+ description: "Get the current UI hierarchy from an Android device or iOS simulator. Returns a structured JSON representation of the screen content.",
166
+ inputSchema: {
167
+ type: "object",
168
+ properties: {
169
+ platform: {
170
+ type: "string",
171
+ enum: ["android", "ios"],
172
+ description: "Platform to get UI tree for"
173
+ },
174
+ deviceId: {
175
+ type: "string",
176
+ description: "Device Serial (Android) or UDID (iOS). Defaults to connected/booted device."
177
+ }
178
+ },
179
+ required: ["platform"]
180
+ }
181
+ },
182
+ {
183
+ name: "get_current_screen",
184
+ description: "Get the currently visible activity on an Android device. Returns package and activity name.",
185
+ inputSchema: {
186
+ type: "object",
187
+ properties: {
188
+ deviceId: {
189
+ type: "string",
190
+ description: "Device Serial (Android). Defaults to connected/booted device."
191
+ }
192
+ }
193
+ }
194
+ },
195
+ {
196
+ name: "wait_for_element",
197
+ description: "Wait until a UI element with matching text appears on screen or timeout is reached.",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ platform: {
202
+ type: "string",
203
+ enum: ["android", "ios"],
204
+ description: "Platform to check"
205
+ },
206
+ text: {
207
+ type: "string",
208
+ description: "Text content of the element to wait for"
209
+ },
210
+ timeout: {
211
+ type: "number",
212
+ description: "Max wait time in ms (default 10000)",
213
+ default: 10000
214
+ },
215
+ deviceId: {
216
+ type: "string",
217
+ description: "Device Serial/UDID. Defaults to connected/booted device."
218
+ }
219
+ },
220
+ required: ["platform", "text"]
221
+ }
222
+ },
223
+ {
224
+ name: "tap",
225
+ description: "Simulate a finger tap on the device screen at specific coordinates.",
226
+ inputSchema: {
227
+ type: "object",
228
+ properties: {
229
+ platform: {
230
+ type: "string",
231
+ enum: ["android", "ios"],
232
+ description: "Platform to tap on"
233
+ },
234
+ x: {
235
+ type: "number",
236
+ description: "X coordinate"
237
+ },
238
+ y: {
239
+ type: "number",
240
+ description: "Y coordinate"
241
+ },
242
+ deviceId: {
243
+ type: "string",
244
+ description: "Device Serial/UDID. Defaults to connected/booted device."
245
+ }
246
+ },
247
+ required: ["x", "y"]
248
+ }
249
+ },
250
+ {
251
+ name: "swipe",
252
+ description: "Simulate a swipe gesture on an Android device.",
253
+ inputSchema: {
254
+ type: "object",
255
+ properties: {
256
+ platform: {
257
+ type: "string",
258
+ enum: ["android"],
259
+ description: "Platform to swipe on (currently only android supported)"
260
+ },
261
+ x1: { type: "number", description: "Start X coordinate" },
262
+ y1: { type: "number", description: "Start Y coordinate" },
263
+ x2: { type: "number", description: "End X coordinate" },
264
+ y2: { type: "number", description: "End Y coordinate" },
265
+ duration: { type: "number", description: "Duration in ms" },
266
+ deviceId: {
267
+ type: "string",
268
+ description: "Device Serial/UDID. Defaults to connected/booted device."
269
+ }
270
+ },
271
+ required: ["x1", "y1", "x2", "y2", "duration"]
272
+ }
273
+ },
274
+ {
275
+ name: "type_text",
276
+ description: "Type text into the currently focused input field on an Android device.",
277
+ inputSchema: {
278
+ type: "object",
279
+ properties: {
280
+ platform: {
281
+ type: "string",
282
+ enum: ["android"],
283
+ description: "Platform to type on (currently only android supported)"
284
+ },
285
+ text: {
286
+ type: "string",
287
+ description: "The text to type"
288
+ },
289
+ deviceId: {
290
+ type: "string",
291
+ description: "Device Serial/UDID. Defaults to connected/booted device."
292
+ }
293
+ },
294
+ required: ["text"]
295
+ }
296
+ },
297
+ {
298
+ name: "press_back",
299
+ description: "Simulate pressing the Android Back button.",
300
+ inputSchema: {
301
+ type: "object",
302
+ properties: {
303
+ platform: {
304
+ type: "string",
305
+ enum: ["android"],
306
+ description: "Platform (currently only android supported)"
307
+ },
308
+ deviceId: {
309
+ type: "string",
310
+ description: "Device Serial/UDID. Defaults to connected/booted device."
311
+ }
312
+ }
313
+ }
156
314
  }
157
315
  ]
158
316
  }));
@@ -165,16 +323,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
165
323
  let launchTimeMs;
166
324
  let deviceInfo;
167
325
  if (platform === "android") {
168
- const result = await startAndroidApp(appId, deviceId);
326
+ const result = await androidInteract.startApp(appId, deviceId);
169
327
  appStarted = result.appStarted;
170
328
  launchTimeMs = result.launchTimeMs;
171
- deviceInfo = await getAndroidDeviceMetadata(appId, deviceId);
329
+ deviceInfo = result.device;
172
330
  }
173
331
  else {
174
- const result = await startIOSApp(appId, deviceId);
332
+ const result = await iosInteract.startApp(appId, deviceId);
175
333
  appStarted = result.appStarted;
176
334
  launchTimeMs = result.launchTimeMs;
177
- deviceInfo = await getIOSDeviceMetadata(deviceId);
335
+ deviceInfo = result.device;
178
336
  }
179
337
  const response = {
180
338
  device: deviceInfo,
@@ -188,14 +346,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
188
346
  let appTerminated;
189
347
  let deviceInfo;
190
348
  if (platform === "android") {
191
- const result = await terminateAndroidApp(appId, deviceId);
349
+ const result = await androidInteract.terminateApp(appId, deviceId);
192
350
  appTerminated = result.appTerminated;
193
- deviceInfo = await getAndroidDeviceMetadata(appId, deviceId);
351
+ deviceInfo = result.device;
194
352
  }
195
353
  else {
196
- const result = await terminateIOSApp(appId, deviceId);
354
+ const result = await iosInteract.terminateApp(appId, deviceId);
197
355
  appTerminated = result.appTerminated;
198
- deviceInfo = await getIOSDeviceMetadata(deviceId);
356
+ deviceInfo = result.device;
199
357
  }
200
358
  const response = {
201
359
  device: deviceInfo,
@@ -209,16 +367,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
209
367
  let launchTimeMs;
210
368
  let deviceInfo;
211
369
  if (platform === "android") {
212
- const result = await restartAndroidApp(appId, deviceId);
370
+ const result = await androidInteract.restartApp(appId, deviceId);
213
371
  appRestarted = result.appRestarted;
214
372
  launchTimeMs = result.launchTimeMs;
215
- deviceInfo = await getAndroidDeviceMetadata(appId, deviceId);
373
+ deviceInfo = result.device;
216
374
  }
217
375
  else {
218
- const result = await restartIOSApp(appId, deviceId);
376
+ const result = await iosInteract.restartApp(appId, deviceId);
219
377
  appRestarted = result.appRestarted;
220
378
  launchTimeMs = result.launchTimeMs;
221
- deviceInfo = await getIOSDeviceMetadata(deviceId);
379
+ deviceInfo = result.device;
222
380
  }
223
381
  const response = {
224
382
  device: deviceInfo,
@@ -232,14 +390,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
232
390
  let dataCleared;
233
391
  let deviceInfo;
234
392
  if (platform === "android") {
235
- const result = await resetAndroidAppData(appId, deviceId);
393
+ const result = await androidInteract.resetAppData(appId, deviceId);
236
394
  dataCleared = result.dataCleared;
237
- deviceInfo = await getAndroidDeviceMetadata(appId, deviceId);
395
+ deviceInfo = result.device;
238
396
  }
239
397
  else {
240
- const result = await resetIOSAppData(appId, deviceId);
398
+ const result = await iosInteract.resetAppData(appId, deviceId);
241
399
  dataCleared = result.dataCleared;
242
- deviceInfo = await getIOSDeviceMetadata(deviceId);
400
+ deviceInfo = result.device;
243
401
  }
244
402
  const response = {
245
403
  device: deviceInfo,
@@ -252,13 +410,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
252
410
  let logs;
253
411
  let deviceInfo;
254
412
  if (platform === "android") {
255
- deviceInfo = await getAndroidDeviceMetadata(appId || "", deviceId);
256
- const response = await getAndroidLogs(appId, lines ?? 200, deviceId);
413
+ deviceInfo = await androidObserve.getDeviceMetadata(appId || "", deviceId);
414
+ const response = await androidObserve.getLogs(appId, lines ?? 200, deviceId);
257
415
  logs = Array.isArray(response.logs) ? response.logs : [];
258
416
  }
259
417
  else {
260
- deviceInfo = await getIOSDeviceMetadata(deviceId);
261
- const response = await getIOSLogs(appId, deviceId);
418
+ deviceInfo = await iosObserve.getDeviceMetadata(deviceId);
419
+ const response = await iosObserve.getLogs(appId, deviceId);
262
420
  logs = Array.isArray(response.logs) ? response.logs : [];
263
421
  }
264
422
  // Filter crash lines (e.g. lines containing 'FATAL EXCEPTION') for internal or AI use
@@ -289,14 +447,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
289
447
  let resolution;
290
448
  let deviceInfo;
291
449
  if (platform === "android") {
292
- deviceInfo = await getAndroidDeviceMetadata("", deviceId);
293
- const result = await captureAndroidScreen(deviceId);
450
+ deviceInfo = await androidObserve.getDeviceMetadata("", deviceId);
451
+ const result = await androidObserve.captureScreen(deviceId);
294
452
  screenshot = result.screenshot;
295
453
  resolution = result.resolution;
296
454
  }
297
455
  else {
298
- deviceInfo = await getIOSDeviceMetadata(deviceId);
299
- const result = await captureIOSScreenshot(deviceId);
456
+ deviceInfo = await iosObserve.getDeviceMetadata(deviceId);
457
+ const result = await iosObserve.captureScreenshot(deviceId);
300
458
  screenshot = result.screenshot;
301
459
  resolution = result.resolution;
302
460
  }
@@ -319,6 +477,92 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
319
477
  ]
320
478
  };
321
479
  }
480
+ if (name === "get_ui_tree") {
481
+ const { platform, deviceId } = args;
482
+ let result;
483
+ if (platform === "android") {
484
+ result = await androidObserve.getUITree(deviceId);
485
+ }
486
+ else if (platform === "ios") {
487
+ result = await iosObserve.getUITree(deviceId);
488
+ }
489
+ else {
490
+ throw new Error(`Platform ${platform} not supported for get_ui_tree`);
491
+ }
492
+ return wrapResponse(result);
493
+ }
494
+ if (name === "get_current_screen") {
495
+ const { deviceId } = (args || {});
496
+ const result = await androidObserve.getCurrentScreen(deviceId);
497
+ return wrapResponse(result);
498
+ }
499
+ if (name === "wait_for_element") {
500
+ const { platform, text, timeout, deviceId } = (args || {});
501
+ const effectiveTimeout = timeout ?? 10000;
502
+ let result;
503
+ if (platform === "android") {
504
+ result = await androidInteract.waitForElement(text, effectiveTimeout, deviceId);
505
+ }
506
+ else {
507
+ result = await iosInteract.waitForElement(text, effectiveTimeout, deviceId);
508
+ }
509
+ return wrapResponse(result);
510
+ }
511
+ if (name === "tap") {
512
+ const { platform, x, y, deviceId } = (args || {});
513
+ const effectivePlatform = platform || "android";
514
+ // Basic validation
515
+ if (typeof x !== 'number' || typeof y !== 'number') {
516
+ throw new Error("x and y coordinates are required and must be numbers");
517
+ }
518
+ let result;
519
+ if (effectivePlatform === "android") {
520
+ result = await androidInteract.tap(x, y, deviceId);
521
+ }
522
+ else {
523
+ result = await iosInteract.tap(x, y, deviceId);
524
+ }
525
+ return wrapResponse(result);
526
+ }
527
+ if (name === "swipe") {
528
+ const { platform, x1, y1, x2, y2, duration, deviceId } = (args || {});
529
+ const effectivePlatform = platform || "android";
530
+ if (typeof x1 !== 'number' || typeof y1 !== 'number' || typeof x2 !== 'number' || typeof y2 !== 'number' || typeof duration !== 'number') {
531
+ throw new Error("x1, y1, x2, y2, and duration are required and must be numbers");
532
+ }
533
+ let result;
534
+ if (effectivePlatform === "android") {
535
+ result = await androidInteract.swipe(x1, y1, x2, y2, duration, deviceId);
536
+ }
537
+ else {
538
+ throw new Error(`Platform ${effectivePlatform} not supported for swipe`);
539
+ }
540
+ return wrapResponse(result);
541
+ }
542
+ if (name === "type_text") {
543
+ const { platform, text, deviceId } = (args || {});
544
+ const effectivePlatform = platform || "android";
545
+ if (typeof text !== 'string') {
546
+ throw new Error("text is required and must be a string");
547
+ }
548
+ let result;
549
+ if (effectivePlatform === "android") {
550
+ result = await androidInteract.typeText(text, deviceId);
551
+ }
552
+ else {
553
+ throw new Error(`Platform ${effectivePlatform} not supported for type_text`);
554
+ }
555
+ return wrapResponse(result);
556
+ }
557
+ if (name === "press_back") {
558
+ const { platform, deviceId } = (args || {});
559
+ const effectivePlatform = platform || "android";
560
+ if (effectivePlatform !== "android") {
561
+ throw new Error(`Platform ${effectivePlatform} not supported for press_back`);
562
+ }
563
+ const result = await androidInteract.pressBack(deviceId);
564
+ return wrapResponse(result);
565
+ }
322
566
  }
323
567
  catch (error) {
324
568
  return {
package/docs/CHANGELOG.md CHANGED
@@ -2,7 +2,17 @@
2
2
 
3
3
  All notable changes to the **Mobile Debug MCP** project will be documented in this file.
4
4
 
5
- ## [0.4.0] - 2026-03-09
5
+ ## [0.7.0]
6
+
7
+ ### Added
8
+ - **`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.
9
+ - **`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.
10
+ - **`tap` tool**: Added ability to tap at specific screen coordinates on Android and iOS devices.
11
+ - **`swipe` tool**: Added ability to simulate swipe gestures (scroll, drag) on Android devices.
12
+ - **`type_text` tool**: Added ability to type text into focused input fields on Android devices.
13
+ - **`press_back` tool**: Added ability to simulate the Android Back button.
14
+
15
+ ## [0.4.0]
6
16
 
7
17
  ### Added
8
18
  - **`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.5.0",
3
+ "version": "0.7.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": {
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@modelcontextprotocol/sdk": "^1.0.0",
19
+ "fast-xml-parser": "^5.5.1",
19
20
  "zod": "^3.22.4"
20
21
  },
21
22
  "devDependencies": {
package/smoke-test.ts CHANGED
@@ -1,8 +1,15 @@
1
- import { startAndroidApp, terminateAndroidApp, captureAndroidScreen, getAndroidLogs } from "./src/android.js";
2
- import { startIOSApp, terminateIOSApp, captureIOSScreenshot, getIOSLogs } from "./src/ios.js";
1
+ import { AndroidObserve } from "./src/android/observe.js";
2
+ import { AndroidInteract } from "./src/android/interact.js";
3
+ import { iOSObserve } from "./src/ios/observe.js";
4
+ import { iOSInteract } from "./src/ios/interact.js";
3
5
  import fs from "fs/promises";
4
6
  import path from "path";
5
7
 
8
+ const androidObserve = new AndroidObserve();
9
+ const androidInteract = new AndroidInteract();
10
+ const iosObserve = new iOSObserve();
11
+ const iosInteract = new iOSInteract();
12
+
6
13
  async function main() {
7
14
  const args = process.argv.slice(2);
8
15
  const platform = args[0] as string; // Cast to string first
@@ -22,11 +29,11 @@ async function main() {
22
29
  let launchTimeMs: number;
23
30
 
24
31
  if (platform === "android") {
25
- const result = await startAndroidApp(appId);
32
+ const result = await androidInteract.startApp(appId);
26
33
  startResult = result.appStarted;
27
34
  launchTimeMs = result.launchTimeMs;
28
35
  } else {
29
- const result = await startIOSApp(appId);
36
+ const result = await iosInteract.startApp(appId);
30
37
  startResult = result.appStarted;
31
38
  launchTimeMs = result.launchTimeMs;
32
39
  }
@@ -47,11 +54,11 @@ async function main() {
47
54
  let resolution: { width: number; height: number };
48
55
 
49
56
  if (platform === "android") {
50
- const result = await captureAndroidScreen();
57
+ const result = await androidObserve.captureScreen();
51
58
  screenshotBase64 = result.screenshot;
52
59
  resolution = result.resolution;
53
60
  } else {
54
- const result = await captureIOSScreenshot();
61
+ const result = await iosObserve.captureScreenshot();
55
62
  screenshotBase64 = result.screenshot;
56
63
  resolution = result.resolution;
57
64
  }
@@ -70,11 +77,11 @@ async function main() {
70
77
  let logs: string[] = [];
71
78
 
72
79
  if (platform === "android") {
73
- const result = await getAndroidLogs(appId, 50);
80
+ const result = await androidObserve.getLogs(appId, 50);
74
81
  logsCount = result.logCount;
75
82
  logs = result.logs;
76
83
  } else {
77
- const result = await getIOSLogs();
84
+ const result = await iosObserve.getLogs(appId);
78
85
  logsCount = result.logCount;
79
86
  logs = result.logs;
80
87
  }
@@ -90,10 +97,10 @@ async function main() {
90
97
  let termResult: boolean;
91
98
 
92
99
  if (platform === "android") {
93
- const result = await terminateAndroidApp(appId);
100
+ const result = await androidInteract.terminateApp(appId);
94
101
  termResult = result.appTerminated;
95
102
  } else {
96
- const result = await terminateIOSApp(appId);
103
+ const result = await iosInteract.terminateApp(appId);
97
104
  termResult = result.appTerminated;
98
105
  }
99
106
 
@@ -0,0 +1,126 @@
1
+ import { StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse, WaitForElementResponse, TapResponse, SwipeResponse, TypeTextResponse, PressBackResponse } from "../types.js"
2
+ import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "./utils.js"
3
+ import { AndroidObserve } from "./observe.js"
4
+
5
+ export class AndroidInteract {
6
+ private observe = new AndroidObserve();
7
+
8
+ async waitForElement(text: string, timeout: number, deviceId?: string): Promise<WaitForElementResponse> {
9
+ const metadata = await getAndroidDeviceMetadata("", deviceId)
10
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
11
+ const startTime = Date.now();
12
+
13
+ while (Date.now() - startTime < timeout) {
14
+ try {
15
+ const tree = await this.observe.getUITree(deviceId);
16
+
17
+ if (tree.error) {
18
+ return { device: deviceInfo, found: false, error: tree.error };
19
+ }
20
+
21
+ const element = tree.elements.find(e => e.text === text);
22
+ if (element) {
23
+ return { device: deviceInfo, found: true, element };
24
+ }
25
+ } catch (e) {
26
+ // Ignore errors during polling and retry
27
+ console.error("Error polling UI tree:", e);
28
+ }
29
+
30
+ const elapsed = Date.now() - startTime;
31
+ const remaining = timeout - elapsed;
32
+ if (remaining <= 0) break;
33
+
34
+ await new Promise(resolve => setTimeout(resolve, Math.min(500, remaining)));
35
+ }
36
+ return { device: deviceInfo, found: false };
37
+ }
38
+
39
+ async tap(x: number, y: number, deviceId?: string): Promise<TapResponse> {
40
+ const metadata = await getAndroidDeviceMetadata("", deviceId)
41
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
42
+
43
+ try {
44
+ await execAdb(['shell', 'input', 'tap', x.toString(), y.toString()], deviceId)
45
+ return { device: deviceInfo, success: true, x, y }
46
+ } catch (e) {
47
+ return { device: deviceInfo, success: false, x, y, error: e instanceof Error ? e.message : String(e) }
48
+ }
49
+ }
50
+
51
+ async swipe(x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string): Promise<SwipeResponse> {
52
+ const metadata = await getAndroidDeviceMetadata("", deviceId)
53
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
54
+
55
+ try {
56
+ await execAdb(['shell', 'input', 'swipe', x1.toString(), y1.toString(), x2.toString(), y2.toString(), duration.toString()], deviceId)
57
+ return { device: deviceInfo, success: true, start: [x1, y1], end: [x2, y2], duration }
58
+ } catch (e) {
59
+ return { device: deviceInfo, success: false, start: [x1, y1], end: [x2, y2], duration, error: e instanceof Error ? e.message : String(e) }
60
+ }
61
+ }
62
+
63
+ async typeText(text: string, deviceId?: string): Promise<TypeTextResponse> {
64
+ const metadata = await getAndroidDeviceMetadata("", deviceId)
65
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
66
+
67
+ try {
68
+ // Encode spaces as %s to ensure proper input handling by adb shell input text
69
+ const encodedText = text.replace(/\s/g, '%s')
70
+ // Note: 'input text' might fail with some characters or if keyboard isn't ready, but it's the standard ADB way.
71
+ await execAdb(['shell', 'input', 'text', encodedText], deviceId)
72
+ return { device: deviceInfo, success: true, text }
73
+ } catch (e) {
74
+ return { device: deviceInfo, success: false, text, error: e instanceof Error ? e.message : String(e) }
75
+ }
76
+ }
77
+
78
+ async pressBack(deviceId?: string): Promise<PressBackResponse> {
79
+ const metadata = await getAndroidDeviceMetadata("", deviceId)
80
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
81
+
82
+ try {
83
+ await execAdb(['shell', 'input', 'keyevent', '4'], deviceId)
84
+ return { device: deviceInfo, success: true }
85
+ } catch (e) {
86
+ return { device: deviceInfo, success: false, error: e instanceof Error ? e.message : String(e) }
87
+ }
88
+ }
89
+
90
+ async startApp(appId: string, deviceId?: string): Promise<StartAppResponse> {
91
+ const metadata = await getAndroidDeviceMetadata(appId, deviceId)
92
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
93
+
94
+ await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId)
95
+
96
+ return { device: deviceInfo, appStarted: true, launchTimeMs: 1000 }
97
+ }
98
+
99
+ async terminateApp(appId: string, deviceId?: string): Promise<TerminateAppResponse> {
100
+ const metadata = await getAndroidDeviceMetadata(appId, deviceId)
101
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
102
+
103
+ await execAdb(['shell', 'am', 'force-stop', appId], deviceId)
104
+
105
+ return { device: deviceInfo, appTerminated: true }
106
+ }
107
+
108
+ async restartApp(appId: string, deviceId?: string): Promise<RestartAppResponse> {
109
+ await this.terminateApp(appId, deviceId)
110
+ const startResult = await this.startApp(appId, deviceId)
111
+ return {
112
+ device: startResult.device,
113
+ appRestarted: startResult.appStarted,
114
+ launchTimeMs: startResult.launchTimeMs
115
+ }
116
+ }
117
+
118
+ async resetAppData(appId: string, deviceId?: string): Promise<ResetAppDataResponse> {
119
+ const metadata = await getAndroidDeviceMetadata(appId, deviceId)
120
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
121
+
122
+ const output = await execAdb(['shell', 'pm', 'clear', appId], deviceId)
123
+
124
+ return { device: deviceInfo, dataCleared: output === 'Success' }
125
+ }
126
+ }