mobile-debug-mcp 0.8.0 → 0.10.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 (48) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.cjs +18 -0
  3. package/.github/workflows/.gitkeep +0 -0
  4. package/.github/workflows/ci.yml +63 -0
  5. package/README.md +13 -534
  6. package/dist/android/interact.js +109 -3
  7. package/dist/android/observe.js +3 -3
  8. package/dist/android/utils.js +61 -21
  9. package/dist/ios/interact.js +89 -22
  10. package/dist/ios/observe.js +4 -4
  11. package/dist/ios/utils.js +8 -8
  12. package/dist/server.js +59 -295
  13. package/dist/tools/app.js +45 -0
  14. package/dist/tools/devices.js +5 -0
  15. package/dist/tools/install.js +47 -0
  16. package/dist/tools/interact.js +89 -0
  17. package/dist/tools/logs.js +62 -0
  18. package/dist/tools/observe.js +126 -0
  19. package/dist/tools/screenshot.js +17 -0
  20. package/dist/tools/ui.js +57 -0
  21. package/dist/utils/index.js +1 -0
  22. package/dist/utils/java.js +76 -0
  23. package/docs/CHANGELOG.md +24 -0
  24. package/docs/TOOLS.md +272 -0
  25. package/eslint.config.cjs +36 -0
  26. package/eslint.config.js +60 -0
  27. package/package.json +8 -2
  28. package/src/android/interact.ts +109 -3
  29. package/src/android/observe.ts +3 -3
  30. package/src/android/utils.ts +68 -21
  31. package/src/ios/interact.ts +91 -26
  32. package/src/ios/observe.ts +4 -4
  33. package/src/ios/utils.ts +8 -8
  34. package/src/server.ts +74 -394
  35. package/src/tools/interact.ts +84 -0
  36. package/src/tools/observe.ts +132 -0
  37. package/src/utils/index.ts +1 -0
  38. package/src/utils/java.ts +69 -0
  39. package/test/integration/install.integration.ts +64 -0
  40. package/test/integration/run-install-android.ts +21 -0
  41. package/test/integration/run-install-ios.ts +21 -0
  42. package/test/integration/smoke-test.ts +1 -1
  43. package/test/integration/test-dist.ts +41 -0
  44. package/test/integration/test-ui-tree.ts +1 -1
  45. package/test/integration/wait_for_element_real.ts +1 -1
  46. package/test/unit/detect-java.test.ts +22 -0
  47. package/test/unit/index.ts +1 -0
  48. package/test/unit/install.test.ts +76 -0
package/src/ios/utils.ts CHANGED
@@ -125,7 +125,7 @@ export async function getIOSDeviceMetadata(deviceId: string = "booted"): Promise
125
125
  }
126
126
  }
127
127
  resolve(fallback)
128
- } catch (error) {
128
+ } catch {
129
129
  resolve(fallback)
130
130
  }
131
131
  })
@@ -169,7 +169,7 @@ export async function listIOSDevices(appId?: string): Promise<DeviceInfo[]> {
169
169
  }
170
170
 
171
171
  Promise.all(checks).then(() => resolve(out)).catch(() => resolve(out))
172
- } catch (e) {
172
+ } catch {
173
173
  resolve([])
174
174
  }
175
175
  })
@@ -192,14 +192,14 @@ export function _clearIOSActiveLogStream(sessionId: string) {
192
192
  iosActiveLogStreams.delete(sessionId)
193
193
  }
194
194
 
