mobile-debug-mcp 0.24.1 → 0.24.3

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.
@@ -185,9 +185,10 @@ export class ToolsInteract {
185
185
  return await interact.tap(x, y, resolved.id);
186
186
  }
187
187
  static async tapElementHandler({ elementId }) {
188
- const timestamp = Date.now();
188
+ const timestampMs = Date.now();
189
+ const timestamp = new Date(timestampMs).toISOString();
189
190
  const actionType = 'tap_element';
190
- const actionId = nextActionId(actionType, timestamp);
191
+ const actionId = nextActionId(actionType, timestampMs);
191
192
  const selector = { elementId };
192
193
  const resolved = ToolsInteract._resolvedUiElements.get(elementId);
193
194
  if (!resolved) {
@@ -225,6 +226,7 @@ export class ToolsInteract {
225
226
  action_id: actionId,
226
227
  timestamp,
227
228
  action_type: actionType,
229
+ ...(tree?.device ? { device: tree.device } : {}),
228
230
  target: {
229
231
  selector,
230
232
  resolved: resolvedTarget
@@ -437,6 +439,12 @@ export class ToolsInteract {
437
439
  }
438
440
  static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId }) {
439
441
  const overallStart = Date.now();
442
+ const requestedIndex = typeof match?.index === 'number' ? match.index : null;
443
+ const requested = {
444
+ selector: selector ?? {},
445
+ condition,
446
+ match: requestedIndex === null ? null : { index: requestedIndex }
447
+ };
440
448
  // Validate selector: require at least one non-empty field (text, resource_id, or accessibility_id)
441
449
  const hasText = typeof selector?.text === 'string' && selector.text.trim().length > 0;
442
450
  const hasResId = typeof selector?.resource_id === 'string' && selector.resource_id.trim().length > 0;
@@ -448,22 +456,27 @@ export class ToolsInteract {
448
456
  code: 'INVALID_SELECTOR',
449
457
  message: 'Selector must include at least one non-empty field: text, resource_id, or accessibility_id'
450
458
  },
451
- metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }
459
+ metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 },
460
+ requested,
461
+ observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null }
452
462
  };
453
463
  }
454
464
  // Validate condition
455
465
  if (!['exists', 'not_exists', 'visible', 'clickable'].includes(condition)) {
456
- return { status: 'timeout', error: { code: 'INVALID_CONDITION', message: `Unsupported condition: ${condition}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 } };
466
+ return { status: 'timeout', error: { code: 'INVALID_CONDITION', message: `Unsupported condition: ${condition}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }, requested, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } };
457
467
  }
458
468
  // Platform check
459
469
  if (platform && !['android', 'ios'].includes(platform)) {
460
- return { status: 'timeout', error: { code: 'PLATFORM_NOT_SUPPORTED', message: `Unsupported platform: ${platform}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 } };
470
+ return { status: 'timeout', error: { code: 'PLATFORM_NOT_SUPPORTED', message: `Unsupported platform: ${platform}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }, requested, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } };
461
471
  }
462
472
  const effectivePoll = Math.max(50, Math.min(poll_interval_ms || 300, 2000));
463
473
  const maxAttempts = (retry && retry.max_attempts) ? Math.max(1, retry.max_attempts) : 1;
464
474
  const backoff = (retry && retry.backoff_ms) ? Math.max(0, retry.backoff_ms) : 0;
465
475
  let attempts = 0;
466
476
  let totalPollCount = 0;
477
+ let lastMatchedCount = 0;
478
+ let lastMatchedElement = null;
479
+ let lastConditionSatisfied = false;
467
480
  // Precompute normalized selector values and helpers (constant across polls)
468
481
  const normalize = ToolsInteract._normalize;
469
482
  const containsFlag = !!selector?.contains;
@@ -573,18 +586,27 @@ export class ToolsInteract {
573
586
  else
574
587
  conditionMet = false;
575
588
  }
