mobile-debug-mcp 0.24.1 → 0.24.2

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.
@@ -437,6 +437,12 @@ export class ToolsInteract {
437
437
  }
438
438
  static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId }) {
439
439
  const overallStart = Date.now();
440
+ const requestedIndex = typeof match?.index === 'number' ? match.index : null;
441
+ const requested = {
442
+ selector: selector ?? {},
443
+ condition,
444
+ match: requestedIndex === null ? null : { index: requestedIndex }
445
+ };
440
446
  // Validate selector: require at least one non-empty field (text, resource_id, or accessibility_id)
441
447
  const hasText = typeof selector?.text === 'string' && selector.text.trim().length > 0;
442
448
  const hasResId = typeof selector?.resource_id === 'string' && selector.resource_id.trim().length > 0;
@@ -448,22 +454,27 @@ export class ToolsInteract {
448
454
  code: 'INVALID_SELECTOR',
449
455
  message: 'Selector must include at least one non-empty field: text, resource_id, or accessibility_id'
450
456
  },
451
- metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }
457
+ metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 },
458
+ requested,
459
+ observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null }
452
460
  };
453
461
  }
454
462
  // Validate condition
455
463
  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 } };
464
+ 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
465
  }
458
466
  // Platform check
459
467
  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 } };
468
+ 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
469
  }
462
470
  const effectivePoll = Math.max(50, Math.min(poll_interval_ms || 300, 2000));
463
471
  const maxAttempts = (retry && retry.max_attempts) ? Math.max(1, retry.max_attempts) : 1;
464
472
  const backoff = (retry && retry.backoff_ms) ? Math.max(0, retry.backoff_ms) : 0;
465
473
  let attempts = 0;
466
474
  let totalPollCount = 0;
475
+ let lastMatchedCount = 0;
476
+ let lastMatchedElement = null;
477
+ let lastConditionSatisfied = false;
467
478
  // Precompute normalized selector values and helpers (constant across polls)
468
479
  const normalize = ToolsInteract._normalize;
469
480
  const containsFlag = !!selector?.contains;
@@ -573,18 +584,27 @@ export class ToolsInteract {
573
584
  else
574
585
  conditionMet = false;
575
586
  }
587
+ const resolvedPlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
588
+ const resolvedDeviceId = tree?.device?.id || deviceId;
589
+ lastMatchedCount = matchedCount;
590
+ lastConditionSatisfied = conditionMet;
591
+ lastMatchedElement = matchedElement ? ToolsInteract._buildResolvedElement(resolvedPlatform, resolvedDeviceId, matchedElement.el, matchedElement.idx) : null;
576
592
  if (conditionMet) {
577
593
  const now = Date.now();
578
594
  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;
595
+ const outEl = lastMatchedElement;
583
596
  return {
584
597
  status: 'success',
585
598
  matched: matchedCount,
586
599
  element: outEl,
587
- metrics: { latency_ms, poll_count: totalPollCount, attempts }
600
+ metrics: { latency_ms, poll_count: totalPollCount, attempts },
601
+ requested,
602
+ observed: {
603
+ matched_count: matchedCount,
604
+ condition_satisfied: true,
605
+ selected_index: outEl?.index ?? null,
606
+ last_matched_element: outEl
607
+ }
588
608
  };
589
609
  }
590
610
  }
@@ -603,16 +623,38 @@ export class ToolsInteract {
603
623
  }
604
624
  // Final failure for this call
605
625
  const elapsed = Date.now() - overallStart;
626
+ const observed = {
627
+ matched_count: lastMatchedCount,
628
+ condition_satisfied: lastConditionSatisfied,
629
+ selected_index: lastMatchedElement?.index ?? null,
630
+ last_matched_element: lastMatchedElement
631
+ };
632
+ const matchNote = requestedIndex !== null && lastMatchedCount <= requestedIndex
633
+ ? ` requested match.index=${requestedIndex} but observed ${lastMatchedCount} match(es)`
634
+ : ` observed ${lastMatchedCount} match(es)`;
606
635
  return {
607
636
  status: 'timeout',
608
- error: { code: 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout` },
609
- metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts }
637
+ error: { code: 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout;${matchNote}` },
638
+ metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
639
+ requested,
640
+ observed
610
641
  };
611
642
  }
612
643
  }
613
644
  catch (err) {
614
645
  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 } };
646
+ return {
647
+ status: 'timeout',
648
+ error: { code: 'INTERNAL_ERROR', message: err instanceof Error ? err.message : String(err) },
649
+ metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
650
+ requested,
651
+ observed: {
652
+ matched_count: lastMatchedCount,
653
+ condition_satisfied: false,
654
+ selected_index: lastMatchedElement?.index ?? null,
655
+ last_matched_element: lastMatchedElement
656
+ }
657
+ };
616
658
  }