195
- export async function startIOSLogStream(bundleId: string, level: 'error' | 'warn' | 'info' | 'debug' = 'error', deviceId: string = 'booted', sessionId: string = 'default') : Promise<{ success: boolean; stream_started?: boolean; error?: string }> {
195
+ export async function startIOSLogStream(bundleId: string, deviceId: string = 'booted', sessionId: string = 'default') : Promise<{ success: boolean; stream_started?: boolean; error?: string }> {
196
196
  try {
197
197
  // Build predicate to filter by process or subsystem
198
198
  const predicate = `process == "${bundleId}" or subsystem contains "${bundleId}"`
199
199
 
200
200
  // Prevent multiple streams per session
201
201
  if (iosActiveLogStreams.has(sessionId)) {
202
- try { iosActiveLogStreams.get(sessionId)!.proc.kill() } catch (e) {}
202
+ try { iosActiveLogStreams.get(sessionId)!.proc.kill() } catch {}
203
203
  iosActiveLogStreams.delete(sessionId)
204
204
  }
205
205
 
@@ -231,14 +231,14 @@ export async function startIOSLogStream(bundleId: string, level: 'error' | 'warn
231
231
  }
232
232
  })
233
233
 
234
- proc.on('close', (code) => {
234
+ proc.on('close', () => {
235
235
  stream.end()
236
236
  iosActiveLogStreams.delete(sessionId)
237
237
  })
238
238
 
239
239
  iosActiveLogStreams.set(sessionId, { proc, file })
240
240
  return { success: true, stream_started: true }
241
- } catch (err) {
241
+ } catch {
242
242
  return { success: false, error: 'log_stream_start_failed' }
243
243
  }
244
244
  }
@@ -246,7 +246,7 @@ export async function startIOSLogStream(bundleId: string, level: 'error' | 'warn
246
246
  export async function stopIOSLogStream(sessionId: string = 'default'): Promise<{ success: boolean }> {
247
247
  const entry = iosActiveLogStreams.get(sessionId)
248
248
  if (!entry) return { success: true }
249
- try { entry.proc.kill() } catch (e) {}
249
+ try { entry.proc.kill() } catch {}
250
250
  iosActiveLogStreams.delete(sessionId)
251
251
  return { success: true }
252
252
  }
@@ -284,7 +284,7 @@ export async function readIOSLogStreamLines(sessionId: string = 'default', limit
284
284
  const crashEntry = entries.find(e => e.crash)
285
285
  const crash_summary = crashEntry ? { crash_detected: true, exception: crashEntry.exception, sample: crashEntry.message } : { crash_detected: false }
286
286
  return { entries, crash_summary }
287
- } catch (e) {
287
+ } catch {
288
288
  return { entries: [], crash_summary: { crash_detected: false } }
289
289
  }
290
290
  }
package/src/server.ts CHANGED
@@ -8,32 +8,19 @@ import {
8
8
 
9
9
  import {
10
10
  StartAppResponse,
11
- DeviceInfo,
12
11
  TerminateAppResponse,
13
12
  RestartAppResponse,
14
13
  ResetAppDataResponse,
15
- GetUITreeResponse,
16
- GetCurrentScreenResponse,
17
- WaitForElementResponse,
18
- TapResponse,
19
- SwipeResponse,
20
- TypeTextResponse,
21
- PressBackResponse,
22
14
  InstallAppResponse
23
15
  } from "./types.js"
24
16
 
25
- import { AndroidObserve } from "./android/observe.js"
26
- import { AndroidInteract } from "./android/interact.js"
27
- import { iOSObserve } from "./ios/observe.js"
28
- import { iOSInteract } from "./ios/interact.js"
29
- import { resolveTargetDevice, listDevices } from "./resolve-device.js"
30
- import { startAndroidLogStream, readLogStreamLines, stopAndroidLogStream } from "./android/utils.js"
31
- import { startIOSLogStream, readIOSLogStreamLines, stopIOSLogStream } from "./ios/utils.js"
17
+ import { ToolsInteract } from './tools/interact.js'
18
+ import { ToolsObserve } from './tools/observe.js'
19
+ import { AndroidInteract } from './android/interact.js'
20
+ import { iOSInteract } from './ios/interact.js'
21
+ import { AndroidObserve } from './android/observe.js'
22
+ import { iOSObserve } from './ios/observe.js'
32
23
 
33
- const androidObserve = new AndroidObserve()
34
- const androidInteract = new AndroidInteract()
35
- const iosObserve = new iOSObserve()
36
- const iosInteract = new iOSInteract()
37
24
 
38
25
  const server = new Server(
39
26
  {
@@ -148,15 +135,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
148
135
  },
149
136
  {
150
137
  name: "install_app",
151
- description: "Install an app on Android (apk) or iOS simulator/device (ipa/.app).",
138
+ description: "Install an app on Android or iOS. Accepts a built binary (apk/.ipa/.app) or a project directory to build then install.",
152
139
  inputSchema: {
153
140
  type: "object",
154
141
  properties: {
155
- platform: { type: "string", enum: ["android", "ios"] },
156
- appPath: { type: "string", description: "Path to APK (Android) or .app/.ipa (iOS) on the host machine" },
142
+ platform: { type: "string", enum: ["android", "ios"], description: "Optional. If omitted the server will attempt to detect platform from appPath/project files." },
143
+ appPath: { type: "string", description: "Path to APK, .app, .ipa, or project directory" },
157
144
  deviceId: { type: "string", description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected." }
158
145
  },
159
- required: ["platform", "appPath"]
146
+ required: ["appPath"]
160
147
  }
161
148
  },
162
149
  {
@@ -409,444 +396,137 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
409
396
 
410
397
  try {
411
398
  if (name === "start_app") {
412
- const { platform, appId, deviceId } = args as {
413
- platform: "android" | "ios"
414
- appId: string
415
- deviceId?: string
416
- }
417
-
418
- let appStarted: boolean
419
- let launchTimeMs: number
420
- let deviceInfo: DeviceInfo
421
-
422
- if (platform === "android") {
423
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
424
- const result = await androidInteract.startApp(appId, resolved.id)
425
- appStarted = result.appStarted
426
- launchTimeMs = result.launchTimeMs
427
- deviceInfo = result.device
428
- } else {
429
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
430
- const result = await iosInteract.startApp(appId, resolved.id)
431
- appStarted = result.appStarted
432
- launchTimeMs = result.launchTimeMs
433
- deviceInfo = result.device
434
- }
435
-
399
+ const { platform, appId, deviceId } = args as any
400
+ const res = await (platform === 'android' ? new AndroidInteract().startApp(appId, deviceId) : new iOSInteract().startApp(appId, deviceId))
436
401
  const response: StartAppResponse = {
437
- device: deviceInfo,
438
- appStarted,
439
- launchTimeMs
402
+ device: res.device,
403
+ appStarted: res.appStarted,
404
+ launchTimeMs: res.launchTimeMs
440
405
  }
441
-
442
406
  return wrapResponse(response)
443
407
  }
444
408
 
445
409
  if (name === "terminate_app") {
446
- const { platform, appId, deviceId } = args as {
447
- platform: "android" | "ios"
448
- appId: string
449
- deviceId?: string
450
- }
451
-
452
- let appTerminated: boolean
453
- let deviceInfo: DeviceInfo
454
-
455
- if (platform === "android") {
456
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
457
- const result = await androidInteract.terminateApp(appId, resolved.id)
458
- appTerminated = result.appTerminated
459
- deviceInfo = result.device
460
- } else {
461
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
462
- const result = await iosInteract.terminateApp(appId, resolved.id)
463
- appTerminated = result.appTerminated
464
- deviceInfo = result.device
465
- }
466
-
467
- const response: TerminateAppResponse = {
468
- device: deviceInfo,
469
- appTerminated
470
- }
471
-
410
+ const { platform, appId, deviceId } = args as any
411
+ const res = await (platform === 'android' ? new AndroidInteract().terminateApp(appId, deviceId) : new iOSInteract().terminateApp(appId, deviceId))
412
+ const response: TerminateAppResponse = { device: res.device, appTerminated: res.appTerminated }
472
413
  return wrapResponse(response)
473
414
  }
474
415
 
475
416
  if (name === "restart_app") {
476
- const { platform, appId, deviceId } = args as {
477
- platform: "android" | "ios"
478
- appId: string
479
- deviceId?: string
480
- }
481
-
482
- let appRestarted: boolean
483
- let launchTimeMs: number
484
- let deviceInfo: DeviceInfo
485
-
486
- if (platform === "android") {
487
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
488
- const result = await androidInteract.restartApp(appId, resolved.id)
489
- appRestarted = result.appRestarted
490
- launchTimeMs = result.launchTimeMs
491
- deviceInfo = result.device
492
- } else {
493
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
494
- const result = await iosInteract.restartApp(appId, resolved.id)
495
- appRestarted = result.appRestarted
496
- launchTimeMs = result.launchTimeMs
497
- deviceInfo = result.device
498
- }
499
-
500
- const response: RestartAppResponse = {
501
- device: deviceInfo,
502
- appRestarted,
503
- launchTimeMs
504
- }
505
-
417
+ const { platform, appId, deviceId } = args as any
418
+ const res = await (platform === 'android' ? new AndroidInteract().restartApp(appId, deviceId) : new iOSInteract().restartApp(appId, deviceId))
419
+ const response: RestartAppResponse = { device: res.device, appRestarted: res.appRestarted, launchTimeMs: res.launchTimeMs }
506
420
  return wrapResponse(response)
507
421
  }
508
422
 
509
423
  if (name === "reset_app_data") {
510
- const { platform, appId, deviceId } = args as {
511
- platform: "android" | "ios"
512
- appId: string
513
- deviceId?: string
514
- }
515
-
516
- let dataCleared: boolean
517
- let deviceInfo: DeviceInfo
518
-
519
- if (platform === "android") {
520
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
521
- const result = await androidInteract.resetAppData(appId, resolved.id)
522
- dataCleared = result.dataCleared
523
- deviceInfo = result.device
524
- } else {
525
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
526
- const result = await iosInteract.resetAppData(appId, resolved.id)
527
- dataCleared = result.dataCleared
528
- deviceInfo = result.device
529
- }
530
-
531
- const response: ResetAppDataResponse = {
532
- device: deviceInfo,
533
- dataCleared
534
- }
535
-
424
+ const { platform, appId, deviceId } = args as any
425
+ const res = await (platform === 'android' ? new AndroidInteract().resetAppData(appId, deviceId) : new iOSInteract().resetAppData(appId, deviceId))
426
+ const response: ResetAppDataResponse = { device: res.device, dataCleared: res.dataCleared }
536
427
  return wrapResponse(response)
537
428
  }
538
429
 
539
430
  if (name === "install_app") {
540
- const { platform, appPath, deviceId } = args as {
541
- platform: "android" | "ios"
542
- appPath: string
543
- deviceId?: string
544
- }
545
-
546
- let installed: boolean
547
- let output: string | undefined
548
- let deviceInfo: DeviceInfo
549
- let errorMsg: string | undefined
550
-
551
- if (platform === "android") {
552
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
553
- const result = await androidInteract.installApp(appPath, resolved.id)
554
- installed = result.installed
555
- output = (result as any).output
556
- deviceInfo = result.device
557
- errorMsg = (result as any).error
558
- } else {
559
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
560
- const result = await iosInteract.installApp(appPath, resolved.id)
561
- installed = result.installed
562
- output = (result as any).output
563
- deviceInfo = result.device
564
- errorMsg = (result as any).error
565
- }
566
-
567
- const response: InstallAppResponse = {
568
- device: deviceInfo,
569
- installed,
570
- output,
571
- error: errorMsg
431
+ const { platform, appPath, deviceId } = args as any
432
+ const res = await ToolsInteract.installAppHandler({ platform, appPath, deviceId })
433
+ const response: InstallAppResponse = {
434
+ device: res.device,
435
+ installed: res.installed,
436
+ output: (res as any).output,
437
+ error: (res as any).error
438
+ }
439
+ return wrapResponse(response)
572
440
  }
573
441
 
574
- return wrapResponse(response)
575
- }
576
442
 
577
443
  if (name === "get_logs") {
578
- const { platform, appId, deviceId, lines } = args as {
579
- platform: "android" | "ios"
580
- appId?: string
581
- deviceId?: string
582
- lines?: number
583
- }
584
-
585
- let logs: string[]
586
- let deviceInfo: DeviceInfo
587
-
588
- if (platform === "android") {
589
- // Resolve an explicit target device when multiple are attached
590
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
591
- deviceInfo = resolved
592
- const response = await androidObserve.getLogs(appId, lines ?? 200, resolved.id)
593
- logs = Array.isArray(response.logs) ? response.logs : []
594
- } else {
595
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
596
- deviceInfo = resolved
597
- const response = await iosObserve.getLogs(appId, resolved.id)
598
- logs = Array.isArray(response.logs) ? response.logs : []
599
- }
600
-
601
- // Filter crash lines (e.g. lines containing 'FATAL EXCEPTION') for internal or AI use
602
- const crashLines = logs.filter(line => line.includes('FATAL EXCEPTION'))
603
-
604
- // Return device metadata plus logs
444
+ const { platform, appId, deviceId, lines } = args as any
445
+ const res = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, lines })
605
446
  return {
606
447
  content: [
607
- {
608
- type: "text",
609
- text: JSON.stringify({
610
- device: deviceInfo,
611
- result: {
612
- lines: logs.length,
613
- crashLines: crashLines.length > 0 ? crashLines : undefined
614
- }
615
- }, null, 2)
616
- },
617
- {
618
- type: "text",
619
- text: logs.join("\n")
620
- }
448
+ { type: 'text', text: JSON.stringify({ device: res.device, result: { lines: res.logs.length, crashLines: res.crashLines.length > 0 ? res.crashLines : undefined } }, null, 2) },
449
+ { type: 'text', text: (res.logs || []).join('\n') }
621
450
  ]
622
451
  }
623
452
  }
624
453
 
625
454
  if (name === "list_devices") {
626
- const { platform, appId } = (args || {}) as { platform?: "android" | "ios"; appId?: string }
627
- const devices = await listDevices(platform, appId)
628
- return wrapResponse({ devices })
455
+ const { platform, appId } = (args || {}) as any
456
+ const res = await ToolsObserve.listDevicesHandler({ platform, appId })
457
+ return wrapResponse(res)
629
458
  }
630
459
 
631
460
 
632
461
  if (name === "capture_screenshot") {
633
- const { platform, deviceId } = args as { platform: "android" | "ios"; deviceId?: string }
634
-
635
- let screenshot: string
636
- let resolution: { width: number; height: number }
637
- let deviceInfo: DeviceInfo
638
-
639
- if (platform === "android") {
640
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
641
- deviceInfo = resolved
642
- const result = await androidObserve.captureScreen(resolved.id)
643
- screenshot = result.screenshot
644
- resolution = result.resolution
645
- } else {
646
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
647
- deviceInfo = resolved
648
- const result = await iosObserve.captureScreenshot(resolved.id)
649
- screenshot = result.screenshot
650
- resolution = result.resolution
651
- }
652
-
462
+ const { platform, deviceId } = args as any
463
+ const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId })
653
464
  return {
654
465
  content: [
655
- {
656
- type: "text",
657
- text: JSON.stringify({
658
- device: deviceInfo,
659
- result: {
660
- resolution
661
- }
662
- }, null, 2)
663
- },
664
- {
665
- type: "image",
666
- data: screenshot,
667
- mimeType: "image/png"
668
- }
466
+ { type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: (res as any).resolution } }, null, 2) },
467
+ { type: 'image', data: (res as any).screenshot, mimeType: 'image/png' }
669
468
  ]
670
469
  }