589
+ const resolvedPlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
590
+ const resolvedDeviceId = tree?.device?.id || deviceId;
591
+ lastMatchedCount = matchedCount;
592
+ lastConditionSatisfied = conditionMet;
593
+ lastMatchedElement = matchedElement ? ToolsInteract._buildResolvedElement(resolvedPlatform, resolvedDeviceId, matchedElement.el, matchedElement.idx) : null;
576
594
  if (conditionMet) {
577
595
  const now = Date.now();
578
596
  const latency_ms = now - overallStart;
579
- // Build element output per spec
580
- const resolvedPlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
581
- const resolvedDeviceId = tree?.device?.id || deviceId;
582
- const outEl = matchedElement ? ToolsInteract._buildResolvedElement(resolvedPlatform, resolvedDeviceId, matchedElement.el, matchedElement.idx) : null;
597
+ const outEl = lastMatchedElement;
583
598
  return {
584
599
  status: 'success',
585
600
  matched: matchedCount,
586
601
  element: outEl,
587
- metrics: { latency_ms, poll_count: totalPollCount, attempts }
602
+ metrics: { latency_ms, poll_count: totalPollCount, attempts },
603
+ requested,
604
+ observed: {
605
+ matched_count: matchedCount,
606
+ condition_satisfied: true,
607
+ selected_index: outEl?.index ?? null,
608
+ last_matched_element: outEl
609
+ }
588
610
  };
589
611
  }
590
612
  }
@@ -603,16 +625,38 @@ export class ToolsInteract {
603
625
  }
604
626
  // Final failure for this call
605
627
  const elapsed = Date.now() - overallStart;
628
+ const observed = {
629
+ matched_count: lastMatchedCount,
630
+ condition_satisfied: lastConditionSatisfied,
631
+ selected_index: lastMatchedElement?.index ?? null,
632
+ last_matched_element: lastMatchedElement
633
+ };
634
+ const matchNote = requestedIndex !== null && lastMatchedCount <= requestedIndex
635
+ ? ` requested match.index=${requestedIndex} but observed ${lastMatchedCount} match(es)`
636
+ : ` observed ${lastMatchedCount} match(es)`;
606
637
  return {
607
638
  status: 'timeout',
608
- error: { code: 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout` },
609
- metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts }
639
+ error: { code: 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout;${matchNote}` },
640
+ metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
641
+ requested,
642
+ observed
610
643
  };
611
644
  }
612
645
  }
613
646
  catch (err) {
614
647
  const elapsed = Date.now() - overallStart;
615
- return { status: 'timeout', error: { code: 'INTERNAL_ERROR', message: err instanceof Error ? err.message : String(err) }, metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts } };
648
+ return {
649
+ status: 'timeout',
650
+ error: { code: 'INTERNAL_ERROR', message: err instanceof Error ? err.message : String(err) },
651
+ metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
652
+ requested,
653
+ observed: {
654
+ matched_count: lastMatchedCount,
655
+ condition_satisfied: false,
656
+ selected_index: lastMatchedElement?.index ?? null,
657
+ last_matched_element: lastMatchedElement
658
+ }
659
+ };
616
660
  }
617
661
  }
618
662
  // Helper: normalize various log objects into plain message strings for comparison