617
659
  }
618
660
  // Helper: normalize various log objects into plain message strings for comparison
@@ -635,10 +677,12 @@ export class ToolsInteract {
635
677
  static async waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs = 5000, pollIntervalMs = 300, deviceId }) {
636
678
  const start = Date.now();
637
679
  let lastFingerprint = null;
680
+ let lastActivity = null;
638
681
  while (Date.now() - start < timeoutMs) {
639
682
  try {
640
683
  const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
641
684
  const fp = res?.fingerprint ?? null;
685
+ lastActivity = res?.activity ?? lastActivity;
642
686
  if (fp === null || fp === undefined) {
643
687
  lastFingerprint = null;
644
688
  await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
@@ -651,8 +695,18 @@ export class ToolsInteract {
651
695
  try {
652
696
  const confirmRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
653
697
  const confirmFp = confirmRes?.fingerprint ?? null;
698
+ lastActivity = confirmRes?.activity ?? lastActivity;
654
699
  if (confirmFp === fp) {
655
- return { success: true, newFingerprint: fp, elapsedMs: Date.now() - start };
700
+ return {
701
+ success: true,
702
+ previousFingerprint,
703
+ newFingerprint: fp,
704
+ elapsedMs: Date.now() - start,
705
+ observed_screen: {
706
+ fingerprint: fp,
707
+ activity: lastActivity
708
+ }
709
+ };
656
710
  }
657
711
  lastFingerprint = confirmFp;
658
712
  continue;
@@ -668,7 +722,17 @@ export class ToolsInteract {
668
722
  }
669
723
  await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
670
724
  }
671
- return { success: false, reason: 'timeout', lastFingerprint, elapsedMs: Date.now() - start };
725
+ return {
726
+ success: false,
727
+ reason: 'timeout',
728
+ previousFingerprint,
729
+ lastFingerprint,
730
+ elapsedMs: Date.now() - start,
731
+ observed_screen: {
732
+ fingerprint: lastFingerprint,
733
+ activity: lastActivity
734
+ }
735
+ };
672
736
  }
673
737
  static async expectScreenHandler({ platform, fingerprint, screen, deviceId }) {
674
738
  const observedFingerprint = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
@@ -691,16 +755,26 @@ export class ToolsInteract {
691
755
  screen: screen ?? null
692
756
  };
693
757
  let success = false;
758
+ let basis = 'none';
759
+ let reason = 'No fingerprint or screen expectation provided';
694
760
  if (fingerprint) {
761
+ basis = 'fingerprint';
695
762
  success = observedScreen.fingerprint === fingerprint;
763
+ reason = success
764
+ ? `observed fingerprint matches expected fingerprint ${fingerprint}`
765
+ : `expected fingerprint ${fingerprint} but observed ${observedScreen.fingerprint ?? 'null'}`;
696
766
  }
697
767
  else if (screen) {
768
+ basis = 'screen';
698
769
  const candidates = new Set();
699
770
  if (observedScreen.screen)
700
771
  candidates.add(observedScreen.screen);
701
772
  if (observedScreenLabel)
702
773
  candidates.add(observedScreenLabel);
703
774
  success = candidates.has(screen);
775
+ reason = success
776
+ ? `observed screen matches expected screen ${screen}`
777
+ : `expected screen ${screen} but observed ${observedScreenLabel ?? observedScreen.screen ?? 'null'}`;
704
778
  }
705
779
  return {
706
780
  success,
@@ -709,7 +783,12 @@ export class ToolsInteract {
709
783
  screen: observedScreenLabel
710
784
  },
711
785
  expected_screen: expectedScreen,
712
- confidence: success ? 1 : 0
786
+ confidence: success ? 1 : 0,
787
+ comparison: {
788
+ basis,
789
+ matched: success,
790
+ reason
791
+ }
713
792
  };
714
793
  }
715
794
  static async expectElementVisibleHandler({ selector, element_id, timeout_ms = 5000, poll_interval_ms = 300, platform, deviceId }) {
@@ -726,6 +805,7 @@ export class ToolsInteract {
726
805
  success: true,
727
806
  selector,
728
807
  element_id: result.element.elementId ?? element_id ?? null,
808
+ expected_condition: 'visible',
729
809
  element: {
730
810
  elementId: result.element.elementId ?? null,
731
811
  text: result.element.text ?? null,
@@ -734,7 +814,23 @@ export class ToolsInteract {
734
814
  class: result.element.class ?? null,
735
815
  bounds: result.element.bounds ?? null,
736
816
  index: typeof result.element.index === 'number' ? result.element.index : null
737
- }
817
+ },
818
+ observed: {
819
+ status: result.status,
820
+ matched_count: typeof result.matched === 'number' ? result.matched : result?.observed?.matched_count ?? null,
821
+ condition_satisfied: true,
822
+ selected_index: typeof result.element.index === 'number' ? result.element.index : null,
823
+ last_matched_element: {
824
+ elementId: result.element.elementId ?? null,
825
+ text: result.element.text ?? null,
826
+ resource_id: result.element.resource_id ?? null,
827
+ accessibility_id: result.element.accessibility_id ?? null,
828
+ class: result.element.class ?? null,
829
+ bounds: result.element.bounds ?? null,
830
+ index: typeof result.element.index === 'number' ? result.element.index : null
831
+ }
832
+ },
833
+ reason: 'selector is visible'
738
834
  };
739
835
  }
740
836
  const errorCode = result?.error?.code === 'INTERNAL_ERROR' ? 'UNKNOWN' : 'TIMEOUT';
@@ -742,6 +838,15 @@ export class ToolsInteract {
742
838
  success: false,
743
839
  selector,
744
840
  element_id: element_id ?? null,
841
+ expected_condition: 'visible',
842
+ observed: {
843
+ status: result?.status,
844
+ matched_count: result?.observed?.matched_count,
845
+ condition_satisfied: result?.observed?.condition_satisfied ?? false,
846
+ selected_index: result?.observed?.selected_index ?? null,
847
+ last_matched_element: result?.observed?.last_matched_element ?? null
848
+ },
849
+ reason: result?.error?.message ?? 'selector is not visible',
745
850
  failure_code: errorCode,
746
851
  retryable: errorCode === 'TIMEOUT'
747
852
  };
@@ -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,13 @@ 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 }) {
51
+ export function buildActionExecutionResult({ actionType, device, selector, resolved, success, uiFingerprintBefore, uiFingerprintAfter, failure, details }) {
52
52
  const timestamp = Date.now();
53
53
  return {
54
54
  action_id: nextActionId(actionType, timestamp),
55
55
  timestamp,
56
56
  action_type: actionType,
57
+ ...(device ? { device } : {}),
57
58
  target: {
58
59
  selector,
59
60
  resolved: normalizeResolvedTarget(resolved)
@@ -61,6 +62,7 @@ export function buildActionExecutionResult({ actionType, selector, resolved, suc
61
62
  success,
62
63
  ...(failure ? { failure_code: failure.failureCode, retryable: failure.retryable } : {}),
63
64
  ui_fingerprint_before: uiFingerprintBefore,
64
- ui_fingerprint_after: uiFingerprintAfter
65
+ ui_fingerprint_after: uiFingerprintAfter,
66
+ ...(details ? { details } : {})
65
67
  };
66
68
  }
@@ -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) {
@@ -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,13 @@
2
2
 
3
3
  All notable changes to the **Mobile Debug MCP** project will be documented in this file.
4
4
 
5
+ ## [0.24.2]
6
+ - Fixed Android install issue
7
+ - Updated tools to have more detailed responses
8
+
5
9
  ## [0.24.1]
6
10
  - Fixed Android install issue
11
+ - Updated tools to have more detailed responses
7
12
 
8
13
  ## [0.24.0]
9
14
  - Improved execution loop
@@ -111,13 +111,26 @@ Input example:
111
111
  Success response example:
112
112
 
113
113
  ```json
114
- { "success": true, "newFingerprint": "<hex-fingerprint>", "elapsedMs": 420 }
114
+ {
115
+ "success": true,
116
+ "previousFingerprint": "<old-hex-fingerprint>",
117
+ "newFingerprint": "<hex-fingerprint>",
118
+ "elapsedMs": 420,
119
+ "observed_screen": { "fingerprint": "<hex-fingerprint>", "activity": "MainActivity" }
120
+ }
115
121
  ```
116
122
 
117
123
  Failure (timeout) example:
118
124
 
119
125
  ```json
120
- { "success": false, "reason": "timeout", "lastFingerprint": "<hex-fingerprint>", "elapsedMs": 5000 }
126
+ {
127
+ "success": false,
128
+ "reason": "timeout",
129
+ "previousFingerprint": "<old-hex-fingerprint>",
130
+ "lastFingerprint": "<hex-fingerprint>",
131
+ "elapsedMs": 5000,
132
+ "observed_screen": { "fingerprint": "<hex-fingerprint>", "activity": "HomeActivity" }
133
+ }
121
134
  ```
122
135
 
123
136
  Notes:
@@ -214,7 +227,26 @@ Success response:
214
227
  "index": 8,
215
228
  "elementId": "el_..."
216
229
  },
217
- "metrics": { "latency_ms": 120, "poll_count": 1, "attempts": 1 }
230
+ "metrics": { "latency_ms": 120, "poll_count": 1, "attempts": 1 },
231
+ "requested": {
232
+ "selector": { "text": "Generate Session", "contains": false },
233
+ "condition": "clickable",
234
+ "match": { "index": 0 }
235
+ },
236
+ "observed": {
237
+ "matched_count": 1,
238
+ "condition_satisfied": true,
239
+ "selected_index": 8,
240
+ "last_matched_element": {
241
+ "text": "Generate Session",
242
+ "resource_id": null,
243
+ "accessibility_id": null,
244
+ "class": "android.widget.TextView",
245
+ "bounds": [471, 1098, 809, 1158],
246
+ "index": 8,
247
+ "elementId": "el_..."
248
+ }
249
+ }
218
250
  }
219
251
  ```
220
252
 
@@ -223,8 +255,27 @@ Timeout response:
223
255
  ```json
224
256
  {
225
257
  "status": "timeout",
226
- "error": { "code": "ELEMENT_NOT_FOUND", "message": "Condition visible not satisfied within timeout" },
227
- "metrics": { "latency_ms": 5000, "poll_count": 17, "attempts": 1 }
258
+ "error": { "code": "ELEMENT_NOT_FOUND", "message": "Condition visible not satisfied within timeout; observed 1 match(es)" },
259
+ "metrics": { "latency_ms": 5000, "poll_count": 17, "attempts": 1 },
260
+ "requested": {
261
+ "selector": { "text": "Generate Session", "contains": false },
262
+ "condition": "visible",
263
+ "match": { "index": 0 }
264
+ },
265
+ "observed": {
266
+ "matched_count": 1,
267
+ "condition_satisfied": false,
268
+ "selected_index": 8,
269
+ "last_matched_element": {
270
+ "text": "Generate Session",
271
+ "resource_id": null,
272
+ "accessibility_id": null,
273
+ "class": "android.widget.TextView",
274
+ "bounds": [471, 1098, 809, 1158],
275
+ "index": 8,
276
+ "elementId": "el_..."
277
+ }
278
+ }
228
279
  }
229
280
  ```
230
281
 
@@ -232,6 +283,7 @@ Notes:
232
283
 
233
284
  - Use `wait_for_ui` to get a stable `elementId` for `tap_element`.
234
285
  - Use it before an action when the target element or timing is uncertain.
286
+ - Use `requested` and `observed` to see exactly what condition was checked and what the last poll actually found.
235
287
  - If the expected outcome is known after the action, follow with `expect_*`.
236
288
 
237
289
  ---
@@ -330,7 +382,12 @@ Response:
330
382
  "success": true,
331
383
  "observed_screen": { "fingerprint": "<actual-fingerprint>", "screen": "com.example.app.MainActivity" },
332
384
  "expected_screen": { "fingerprint": "<expected-fingerprint>", "screen": null },
333
- "confidence": 1
385
+ "confidence": 1,
386
+ "comparison": {
387
+ "basis": "fingerprint",
388
+ "matched": true,
389
+ "reason": "observed fingerprint matches expected fingerprint <expected-fingerprint>"
390
+ }
334
391
  }
335
392
  ```
336
393
 
@@ -367,6 +424,7 @@ Response:
367
424
  "success": true,
368
425
  "selector": { "text": "Play session" },
369
426
  "element_id": "el_123",
427
+ "expected_condition": "visible",
370
428
  "element": {
371
429
  "elementId": "el_123",
372
430
  "text": "Play session",
@@ -375,7 +433,14 @@ Response:
375
433
  "class": "android.widget.TextView",
376
434
  "bounds": [519, 1770, 762, 1830],
377
435
  "index": 11
378
- }
436
+ },
437
+ "observed": {
438
+ "status": "success",
439
+ "matched_count": 1,
440
+ "condition_satisfied": true,
441
+ "selected_index": 11
442
+ },
443
+ "reason": "selector is visible"
379
444
  }
380
445
  ```
381
446
 
@@ -384,4 +449,5 @@ Notes:
384
449
  - Primary and authoritative verification tool for expected element visibility.
385
450
  - `selector` is the primary input; `element_id` is optional context only.
386
451
  - The tool resolves the selector internally when needed.
452
+ - On failure, `reason` and `observed` tell you whether the selector was missing entirely or present but not yet visible.
387
453
  - Use when the screen should remain on the same destination but a specific element should appear or become visible.