671
470
  }
672
471
 
673
472
  if (name === "get_ui_tree") {
674
- const { platform, deviceId } = args as { platform: "android" | "ios", deviceId?: string }
675
-
676
- let result: GetUITreeResponse
677
- if (platform === "android") {
678
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
679
- result = await androidObserve.getUITree(resolved.id)
680
- } else if (platform === "ios") {
681
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
682
- result = await iosObserve.getUITree(resolved.id)
683
- } else {
684
- throw new Error(`Platform ${platform} not supported for get_ui_tree`)
685
- }
686
-
687
- return wrapResponse(result)
473
+ const { platform, deviceId } = args as any
474
+ const res = await (platform === 'android' ? new AndroidObserve().getUITree(deviceId) : new iOSObserve().getUITree(deviceId))
475
+ return wrapResponse(res)
688
476
  }
689
477
 
690
478
  if (name === "get_current_screen") {
691
- const { deviceId } = (args || {}) as { deviceId?: string }
692
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
693
- const result = await androidObserve.getCurrentScreen(resolved.id)
694
- return wrapResponse(result)
479
+ const { deviceId } = (args || {}) as any
480
+ const res = await new AndroidObserve().getCurrentScreen(deviceId)
481
+ return wrapResponse(res)
695
482
  }
696
483
 
