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/dist/ios/utils.js CHANGED
@@ -112,3 +112,157 @@ export async function getIOSDeviceMetadata(deviceId = "booted") {
112
112
  });
113
113
  });
114
114
  }
115
+ export async function listIOSDevices(appId) {
116
+ return new Promise((resolve) => {
117
+ execFile(XCRUN, ['simctl', 'list', 'devices', '--json'], (err, stdout) => {
118
+ if (err || !stdout)
119
+ return resolve([]);
120
+ try {
121
+ const data = JSON.parse(stdout);
122
+ const devicesMap = data.devices || {};
123
+ const out = [];
124
+ const checks = [];
125
+ for (const runtime in devicesMap) {
126
+ const devices = devicesMap[runtime];
127
+ if (Array.isArray(devices)) {
128
+ for (const device of devices) {
129
+ const info = {
130
+ platform: 'ios',
131
+ id: device.udid,
132
+ osVersion: parseRuntimeName(runtime),
133
+ model: device.name,
134
+ simulator: true
135
+ };
136
+ if (appId) {
137
+ // check if installed
138
+ const p = execCommand(['simctl', 'get_app_container', device.udid, appId, 'data'], device.udid)
139
+ .then(() => { info.appInstalled = true; })
140
+ .catch(() => { info.appInstalled = false; })
141
+ .then(() => { out.push(info); });
142
+ checks.push(p);
143
+ }
144
+ else {
145
+ out.push(info);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ Promise.all(checks).then(() => resolve(out)).catch(() => resolve(out));
151
+ }
152
+ catch (e) {
153
+ resolve([]);
154
+ }
155
+ });
156
+ });
157
+ }
158
+ // --- iOS live log stream support ---
159
+ import { createWriteStream, promises as fsPromises } from 'fs';
160
+ import path from 'path';
161
+ import { parseLogLine } from '../android/utils.js';
162
+ const iosActiveLogStreams = new Map();
163
+ // Test helpers
164
+ export function _setIOSActiveLogStream(sessionId, file) {
165
+ iosActiveLogStreams.set(sessionId, { proc: {}, file });
166
+ }
167
+ export function _clearIOSActiveLogStream(sessionId) {
168
+ iosActiveLogStreams.delete(sessionId);
169
+ }
170
+ export async function startIOSLogStream(bundleId, level = 'error', deviceId = 'booted', sessionId = 'default') {
171
+ try {
172
+ // Build predicate to filter by process or subsystem
173
+ const predicate = `process == "${bundleId}" or subsystem contains "${bundleId}"`;
174
+ // Prevent multiple streams per session
175
+ if (iosActiveLogStreams.has(sessionId)) {
176
+ try {
177
+ iosActiveLogStreams.get(sessionId).proc.kill();
178
+ }
179
+ catch (e) { }
180
+ iosActiveLogStreams.delete(sessionId);
181
+ }
182
+ // Start simctl log stream: xcrun simctl spawn <device> log stream --style syslog --predicate '<predicate>'
183
+ const args = ['simctl', 'spawn', deviceId, 'log', 'stream', '--style', 'syslog', '--predicate', predicate];
184
+ const proc = spawn(XCRUN, args);
185
+ // Prepare output file
186
+ const tmpDir = process.env.TMPDIR || '/tmp';
187
+ const file = path.join(tmpDir, `mobile-debug-ios-log-${sessionId}.ndjson`);
188
+ const stream = createWriteStream(file, { flags: 'a' });
189
+ proc.stdout.on('data', (chunk) => {
190
+ const text = chunk.toString();
191
+ const lines = text.split(/\r?\n/).filter(Boolean);
192
+ for (const l of lines) {
193
+ // Try to parse with shared parser; parser may be optimized for Android but extracts exceptions and message
194
+ const entry = parseLogLine(l);
195
+ stream.write(JSON.stringify(entry) + '\n');
196
+ }
197
+ });
198
+ proc.stderr.on('data', (chunk) => {
199
+ const text = chunk.toString();
200
+ const lines = text.split(/\r?\n/).filter(Boolean);
201
+ for (const l of lines) {
202
+ const entry = { timestamp: '', level: 'E', tag: 'xcrun', message: l };
203
+ stream.write(JSON.stringify(entry) + '\n');
204
+ }
205
+ });
206
+ proc.on('close', (code) => {
207
+ stream.end();
208
+ iosActiveLogStreams.delete(sessionId);
209
+ });
210
+ iosActiveLogStreams.set(sessionId, { proc, file });
211
+ return { success: true, stream_started: true };
212
+ }
213
+ catch (err) {
214
+ return { success: false, error: 'log_stream_start_failed' };
215
+ }
216
+ }
217
+ export async function stopIOSLogStream(sessionId = 'default') {
218
+ const entry = iosActiveLogStreams.get(sessionId);
219
+ if (!entry)
220
+ return { success: true };
221
+ try {
222
+ entry.proc.kill();
223
+ }
224
+ catch (e) { }
225
+ iosActiveLogStreams.delete(sessionId);
226
+ return { success: true };
227
+ }
228
+ export async function readIOSLogStreamLines(sessionId = 'default', limit = 100, since) {
229
+ const entry = iosActiveLogStreams.get(sessionId);
230
+ if (!entry)
231
+ return { entries: [] };
232
+ try {
233
+ const data = await fsPromises.readFile(entry.file, 'utf8').catch(() => '');
234
+ if (!data)
235
+ return { entries: [], crash_summary: { crash_detected: false } };
236
+ const lines = data.split(/\r?\n/).filter(Boolean);
237
+ const parsed = lines.map(l => {
238
+ try {
239
+ return JSON.parse(l);
240
+ }
241
+ catch {
242
+ return { message: l, _iso: null, crash: false };
243
+ }
244
+ });
245
+ // Minimal since filtering if provided
246
+ let filtered = parsed;
247
+ if (since) {
248
+ let sinceMs = null;
249
+ if (/^\d+$/.test(since))
250
+ sinceMs = Number(since);
251
+ else {
252
+ const sDate = new Date(since);
253
+ if (!isNaN(sDate.getTime()))
254
+ sinceMs = sDate.getTime();
255
+ }
256
+ if (sinceMs !== null) {
257
+ filtered = parsed.filter(p => p._iso && (new Date(p._iso).getTime() >= sinceMs));
258
+ }
259
+ }
260
+ const entries = filtered.slice(-Math.max(0, limit));
261
+ const crashEntry = entries.find(e => e.crash);
262
+ const crash_summary = crashEntry ? { crash_detected: true, exception: crashEntry.exception, sample: crashEntry.message } : { crash_detected: false };
263
+ return { entries, crash_summary };
264
+ }
265
+ catch (e) {
266
+ return { entries: [], crash_summary: { crash_detected: false } };
267
+ }
268
+ }
@@ -0,0 +1,70 @@
1
+ import { listAndroidDevices } from "./android/utils.js";
2
+ import { listIOSDevices } from "./ios/utils.js";
3
+ function parseNumericVersion(v) {
4
+ if (!v)
5
+ return 0;
6
+ // extract first number groups like 17.0 -> 17.0 or Android 12 -> 12
7
+ const m = v.match(/(\d+)(?:[\.\-](\d+))?/);
8
+ if (!m)
9
+ return 0;
10
+ const major = parseInt(m[1], 10) || 0;
11
+ const minor = parseInt(m[2] || "0", 10) || 0;
12
+ return major + minor / 100;
13
+ }
14
+ export async function listDevices(platform, appId) {
15
+ if (!platform || platform === "android") {
16
+ const android = await listAndroidDevices(appId);
17
+ if (platform === "android")
18
+ return android;
19
+ // if no platform specified, merge with ios below
20
+ const ios = await listIOSDevices(appId);
21
+ return [...android, ...ios];
22
+ }
23
+ return listIOSDevices(appId);
24
+ }
25
+ export async function resolveTargetDevice(opts) {
26
+ const { platform, appId, prefer, deviceId } = opts;
27
+ const devices = await listDevices(platform, appId);
28
+ if (deviceId) {
29
+ const found = devices.find(d => d.id === deviceId);
30
+ if (!found)
31
+ throw new Error(`Device '${deviceId}' not found for platform ${platform}`);
32
+ return found;
33
+ }
34
+ let candidates = devices.slice();
35
+ // Apply prefer filter
36
+ if (prefer === "physical")
37
+ candidates = candidates.filter(d => !d.simulator);
38
+ if (prefer === "emulator")
39
+ candidates = candidates.filter(d => d.simulator);
40
+ // If appId provided, prefer devices with appInstalled
41
+ if (appId) {
42
+ const installed = candidates.filter(d => d.appInstalled);
43
+ if (installed.length > 0)
44
+ candidates = installed;
45
+ }
46
+ if (candidates.length === 1)
47
+ return candidates[0];
48
+ if (candidates.length > 1) {
49
+ // Prefer physical over emulator unless prefer=emulator
50
+ if (!prefer) {
51
+ const physical = candidates.filter(d => !d.simulator);
52
+ if (physical.length === 1)
53
+ return physical[0];
54
+ if (physical.length > 1)
55
+ candidates = physical;
56
+ }
57
+ // Pick highest OS version
58
+ candidates.sort((a, b) => parseNumericVersion(b.osVersion) - parseNumericVersion(a.osVersion));
59
+ // If top is unique (numeric differs), return it
60
+ if (candidates.length > 1 && parseNumericVersion(candidates[0].osVersion) > parseNumericVersion(candidates[1].osVersion)) {
61
+ return candidates[0];
62
+ }
63
+ // Ambiguous: throw an error with candidate list so caller (agent) can present choices
64
+ const list = candidates.map(d => ({ id: d.id, platform: d.platform, osVersion: d.osVersion, model: d.model, simulator: d.simulator, appInstalled: d.appInstalled }));
65
+ const err = new Error(`Multiple matching devices found: ${JSON.stringify(list, null, 2)}`);
66
+ err.devices = list;
67
+ throw err;
68
+ }
69
+ throw new Error(`No devices found for platform ${platform}`);
70
+ }
package/dist/server.js CHANGED
@@ -6,6 +6,12 @@ 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 { installAppHandler } from './tools/install.js';
10
+ import { startAppHandler, terminateAppHandler, restartAppHandler, resetAppDataHandler } from './tools/app.js';
11
+ import { getLogsHandler, startLogStreamHandler, readLogStreamHandler, stopLogStreamHandler } from './tools/logs.js';
12
+ import { listDevicesHandler } from './tools/devices.js';
13
+ import { captureScreenshotHandler } from './tools/screenshot.js';
14
+ import { getUITreeHandler, getCurrentScreenHandler, waitForElementHandler, tapHandler, swipeHandler, typeTextHandler, pressBackHandler } from './tools/ui.js';
9
15
  const androidObserve = new AndroidObserve();
10
16
  const androidInteract = new AndroidInteract();
11
17
  const iosObserve = new iOSObserve();
@@ -116,6 +122,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
116
122
  required: ["platform", "appId"]
117
123
  }
118
124
  },
125
+ {
126
+ name: "install_app",
127
+ description: "Install an app on Android or iOS. Accepts a built binary (apk/.ipa/.app) or a project directory to build then install.",
128
+ inputSchema: {
129
+ type: "object",
130
+ properties: {
131
+ platform: { type: "string", enum: ["android", "ios"], description: "Optional. If omitted the server will attempt to detect platform from appPath/project files." },
132
+ appPath: { type: "string", description: "Path to APK, .app, .ipa, or project directory" },
133
+ deviceId: { type: "string", description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected." }
134
+ },
135
+ required: ["appPath"]
136
+ }
137
+ },
119
138
  {
120
139
  name: "get_logs",
121
140
  description: "Get recent logs from Android or iOS simulator. Returns device metadata and the log output.",
@@ -142,6 +161,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
142
161
  required: ["platform"]
143
162
  }
144
163
  },
164
+ {
165
+ name: "list_devices",
166
+ description: "List connected devices and their metadata (android + ios).",
167
+ inputSchema: {
168
+ type: "object",
169
+ properties: {
170
+ platform: { type: "string", enum: ["android", "ios"] }
171
+ }
172
+ }
173
+ },
145
174
  {
146
175
  name: "capture_screenshot",
147
176
  description: "Capture a screenshot from an Android device or iOS simulator. Returns device metadata and the screenshot image.",
@@ -160,6 +189,41 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
160
189
  required: ["platform"]
161
190
  }
162
191
  },
192
+ {
193
+ name: "start_log_stream",
194
+ 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.",
195
+ inputSchema: {
196
+ type: "object",
197
+ properties: {
198
+ platform: { type: "string", enum: ["android", "ios"], default: "android" },
199
+ packageName: { type: "string", description: "Android package name or iOS bundle id" },
200
+ level: { type: "string", enum: ["error", "warn", "info", "debug"], default: "error" },
201
+ deviceId: { type: "string", description: "Device Serial (Android) or UDID (iOS). Defaults to connected/booted device." },
202
+ sessionId: { type: "string", description: "Session identifier for the log stream" }
203
+ },
204
+ required: ["packageName"]
205
+ }
206
+ },
207
+ {
208
+ name: "read_log_stream",
209
+ description: "Read accumulated log stream entries for the active session.",
210
+ inputSchema: {
211
+ type: "object",
212
+ properties: {
213
+ sessionId: { type: "string" }
214
+ }
215
+ }
216
+ },
217
+ {
218
+ name: "stop_log_stream",
219
+ description: "Stop an active log stream for the session.",
220
+ inputSchema: {
221
+ type: "object",
222
+ properties: {
223
+ sessionId: { type: "string" }
224
+ }
225
+ }
226
+ },
163
227
  {
164
228
  name: "get_ui_tree",
165
229
  description: "Get the current UI hierarchy from an Android device or iOS simulator. Returns a structured JSON representation of the screen content.",
@@ -319,249 +383,117 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
319
383
  try {
320
384
  if (name === "start_app") {
321
385
  const { platform, appId, deviceId } = args;
322
- let appStarted;
323
- let launchTimeMs;
324
- let deviceInfo;
325
- if (platform === "android") {
326
- const result = await androidInteract.startApp(appId, deviceId);
327
- appStarted = result.appStarted;
328
- launchTimeMs = result.launchTimeMs;
329
- deviceInfo = result.device;
330
- }
331
- else {
332
- const result = await iosInteract.startApp(appId, deviceId);
333
- appStarted = result.appStarted;
334
- launchTimeMs = result.launchTimeMs;
335
- deviceInfo = result.device;
336
- }
386
+ const result = await startAppHandler({ platform, appId, deviceId });
337
387
  const response = {
338
- device: deviceInfo,
339
- appStarted,
340
- launchTimeMs
388
+ device: result.device,
389
+ appStarted: result.appStarted,
390
+ launchTimeMs: result.launchTimeMs
341
391
  };
342
392
  return wrapResponse(response);
343
393
  }
344
394
  if (name === "terminate_app") {
345
395
  const { platform, appId, deviceId } = args;
346
- let appTerminated;
347
- let deviceInfo;
348
- if (platform === "android") {
349
- const result = await androidInteract.terminateApp(appId, deviceId);
350
- appTerminated = result.appTerminated;
351
- deviceInfo = result.device;
352
- }
353
- else {
354
- const result = await iosInteract.terminateApp(appId, deviceId);
355
- appTerminated = result.appTerminated;
356
- deviceInfo = result.device;
357
- }
358
- const response = {
359
- device: deviceInfo,
360
- appTerminated
361
- };
396
+ const result = await terminateAppHandler({ platform, appId, deviceId });
397
+ const response = { device: result.device, appTerminated: result.appTerminated };
362
398
  return wrapResponse(response);
363
399
  }
364
400
  if (name === "restart_app") {
365
401
  const { platform, appId, deviceId } = args;
366
- let appRestarted;
367
- let launchTimeMs;
368
- let deviceInfo;
369
- if (platform === "android") {
370
- const result = await androidInteract.restartApp(appId, deviceId);
371
- appRestarted = result.appRestarted;
372
- launchTimeMs = result.launchTimeMs;
373
- deviceInfo = result.device;
374
- }
375
- else {
376
- const result = await iosInteract.restartApp(appId, deviceId);
377
- appRestarted = result.appRestarted;
378
- launchTimeMs = result.launchTimeMs;
379
- deviceInfo = result.device;
380
- }
381
- const response = {
382
- device: deviceInfo,
383
- appRestarted,
384
- launchTimeMs
385
- };
402
+ const result = await restartAppHandler({ platform, appId, deviceId });
403
+ const response = { device: result.device, appRestarted: result.appRestarted, launchTimeMs: result.launchTimeMs };
386
404
  return wrapResponse(response);
387
405
  }
388
406
  if (name === "reset_app_data") {
389
407
  const { platform, appId, deviceId } = args;
390
- let dataCleared;
391
- let deviceInfo;
392
- if (platform === "android") {
393
- const result = await androidInteract.resetAppData(appId, deviceId);
394
- dataCleared = result.dataCleared;
395
- deviceInfo = result.device;
396
- }
397
- else {
398
- const result = await iosInteract.resetAppData(appId, deviceId);
399
- dataCleared = result.dataCleared;
400
- deviceInfo = result.device;
401
- }
408
+ const result = await resetAppDataHandler({ platform, appId, deviceId });
409
+ const response = { device: result.device, dataCleared: result.dataCleared };
410
+ return wrapResponse(response);
411
+ }
412
+ if (name === "install_app") {
413
+ const { platform, appPath, deviceId } = args;
414
+ const result = await installAppHandler({ platform, appPath, deviceId });
402
415
  const response = {
403
- device: deviceInfo,
404
- dataCleared
416
+ device: result.device,
417
+ installed: result.installed,
418
+ output: result.output,
419
+ error: result.error
405
420
  };
406
421
  return wrapResponse(response);
407
422
  }
408
423
  if (name === "get_logs") {
409
424
  const { platform, appId, deviceId, lines } = args;
410
- let logs;
411
- let deviceInfo;
412
- if (platform === "android") {
413
- deviceInfo = await androidObserve.getDeviceMetadata(appId || "", deviceId);
414
- const response = await androidObserve.getLogs(appId, lines ?? 200, deviceId);
415
- logs = Array.isArray(response.logs) ? response.logs : [];
416
- }
417
- else {
418
- deviceInfo = await iosObserve.getDeviceMetadata(deviceId);
419
- const response = await iosObserve.getLogs(appId, deviceId);
420
- logs = Array.isArray(response.logs) ? response.logs : [];
421
- }
422
- // Filter crash lines (e.g. lines containing 'FATAL EXCEPTION') for internal or AI use
423
- const crashLines = logs.filter(line => line.includes('FATAL EXCEPTION'));
424
- // Return device metadata plus logs
425
+ const res = await getLogsHandler({ platform, appId, deviceId, lines });
425
426
  return {
426
427
  content: [
427
- {
428
- type: "text",
429
- text: JSON.stringify({
430
- device: deviceInfo,
431
- result: {
432
- lines: logs.length,
433
- crashLines: crashLines.length > 0 ? crashLines : undefined
434
- }
435
- }, null, 2)
436
- },
437
- {
438
- type: "text",
439
- text: logs.join("\n")
440
- }
428
+ { type: 'text', text: JSON.stringify({ device: res.device, result: { lines: res.logs.length, crashLines: res.crashLines.length > 0 ? res.crashLines : undefined } }, null, 2) },
429
+ { type: 'text', text: (res.logs || []).join('\n') }
441
430
  ]
442
431
  };
443
432
  }
433
+ if (name === "list_devices") {
434
+ const { platform, appId } = (args || {});
435
+ const res = await listDevicesHandler({ platform, appId });
436
+ return wrapResponse(res);
437
+ }
444
438
  if (name === "capture_screenshot") {
445
439
  const { platform, deviceId } = args;
446
- let screenshot;
447
- let resolution;
448
- let deviceInfo;
449
- if (platform === "android") {
450
- deviceInfo = await androidObserve.getDeviceMetadata("", deviceId);
451
- const result = await androidObserve.captureScreen(deviceId);
452
- screenshot = result.screenshot;
453
- resolution = result.resolution;
454
- }
455
- else {
456
- deviceInfo = await iosObserve.getDeviceMetadata(deviceId);
457
- const result = await iosObserve.captureScreenshot(deviceId);
458
- screenshot = result.screenshot;
459
- resolution = result.resolution;
460
- }
440
+ const res = await captureScreenshotHandler({ platform, deviceId });
461
441
  return {
462
442
  content: [
463
- {
464
- type: "text",
465
- text: JSON.stringify({
466
- device: deviceInfo,
467
- result: {
468
- resolution
469
- }
470
- }, null, 2)
471
- },
472
- {
473
- type: "image",
474
- data: screenshot,
475
- mimeType: "image/png"
476
- }
443
+ { type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: res.resolution } }, null, 2) },
444
+ { type: 'image', data: res.screenshot, mimeType: 'image/png' }
477
445
  ]
478
446
  };
479
447
  }