@@ -635,10 +679,12 @@ export class ToolsInteract {
635
679
  static async waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs = 5000, pollIntervalMs = 300, deviceId }) {
636
680
  const start = Date.now();
637
681
  let lastFingerprint = null;
682
+ let lastActivity = null;
638
683
  while (Date.now() - start < timeoutMs) {
639
684
  try {
640
685
  const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
641
686
  const fp = res?.fingerprint ?? null;
687
+ lastActivity = res?.activity ?? lastActivity;
642
688
  if (fp === null || fp === undefined) {
643
689
  lastFingerprint = null;
644
690
  await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
@@ -651,8 +697,18 @@ export class ToolsInteract {
651
697
  try {
652
698
  const confirmRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
653
699
  const confirmFp = confirmRes?.fingerprint ?? null;
700
+ lastActivity = confirmRes?.activity ?? lastActivity;
654
701
  if (confirmFp === fp) {
655
- return { success: true, newFingerprint: fp, elapsedMs: Date.now() - start };
702
+ return {
703
+ success: true,
704
+ previousFingerprint,
705
+ newFingerprint: fp,
706
+ elapsedMs: Date.now() - start,
707
+ observed_screen: {
708
+ fingerprint: fp,
709
+ activity: lastActivity
710
+ }
711
+ };
656
712
  }
657
713
  lastFingerprint = confirmFp;
658
714
  continue;
@@ -668,7 +724,17 @@ export class ToolsInteract {
668
724
  }
669
725
  await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
670
726
  }
671
- return { success: false, reason: 'timeout', lastFingerprint, elapsedMs: Date.now() - start };
727
+ return {
728
+ success: false,
729
+ reason: 'timeout',
730
+ previousFingerprint,
731
+ lastFingerprint,
732
+ elapsedMs: Date.now() - start,
733
+ observed_screen: {
734
+ fingerprint: lastFingerprint,
735
+ activity: lastActivity
736
+ }
737
+ };
672
738
  }
673
739
  static async expectScreenHandler({ platform, fingerprint, screen, deviceId }) {
674
740
  const observedFingerprint = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
@@ -691,16 +757,26 @@ export class ToolsInteract {
691
757
  screen: screen ?? null
692
758
  };
693
759
  let success = false;
760
+ let basis = 'none';
761
+ let reason = 'No fingerprint or screen expectation provided';
694
762
  if (fingerprint) {
763
+ basis = 'fingerprint';
695
764
  success = observedScreen.fingerprint === fingerprint;
765
+ reason = success
766
+ ? `observed fingerprint matches expected fingerprint ${fingerprint}`
767
+ : `expected fingerprint ${fingerprint} but observed ${observedScreen.fingerprint ?? 'null'}`;
696
768
  }
697
769
  else if (screen) {
770
+ basis = 'screen';
698
771
  const candidates = new Set();
699
772
  if (observedScreen.screen)
700
773
  candidates.add(observedScreen.screen);
701
774
  if (observedScreenLabel)
702
775
  candidates.add(observedScreenLabel);
703
776
  success = candidates.has(screen);
777
+ reason = success
778
+ ? `observed screen matches expected screen ${screen}`
779
+ : `expected screen ${screen} but observed ${observedScreenLabel ?? observedScreen.screen ?? 'null'}`;
704
780
  }
705
781
  return {
706
782
  success,
@@ -709,7 +785,12 @@ export class ToolsInteract {
709
785
  screen: observedScreenLabel
710
786
  },
711
787
  expected_screen: expectedScreen,
712
- confidence: success ? 1 : 0
788
+ confidence: success ? 1 : 0,
789
+ comparison: {
790
+ basis,
791
+ matched: success,
792
+ reason
793
+ }
713
794
  };
714
795
  }
715
796
  static async expectElementVisibleHandler({ selector, element_id, timeout_ms = 5000, poll_interval_ms = 300, platform, deviceId }) {
@@ -726,6 +807,7 @@ export class ToolsInteract {
726
807
  success: true,
727
808
  selector,
728
809
  element_id: result.element.elementId ?? element_id ?? null,
810
+ expected_condition: 'visible',
729
811
  element: {
730
812
  elementId: result.element.elementId ?? null,
731
813
  text: result.element.text ?? null,
@@ -734,7 +816,23 @@ export class ToolsInteract {
734
816
  class: result.element.class ?? null,
735
817
  bounds: result.element.bounds ?? null,
736
818
  index: typeof result.element.index === 'number' ? result.element.index : null
737
- }
819
+ },
820
+ observed: {
821
+ status: result.status,
822
+ matched_count: typeof result.matched === 'number' ? result.matched : result?.observed?.matched_count ?? null,
823
+ condition_satisfied: true,
824
+ selected_index: typeof result.element.index === 'number' ? result.element.index : null,
825
+ last_matched_element: {
826
+ elementId: result.element.elementId ?? null,
827
+ text: result.element.text ?? null,
828
+ resource_id: result.element.resource_id ?? null,
829
+ accessibility_id: result.element.accessibility_id ?? null,
830
+ class: result.element.class ?? null,
831
+ bounds: result.element.bounds ?? null,
832
+ index: typeof result.element.index === 'number' ? result.element.index : null
833
+ }
834
+ },
835
+ reason: 'selector is visible'
738
836
  };
739
837
  }
740
838
  const errorCode = result?.error?.code === 'INTERNAL_ERROR' ? 'UNKNOWN' : 'TIMEOUT';
@@ -742,6 +840,15 @@ export class ToolsInteract {
742
840
  success: false,
743
841
  selector,
744
842
  element_id: element_id ?? null,
843
+ expected_condition: 'visible',
844
+ observed: {
845
+ status: result?.status,
846
+ matched_count: result?.observed?.matched_count,
847
+ condition_satisfied: result?.observed?.condition_satisfied ?? false,
848
+ selected_index: result?.observed?.selected_index ?? null,
849
+ last_matched_element: result?.observed?.last_matched_element ?? null
850
+ },
851
+ reason: result?.error?.message ?? 'selector is not visible',
745
852
  failure_code: errorCode,
746
853
  retryable: errorCode === 'TIMEOUT'
747
854
  };
@@ -5,6 +5,7 @@ import { existsSync } from 'fs';
5
5
  import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } from '../utils/android/utils.js';
6
6
  import { execAdbWithDiagnostics } from '../utils/diagnostics.js';
7
7
  import { detectJavaHome } from '../utils/java.js';
8
+ import { AndroidObserve } from '../observe/android.js';
8
9
  export class AndroidManage {
9
10
  isTestOnlyInstallFailure(output) {
10
11
  return typeof output === 'string' && output.includes('INSTALL_FAILED_TEST_ONLY');
@@ -144,8 +145,21 @@ export class AndroidManage {
144
145
  const metadata = await getAndroidDeviceMetadata(appId, deviceId);
145
146
  const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
146
147
  try {
147
- await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
148
- return { device: deviceInfo, appStarted: true, launchTimeMs: 1000 };
148
+ const output = await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
149
+ const current = await new AndroidObserve().getCurrentScreen(deviceId).catch(() => null);
150
+ return {
151
+ device: deviceInfo,
152
+ appStarted: true,
153
+ launchTimeMs: 1000,
154
+ output,
155
+ observedApp: {
156
+ appId,
157
+ package: current?.package ?? null,
158
+ activity: current?.activity ?? null,
159
+ screen: current?.shortActivity ?? current?.activity ?? null,
160
+ matchedTarget: current ? current.package === appId : null
161
+ }
162
+ };
149
163
  }
150
164
  catch (e) {
151
165
  const diag = execAdbWithDiagnostics(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
@@ -165,12 +179,18 @@ export class AndroidManage {
165
179
  }
166
180
  }
167
181
  async restartApp(appId, deviceId) {
168
- await this.terminateApp(appId, deviceId);
182
+ const terminateResult = await this.terminateApp(appId, deviceId);
169
183
  const startResult = await this.startApp(appId, deviceId);
170
184
  return {
171
185
  device: startResult.device,
172
186
  appRestarted: startResult.appStarted,
173
- launchTimeMs: startResult.launchTimeMs
187
+ launchTimeMs: startResult.launchTimeMs,
188
+ output: startResult.output,
189
+ observedApp: startResult.observedApp,
190
+ terminatedBeforeRestart: terminateResult.appTerminated,
191
+ ...(terminateResult.error ? { terminateError: terminateResult.error } : {}),
192
+ ...(startResult.error ? { error: startResult.error } : {}),
193
+ ...(startResult.diagnostics ? { diagnostics: startResult.diagnostics } : {})
174
194
  };
175
195
  }
176
196
  async resetAppData(appId, deviceId) {
@@ -1,6 +1,7 @@
1
1
  import { promises as fs } from "fs";
2
2
  import { spawn, spawnSync } from "child_process";
3
3
  import { execCommand, execCommandWithDiagnostics, getIOSDeviceMetadata, validateBundleId, getIdbCmd, findAppBundle } from "../utils/ios/utils.js";
4
+ import { iOSObserve } from "../observe/ios.js";
4
5
  import path from "path";
5
6
  export class iOSManage {
6
7
  async build(projectPath, optsOrVariant) {
@@ -294,7 +295,20 @@ export class iOSManage {
294
295
  try {
295
296
  const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId);
296
297
  const device = await getIOSDeviceMetadata(deviceId);
297
- return { device, appStarted: !!result.output, launchTimeMs: 1000 };
298
+ const fingerprint = await new iOSObserve().getScreenFingerprint(deviceId).catch(() => null);
299
+ const pidMatch = result.output.match(/:\s*(\d+)\s*$/);
300
+ return {
301
+ device,
302
+ appStarted: !!result.output,
303
+ launchTimeMs: 1000,
304
+ output: result.output,
305
+ observedApp: {
306
+ appId: bundleId,
307
+ pid: pidMatch ? Number(pidMatch[1]) : null,
308
+ screen: fingerprint?.activity ?? null,
309
+ matchedTarget: null
310
+ }
311
+ };
298
312
  }
299
313
  catch (e) {
300
314
  const diag = execCommandWithDiagnostics(['simctl', 'launch', deviceId, bundleId], deviceId);
@@ -316,9 +330,19 @@ export class iOSManage {
316
330
  }
317
331
  }
318
332
  async restartApp(bundleId, deviceId = "booted") {
319
- await this.terminateApp(bundleId, deviceId);
333
+ const terminateResult = await this.terminateApp(bundleId, deviceId);
320
334
  const startResult = await this.startApp(bundleId, deviceId);
321
- return { device: startResult.device, appRestarted: startResult.appStarted, launchTimeMs: startResult.launchTimeMs };
335
+ return {
336
+ device: startResult.device,
337
+ appRestarted: startResult.appStarted,
338
+ launchTimeMs: startResult.launchTimeMs,
339
+ output: startResult.output,
340
+ observedApp: startResult.observedApp,
341
+ terminatedBeforeRestart: terminateResult.appTerminated,
342
+ ...(terminateResult.error ? { terminateError: terminateResult.error } : {}),
343
+ ...(startResult.error ? { error: startResult.error } : {}),
344
+ ...(startResult.diagnostics ? { diagnostics: startResult.diagnostics } : {})
345
+ };
322
346
  }
323
347
  async resetAppData(bundleId, deviceId = "booted") {
324
348
  validateBundleId(bundleId);
@@ -48,12 +48,14 @@ export function inferScrollFailure(message) {
48
48
  return { failureCode: 'TIMEOUT', retryable: true };
49
49
  return { failureCode: 'UNKNOWN', retryable: false };
50
50
  }
51
- export function buildActionExecutionResult({ actionType, selector, resolved, success, uiFingerprintBefore, uiFingerprintAfter, failure }) {
52
- const timestamp = Date.now();
51
+ export function buildActionExecutionResult({ actionType, device, selector, resolved, success, uiFingerprintBefore, uiFingerprintAfter, failure, details }) {
52
+ const timestampMs = Date.now();
53
+ const timestamp = new Date(timestampMs).toISOString();
53
54
  return {
54
- action_id: nextActionId(actionType, timestamp),
55
+ action_id: nextActionId(actionType, timestampMs),
55
56
  timestamp,
56
57
  action_type: actionType,
58
+ ...(device ? { device } : {}),
57
59
  target: {
58
60
  selector,
59
61
  resolved: normalizeResolvedTarget(resolved)
@@ -61,6 +63,32 @@ export function buildActionExecutionResult({ actionType, selector, resolved, suc
61
63
  success,
62
64
  ...(failure ? { failure_code: failure.failureCode, retryable: failure.retryable } : {}),
63
65
  ui_fingerprint_before: uiFingerprintBefore,
64
- ui_fingerprint_after: uiFingerprintAfter
66
+ ui_fingerprint_after: uiFingerprintAfter,
67
+ ...(details ? { details } : {})
68
+ };
69
+ }
70
+ export function wrapToolError(name, error) {
71
+ const message = error instanceof Error
72
+ ? error.message
73
+ : typeof error === 'object' && error !== null
74
+ ? (() => {
75
+ try {
76
+ return JSON.stringify(error, null, 2);
77
+ }
78
+ catch {
79
+ return '[unserializable error object]';
80
+ }
81
+ })()
82
+ : String(error);
83
+ return {
84
+ content: [{
85
+ type: 'text',
86
+ text: JSON.stringify({
87
+ error: {
88
+ tool: name,
89
+ message
90
+ }
91
+ }, null, 2)
92
+ }]
65
93
  };
66
94
  }
@@ -10,7 +10,7 @@ Inputs:
10
10
  - deviceId (optional)
11
11
 
12
12
  Output Structure:
13
- - action_id, timestamp, action_type
13
+ - action_id, timestamp (ISO 8601), action_type
14
14
  - target.selector = { appId }
15
15
  - success = true when launch was dispatched successfully
16
16
  - failure_code/retryable when launch dispatch fails
@@ -83,7 +83,7 @@ Inputs:
83
83
  - deviceId (optional)
84
84
 
85
85
  Output Structure:
86
- - action_id, timestamp, action_type
86
+ - action_id, timestamp (ISO 8601), action_type
87
87
  - target.selector = { appId }
88
88
  - success = true when the restart command completed
89
89
  - failure_code/retryable when restart dispatch fails
@@ -532,7 +532,7 @@ Inputs:
532
532
  - deviceId (optional)
533
533
 
534
534
  Output Structure:
535
- - action_id, timestamp, action_type
535
+ - action_id, timestamp (ISO 8601), action_type
536
536
  - target.selector = { x, y }
537
537
  - success = true when the tap was dispatched
538
538
  - failure_code/retryable when dispatch fails
@@ -587,7 +587,7 @@ Inputs:
587
587
 
588
588
  Output Structure:
589
589
  - action_id: unique timestamp-based action identifier
590
- - timestamp: epoch milliseconds for the action attempt
590
+ - timestamp: ISO 8601 timestamp for the action attempt
591
591
  - action_type: "tap_element"
592
592
  - target.selector: original target handle ({ elementId })
593
593
  - target.resolved: minimal resolved element info used for the tap
@@ -640,7 +640,7 @@ Inputs:
640
640
  - platform/deviceId (optional)
641
641
 
642
642
  Output Structure:
643
- - action_id, timestamp, action_type
643
+ - action_id, timestamp (ISO 8601), action_type
644
644
  - target.selector = { x1, y1, x2, y2, duration }
645
645
  - success = true when the swipe was dispatched
646
646
  - failure_code/retryable when dispatch fails
@@ -692,7 +692,7 @@ Inputs:
692
692
  - direction, maxScrolls, scrollAmount, deviceId (optional)
693
693
 
694
694
  Output Structure:
695
- - action_id, timestamp, action_type
695
+ - action_id, timestamp (ISO 8601), action_type
696
696
  - target.selector = original selector
697
697
  - target.resolved = minimal resolved element info when found
698
698
  - success = true when scrolling produced a visible target element
@@ -746,7 +746,7 @@ Inputs:
746
746
  - platform/deviceId (optional)
747
747
 
748
748
  Output Structure:
749
- - action_id, timestamp, action_type
749
+ - action_id, timestamp (ISO 8601), action_type
750
750
  - target.selector = { text }
751
751
  - success = true when text input was dispatched
752
752
  - failure_code/retryable when dispatch fails
@@ -795,7 +795,7 @@ Inputs:
795
795
  - platform/deviceId (optional)
796
796
 
797
797
  Output Structure:
798
- - action_id, timestamp, action_type
798
+ - action_id, timestamp (ISO 8601), action_type
799
799
  - target.selector = { key: "back" }
800
800
  - success = true when the back action was dispatched
801
801
  - failure_code/retryable when dispatch fails
@@ -4,7 +4,7 @@ import { ToolsObserve } from '../observe/index.js';
4
4
  import { classifyActionOutcome } from '../interact/classify.js';
5
5
  import { ToolsNetwork } from '../network/index.js';
6
6
  import { getSystemStatus } from '../system/index.js';
7
- import { buildActionExecutionResult, captureActionFingerprint, inferGenericFailure, inferScrollFailure, wrapResponse } from './common.js';
7
+ import { buildActionExecutionResult, captureActionFingerprint, inferGenericFailure, inferScrollFailure, wrapResponse, wrapToolError } from './common.js';
8
8
  async function handleStartApp(args) {
9
9
  const { platform, appId, deviceId } = args;
10
10
  const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId);
@@ -13,11 +13,19 @@ async function handleStartApp(args) {
13
13
  const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId);
14
14
  return wrapResponse(buildActionExecutionResult({
15
15
  actionType: 'start_app',
16
+ device: res.device,
16
17
  selector: { appId },
17
18
  success: !!res.appStarted,
18
19
  uiFingerprintBefore,
19
20
  uiFingerprintAfter,
20
- failure: res.appStarted ? undefined : inferGenericFailure(res.error)
21
+ failure: res.appStarted ? undefined : inferGenericFailure(res.error),
22
+ details: {
23
+ launch_time_ms: res.launchTimeMs,
24
+ ...(typeof res.output === 'string' ? { output: res.output } : {}),
25
+ ...(res.device ? { device_id: res.device.id } : {}),
26
+ ...(typeof res.error === 'string' ? { error: res.error } : {}),
27
+ ...(res.observedApp ? { observed_app: res.observedApp } : {})
28
+ }
21
29
  }));
22
30
  }
23
31
  async function handleTerminateApp(args) {
@@ -34,11 +42,20 @@ async function handleRestartApp(args) {
34
42
  const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId);
35
43
  return wrapResponse(buildActionExecutionResult({
36
44
  actionType: 'restart_app',
45
+ device: res.device,
37
46
  selector: { appId },
38
47
  success: !!res.appRestarted,
39
48
  uiFingerprintBefore,
40
49
  uiFingerprintAfter,
41
- failure: res.appRestarted ? undefined : inferGenericFailure(res.error)
50
+ failure: res.appRestarted ? undefined : inferGenericFailure(res.error),
51
+ details: {
52
+ launch_time_ms: res.launchTimeMs,
53
+ ...(typeof res.output === 'string' ? { output: res.output } : {}),
54
+ ...(typeof res.terminatedBeforeRestart === 'boolean' ? { terminated_before_restart: res.terminatedBeforeRestart } : {}),
55
+ ...(typeof res.terminateError === 'string' ? { terminate_error: res.terminateError } : {}),
56
+ ...(typeof res.error === 'string' ? { error: res.error } : {}),
57
+ ...(res.observedApp ? { observed_app: res.observedApp } : {})
58
+ }
42
59
  }));
43
60
  }
44
61
  async function handleResetAppData(args) {
@@ -313,8 +330,7 @@ export async function handleToolCall(name, args = {}) {
313
330
  return await handler(args);
314
331
  }
315
332
  catch (error) {
316
- return {
317
- content: [{ type: 'text', text: `Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}` }]
318
- };
333
+ console.error(`Error executing tool ${name}:`, error);
334
+ return wrapToolError(name, error);
319
335
  }
320
336
  }
@@ -8,8 +8,30 @@ export async function getSystemStatus() {
8
8
  const gradle = await checkGradle();
9
9
  const issues = [...android.issues, ...ios.issues, ...(gradle.issues || [])];
10
10
  const success = issues.length === 0;
11
+ const androidReady = android.adbAvailable && android.devices > 0 && !android.issues.some((issue) => /unauthorized|offline/i.test(issue));
12
+ const iosReady = ios.iosAvailable && ios.iosDevices > 0;
13
+ const gradleReady = (gradle.issues || []).length === 0;
14
+ const overallStatus = success ? 'ready' : (androidReady || iosReady ? 'degraded' : 'blocked');
15
+ const androidSummary = !android.adbAvailable
16
+ ? 'ADB unavailable'
17
+ : android.devices === 0
18
+ ? 'ADB available but no Android devices connected'
19
+ : android.logsAvailable
20
+ ? `${android.devices} Android device(s) connected; log access available`
21
+ : `${android.devices} Android device(s) connected; log access unavailable`;
22
+ const iosSummary = !ios.iosAvailable
23
+ ? 'xcrun unavailable'
24
+ : ios.iosDevices === 0
25
+ ? 'xcrun available but no iOS simulators booted'
26
+ : `${ios.iosDevices} iOS simulator(s) booted`;
27
+ const gradleSummary = !gradle.gradleJavaHome
28
+ ? 'No explicit Gradle JDK override detected'
29
+ : gradleReady
30
+ ? `Gradle JDK configured at ${gradle.gradleJavaHome}`
31
+ : `Gradle JDK override invalid: ${gradle.gradleJavaHome}`;
11
32
  return {
12
33
  success,
34
+ status: overallStatus,
13
35
  adbAvailable: android.adbAvailable,
14
36
  adbVersion: android.adbVersion,
15
37
  devices: android.devices,
@@ -23,10 +45,39 @@ export async function getSystemStatus() {
23
45
  gradleJavaHome: gradle.gradleJavaHome,
24
46
  gradleValid: gradle.gradleValid,
25
47
  gradleFilesChecked: gradle.filesChecked,
26
- gradleSuggestedFixes: gradle.suggestedFixes
48
+ gradleSuggestedFixes: gradle.suggestedFixes,
49
+ summary: {
50
+ overall: overallStatus,
51
+ android: {
52
+ ready: androidReady,
53
+ summary: androidSummary,
54
+ blockers: android.issues
55
+ },
56
+ ios: {
57
+ ready: iosReady,
58
+ summary: iosSummary,
59
+ blockers: ios.issues
60
+ },
61
+ gradle: {
62
+ ready: gradleReady,
63
+ summary: gradleSummary,
64
+ blockers: gradle.issues || [],
65
+ suggestedFixes: gradle.suggestedFixes || []
66
+ }
67
+ }
27
68
  };
28
69
  }
29
70
  catch (e) {
30
- return { success: false, issues: ['Internal error: ' + (e instanceof Error ? e.message : String(e))] };
71
+ return {
72
+ success: false,
73
+ status: 'blocked',
74
+ issues: ['Internal error: ' + (e instanceof Error ? e.message : String(e))],
75
+ summary: {
76
+ overall: 'blocked',
77
+ android: { ready: false, summary: 'Android status unavailable', blockers: [] },
78
+ ios: { ready: false, summary: 'iOS status unavailable', blockers: [] },
79
+ gradle: { ready: false, summary: 'Gradle status unavailable', blockers: [], suggestedFixes: [] }
80
+ }
81
+ };
31
82
  }
32
83
  }
package/docs/CHANGELOG.md CHANGED
@@ -2,8 +2,16 @@
2
2
 
3
3
  All notable changes to the **Mobile Debug MCP** project will be documented in this file.
4
4
 
5
+ ## [0.24.3]
6
+ - Improved output consistency
7
+
8
+ ## [0.24.2]
9
+ - Fixed Android install issue
10
+ - Updated tools to have more detailed responses
11
+
5
12
  ## [0.24.1]
6
13
  - Fixed Android install issue
14
+ - Updated tools to have more detailed responses
7
15
 
8
16
  ## [0.24.0]
9
17
  - Improved execution loop