697
484
  if (name === "wait_for_element") {
698
- const { platform, text, timeout, deviceId } = (args || {}) as {
699
- platform: "android" | "ios"
700
- text: string
701
- timeout?: number
702
- deviceId?: string
703
- }
704
-
705
- const effectiveTimeout = timeout ?? 10000;
706
-
707
- let result: WaitForElementResponse;
708
- if (platform === "android") {
709
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
710
- result = await androidInteract.waitForElement(text, effectiveTimeout, resolved.id)
711
- } else {
712
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
713
- result = await iosInteract.waitForElement(text, effectiveTimeout, resolved.id)
714
- }
715
- return wrapResponse(result)
485
+ const { platform, text, timeout, deviceId } = (args || {}) as any
486
+ const res = await (platform === 'android' ? new AndroidInteract().waitForElement(text, timeout, deviceId) : new iOSInteract().waitForElement(text, timeout, deviceId))
487
+ return wrapResponse(res)
716
488
  }
717
489
 
718
490
  if (name === "tap") {
719
- const { platform, x, y, deviceId } = (args || {}) as {
720
- platform?: "android" | "ios"
721
- x: number
722
- y: number
723
- deviceId?: string
724
- }
725
-
726
- const effectivePlatform = platform || "android";
727
-
728
- // Basic validation
729
- if (typeof x !== 'number' || typeof y !== 'number') {
730
- throw new Error("x and y coordinates are required and must be numbers");
731
- }
732
-
733
- let result: TapResponse;
734
- if (effectivePlatform === "android") {
735
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
736
- result = await androidInteract.tap(x, y, resolved.id)
737
- } else {
738
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
739
- result = await iosInteract.tap(x, y, resolved.id)
740
- }
741
- return wrapResponse(result)
491
+ const { platform, x, y, deviceId } = (args || {}) as any
492
+ const res = await (platform === 'android' ? new AndroidInteract().tap(x, y, deviceId) : new iOSInteract().tap(x, y, deviceId))
493
+ return wrapResponse(res)
742
494
  }