480
448
  if (name === "get_ui_tree") {
481
449
  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);
450
+ const res = await getUITreeHandler({ platform, deviceId });
451
+ return wrapResponse(res);
493
452
  }
494
453
  if (name === "get_current_screen") {
495
454
  const { deviceId } = (args || {});
496
- const result = await androidObserve.getCurrentScreen(deviceId);
497
- return wrapResponse(result);
455
+ const res = await getCurrentScreenHandler({ deviceId });
456
+ return wrapResponse(res);
498
457
  }
499
458
  if (name === "wait_for_element") {
500
459
  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);
460
+ const res = await waitForElementHandler({ platform, text, timeout, deviceId });
461
+ return wrapResponse(res);
510
462
  }
511
463
  if (name === "tap") {
512
464
  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);
465
+ const res = await tapHandler({ platform, x, y, deviceId });
466
+ return wrapResponse(res);
526
467
  }
527
468
  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);
469
+ const { x1, y1, x2, y2, duration, deviceId } = (args || {});
470
+ const res = await swipeHandler({ x1, y1, x2, y2, duration, deviceId });
471
+ return wrapResponse(res);
541
472
  }
542
473
  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);
474
+ const { text, deviceId } = (args || {});
475
+ const res = await typeTextHandler({ text, deviceId });
476
+ return wrapResponse(res);
556
477
  }