743
495
 
744
496
  if (name === "swipe") {
745
- const { platform, x1, y1, x2, y2, duration, deviceId } = (args || {}) as {
746
- platform?: "android"
747
- x1: number
748
- y1: number
749
- x2: number
750
- y2: number
751
- duration: number
752
- deviceId?: string
753
- }
754
-
755
- const effectivePlatform = platform || "android";
756
-
757
- if (typeof x1 !== 'number' || typeof y1 !== 'number' || typeof x2 !== 'number' || typeof y2 !== 'number' || typeof duration !== 'number') {
758
- throw new Error("x1, y1, x2, y2, and duration are required and must be numbers");
759
- }
760
-
761
- let result: SwipeResponse;
762
- if (effectivePlatform === "android") {
763
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
764
- result = await androidInteract.swipe(x1, y1, x2, y2, duration, resolved.id)
765
- } else {
766
- throw new Error(`Platform ${effectivePlatform} not supported for swipe`)
767
- }
768
- return wrapResponse(result)
497
+ const { x1, y1, x2, y2, duration, deviceId } = (args || {}) as any
498
+ const res = await new AndroidInteract().swipe(x1, y1, x2, y2, duration, deviceId)
499
+ return wrapResponse(res)
769
500
  }
770
501
 
771
502
  if (name === "type_text") {
772
- const { platform, text, deviceId } = (args || {}) as {
773
- platform?: "android"
774
- text: string
775
- deviceId?: string
776
- }
777
-
778
- const effectivePlatform = platform || "android";
779
-
780
- if (typeof text !== 'string') {
781
- throw new Error("text is required and must be a string");
782
- }
783
-
784
- let result: TypeTextResponse;
785
- if (effectivePlatform === "android") {
786
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
787
- result = await androidInteract.typeText(text, resolved.id)
788
- } else {
789
- throw new Error(`Platform ${effectivePlatform} not supported for type_text`)
790
- }
791
- return wrapResponse(result)
503
+ const { text, deviceId } = (args || {}) as any
504
+ const res = await new AndroidInteract().typeText(text, deviceId)
505
+ return wrapResponse(res)
792
506
  }
793
507
 
794
508
  if (name === "press_back") {
795
- const { platform, deviceId } = (args || {}) as {
796
- platform?: "android"
797
- deviceId?: string
798
- }
799
-
800
- const effectivePlatform = platform || "android";
801
-
802
- if (effectivePlatform !== "android") {
803
- throw new Error(`Platform ${effectivePlatform} not supported for press_back`)
804
- }
805
-
806
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
807
- const result = await androidInteract.pressBack(resolved.id)
808
- return wrapResponse(result)
509
+ const { deviceId } = (args || {}) as any
510
+ const res = await new AndroidInteract().pressBack(deviceId)
511
+ return wrapResponse(res)
809
512
  }
810
513
 
811
514
  if (name === 'start_log_stream') {
812
- const { platform, packageName, level, sessionId: argSession, deviceId } = args as { platform?: 'android' | 'ios'; packageName: string; level?: 'error' | 'warn' | 'info' | 'debug'; sessionId?: string; deviceId?: string }
813
- const sessionId = argSession || 'default'
814
- const effectivePlatform = platform || 'android'
815
- if (effectivePlatform === 'android') {
816
- const resolved = await resolveTargetDevice({ platform: 'android', appId: packageName, deviceId })
817
- const res = await startAndroidLogStream(packageName, level || 'error', resolved.id, sessionId)
818
- return wrapResponse(res)
819
- } else {
820
- const resolved = await resolveTargetDevice({ platform: 'ios', appId: packageName, deviceId })
821
- const res = await startIOSLogStream(packageName, level || 'error', resolved.id, sessionId)
822
- return wrapResponse(res)
823
- }
515
+ const { platform, packageName, level, sessionId, deviceId } = args as any
516
+ const res = await ToolsObserve.startLogStreamHandler({ platform, packageName, level, sessionId, deviceId })
517
+ return wrapResponse(res)
824
518
  }