557
478
  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);
479
+ const { deviceId } = (args || {});
480
+ const res = await pressBackHandler({ deviceId });
481
+ return wrapResponse(res);
482
+ }
483
+ if (name === 'start_log_stream') {
484
+ const { platform, packageName, level, sessionId, deviceId } = args;
485
+ const res = await startLogStreamHandler({ platform, packageName, level, sessionId, deviceId });
486
+ return wrapResponse(res);
487
+ }
488
+ if (name === 'read_log_stream') {
489
+ const { platform, sessionId, limit, since } = args;
490
+ const res = await readLogStreamHandler({ platform, sessionId, limit, since });
491
+ return wrapResponse(res);
492
+ }
493
+ if (name === 'stop_log_stream') {
494
+ const { platform, sessionId } = (args || {});
495
+ const res = await stopLogStreamHandler({ platform, sessionId });
496
+ return wrapResponse(res);
565
497
  }
566
498
  }
567
499
  catch (error) {
@@ -0,0 +1,45 @@
1
+ import { resolveTargetDevice } from '../resolve-device.js';
2
+ import { AndroidInteract } from '../android/interact.js';
3
+ import { iOSInteract } from '../ios/interact.js';
4
+ const androidInteract = new AndroidInteract();
5
+ const iosInteract = new iOSInteract();
6
+ export async function startAppHandler({ platform, appId, deviceId }) {
7
+ if (platform === 'android') {
8
+ const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
9
+ return await androidInteract.startApp(appId, resolved.id);
10
+ }
11
+ else {
12
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
13
+ return await iosInteract.startApp(appId, resolved.id);
14
+ }
15
+ }
16
+ export async function terminateAppHandler({ platform, appId, deviceId }) {
17
+ if (platform === 'android') {
18
+ const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
19
+ return await androidInteract.terminateApp(appId, resolved.id);
20
+ }
21
+ else {
22
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
23
+ return await iosInteract.terminateApp(appId, resolved.id);
24
+ }
25
+ }
26
+ export async function restartAppHandler({ platform, appId, deviceId }) {
27
+ if (platform === 'android') {
28
+ const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
29
+ return await androidInteract.restartApp(appId, resolved.id);
30
+ }
31
+ else {
32
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
33
+ return await iosInteract.restartApp(appId, resolved.id);
34
+ }
35
+ }
36
+ export async function resetAppDataHandler({ platform, appId, deviceId }) {
37
+ if (platform === 'android') {
38
+ const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId });
39
+ return await androidInteract.resetAppData(appId, resolved.id);
40
+ }
41
+ else {
42
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId });
43
+ return await iosInteract.resetAppData(appId, resolved.id);
44
+ }
45
+ }