825
519
 
826
520
  if (name === 'read_log_stream') {
827
- const { platform, sessionId: argSession, limit, since } = (args || {}) as { platform?: 'android' | 'ios'; sessionId?: string, limit?: number, since?: string }
828
- const sid = argSession || 'default'
829
- const effectivePlatform = platform || 'android'
830
- if (effectivePlatform === 'android') {
831
- const { entries, crash_summary } = await readLogStreamLines(sid, limit ?? 100, since)
832
- return wrapResponse({ entries, crash_summary })
833
- } else {
834
- const { entries, crash_summary } = await readIOSLogStreamLines(sid, limit ?? 100, since)
835
- return wrapResponse({ entries, crash_summary })
836
- }
521
+ const { platform, sessionId, limit, since } = args as any
522
+ const res = await ToolsObserve.readLogStreamHandler({ platform, sessionId, limit, since })
523
+ return wrapResponse(res)
837
524
  }
838
525
 
839
526
  if (name === 'stop_log_stream') {
840
- const { platform, sessionId: argSession } = (args || {}) as { platform?: 'android' | 'ios'; sessionId?: string }
841
- const sid = argSession || 'default'
842
- const effectivePlatform = platform || 'android'
843
- if (effectivePlatform === 'android') {
844
- const res = await stopAndroidLogStream(sid)
845
- return wrapResponse(res)
846
- } else {
847
- const res = await stopIOSLogStream(sid)
848
- return wrapResponse(res)
849
- }
527
+ const { platform, sessionId } = (args || {}) as any
528
+ const res = await ToolsObserve.stopLogStreamHandler({ platform, sessionId })
529
+ return wrapResponse(res)
850
530
  }
851
531
  } catch (error) {
852
532
  return {