mobile-debug-mcp 0.31.0 → 0.32.1

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 (109) hide show
  1. package/README.md +1 -0
  2. package/dist/interact/android.js +13 -8
  3. package/dist/interact/index.js +33 -22
  4. package/dist/interact/ios.js +5 -2
  5. package/dist/manage/android.js +20 -10
  6. package/dist/manage/ios.js +19 -10
  7. package/dist/observe/android-tree-bridge.js +192 -0
  8. package/dist/observe/android.js +53 -14
  9. package/dist/observe/index.js +22 -5
  10. package/dist/observe/ios.js +50 -29
  11. package/dist/observe/performance-metrics.js +124 -0
  12. package/dist/observe/snapshot-metadata.js +94 -15
  13. package/dist/server/common.js +14 -7
  14. package/dist/server/journey-schema.js +60 -0
  15. package/dist/server/tool-definitions.js +59 -6
  16. package/dist/server/tool-handlers.js +381 -56
  17. package/dist/server-core.js +1 -1
  18. package/dist/utils/exec.js +18 -5
  19. package/dist/utils/ios/utils.js +5 -2
  20. package/dist/utils/operation-budget.js +25 -0
  21. package/dist/utils/resolve-device.js +12 -1
  22. package/dist/utils/timing.js +19 -0
  23. package/dist/utils/ui/index.js +2 -0
  24. package/docs/CHANGELOG.md +6 -0
  25. package/docs/ROADMAP.md +9 -0
  26. package/docs/benchmarks/interaction-latency/README.md +26 -0
  27. package/docs/benchmarks/interaction-latency/final.json +1810 -0
  28. package/docs/benchmarks/interaction-latency/final.md +17 -0
  29. package/docs/benchmarks/interaction-latency/ios-validation.md +11 -0
  30. package/docs/benchmarks/ui-tree-performance/README.md +51 -0
  31. package/docs/benchmarks/ui-tree-performance/baseline.json +59546 -0
  32. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +28 -0
  33. package/docs/benchmarks/ui-tree-performance/candidate.json +51691 -0
  34. package/docs/benchmarks/ui-tree-performance/comparison-attempt-1.json +110679 -0
  35. package/docs/benchmarks/ui-tree-performance/comparison.json +111230 -0
  36. package/docs/benchmarks/ui-tree-performance/comparison.md +41 -0
  37. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +59560 -0
  38. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +301 -0
  39. package/docs/specs/019-ui-tree-observation-performance/spec.md +148 -0
  40. package/docs/tools/interact.md +30 -0
  41. package/docs/tools/observe.md +1 -0
  42. package/package.json +1 -1
  43. package/skills/mobile-debug-tools-usage/SKILL.md +74 -0
  44. package/src/interact/android.ts +13 -8
  45. package/src/interact/index.ts +32 -23
  46. package/src/interact/ios.ts +5 -2
  47. package/src/manage/android.ts +19 -10
  48. package/src/manage/ios.ts +18 -10
  49. package/src/observe/android-tree-bridge.ts +170 -0
  50. package/src/observe/android.ts +48 -17
  51. package/src/observe/index.ts +21 -5
  52. package/src/observe/ios.ts +49 -36
  53. package/src/observe/performance-metrics.ts +125 -0
  54. package/src/observe/snapshot-metadata.ts +94 -15
  55. package/src/server/common.ts +13 -6
  56. package/src/server/journey-schema.ts +63 -0
  57. package/src/server/tool-definitions.ts +60 -7
  58. package/src/server/tool-handlers.ts +349 -54
  59. package/src/server-core.ts +1 -1
  60. package/src/types.ts +31 -2
  61. package/src/utils/exec.ts +13 -5
  62. package/src/utils/ios/utils.ts +5 -2
  63. package/src/utils/operation-budget.ts +22 -0
  64. package/src/utils/resolve-device.ts +13 -1
  65. package/src/utils/timing.ts +15 -0
  66. package/src/utils/ui/index.ts +2 -0
  67. package/test/device/automated/latency/benchmark.ts +136 -0
  68. package/test/device/automated/latency/report.ts +6 -0
  69. package/test/device/automated/ui-tree-performance/benchmark.ts +401 -0
  70. package/test/device/automated/ui-tree-performance/bridge-probe.ts +183 -0
  71. package/test/fixtures/latency-journey-app/AndroidManifest.xml +7 -0
  72. package/test/fixtures/latency-journey-app/README.md +28 -0
  73. package/test/fixtures/latency-journey-app/build.ts +26 -0
  74. package/test/fixtures/latency-journey-app/src/MainActivity.java +104 -0
  75. package/test/fixtures/ui-tree-bridge/AndroidManifest.xml +6 -0
  76. package/test/fixtures/ui-tree-bridge/build.ts +34 -0
  77. package/test/fixtures/ui-tree-bridge/src/TreeInstrumentation.java +119 -0
  78. package/test/unit/manage/restart_budget.test.ts +37 -0
  79. package/test/unit/observe/android_tree_acquisition.test.ts +70 -0
  80. package/test/unit/observe/android_tree_bridge_cleanup.test.ts +76 -0
  81. package/test/unit/observe/capture_debug_snapshot.test.ts +6 -0
  82. package/test/unit/observe/ios_tree_acquisition.test.ts +77 -0
  83. package/test/unit/observe/performance_metrics.test.ts +40 -0
  84. package/test/unit/observe/snapshot_metadata.test.ts +20 -1
  85. package/test/unit/observe/state_delta.test.ts +15 -0
  86. package/test/unit/server/benchmark_report.test.ts +4 -0
  87. package/test/unit/server/contract.test.ts +1 -1
  88. package/test/unit/server/interaction_modes.test.ts +60 -0
  89. package/test/unit/server/operation_budget.test.ts +45 -0
  90. package/test/unit/server/response_shapes.test.ts +3 -3
  91. package/test/unit/server/run_journey.test.ts +120 -0
  92. package/test/unit/server/tap_selector.test.ts +68 -0
  93. /package/docs/{specs-deltas/001-state-verification.md → specs/001-state-verification/spec.md} +0 -0
  94. /package/docs/{specs-deltas/002-richer-element-identity → specs/002-richer-element-identity/spec.md} +0 -0
  95. /package/docs/{specs-deltas/003-wait-and-synchronization-reliability.md → specs/003-wait-and-synchronization-reliability/spec.md} +0 -0
  96. /package/docs/{specs-deltas/004-action-verification-routing.md → specs/004-action-verification-routing/spec.md} +0 -0
  97. /package/docs/{specs-deltas/005-unified-action-execution-and-verification-model.md → specs/005-unified-action-execution-and-verification-model/spec.md} +0 -0
  98. /package/docs/{specs-deltas/006-runtime-action-instrumentation-and-binding-layer.md → specs/006-runtime-action-instrumentation-and-binding-layer/spec.md} +0 -0
  99. /package/docs/{specs-deltas/007-actionability-resolution-and-executable-target-selection.md → specs/007-actionability-resolution-and-executable-target-selection/spec.md} +0 -0
  100. /package/docs/{specs-deltas/008-adjustable-control-support-and-semantic-value-manipulation.md → specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md} +0 -0
  101. /package/docs/{specs-deltas/009-semantic-control-modeling-for-custom-and-composite-controls.md → specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md} +0 -0
  102. /package/docs/{specs-deltas/010-verification-stabilization-and-temporal-convergence.md → specs/010-verification-stabilization-and-temporal-convergence/spec.md} +0 -0
  103. /package/docs/{specs-deltas/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md → specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md} +0 -0
  104. /package/docs/{specs-deltas/011.1-recovery-contract-types-and-runtime-wiring-spec.md → specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md} +0 -0
  105. /package/docs/{specs-deltas/012-action-trace-and-xecution-observability.md → specs/012-action-trace-and-xecution-observability/spec.md} +0 -0
  106. /package/docs/{specs-deltas/013-wait-and-synchronization-reliability.md → specs/013-wait-and-synchronization-reliability/spec.md} +0 -0
  107. /package/docs/{specs-deltas/014-actionability-resolution.md → specs/014-actionability-resolution/spec.md} +0 -0
  108. /package/docs/{specs-deltas/015-adjustable-control-support.md → specs/015-adjustable-control-support/spec.md} +0 -0
  109. /package/docs/{specs-deltas/016-environment-tools.md → specs/016-environment-tools/spec.md} +0 -0
@@ -1,6 +1,8 @@
1
+ import { remainingBudget } from './operation-budget.js';
1
2
  import { spawn } from 'child_process';
2
3
  export async function execCmd(cmd, args, opts = {}) {
3
- const { timeout = 0, env, cwd, shell } = opts;
4
+ const { env, cwd, shell } = opts;
5
+ const timeout = remainingBudget(opts.timeout ?? 0);
4
6
  return new Promise((resolve, reject) => {
5
7
  const child = spawn(cmd, args, { env: { ...process.env, ...(env || {}) }, cwd, shell });
6
8
  let stdout = '';
@@ -10,24 +12,35 @@ export async function execCmd(cmd, args, opts = {}) {
10
12
  if (child.stderr)
11
13
  child.stderr.on('data', (d) => { stderr += d.toString(); });
12
14
  let timedOut = false;
15
+ let forceKillTimer = null;
13
16
  const timer = timeout && timeout > 0 ? setTimeout(() => {
14
17
  timedOut = true;
15
18
  try {
16
19
  child.kill();
17
20
  }
18
21
  catch { }
19
- resolve({ exitCode: null, stdout: stdout.trim(), stderr: stderr.trim() });
22
+ forceKillTimer = setTimeout(() => {
23
+ try {
24
+ child.kill('SIGKILL');
25
+ }
26
+ catch { }
27
+ }, 1000);
28
+ forceKillTimer.unref();
20
29
  }, timeout) : null;
21
30
  child.on('close', (code) => {
22
31
  if (timer)
23
32
  clearTimeout(timer);
24
- if (timedOut)
25
- return;
26
- resolve({ exitCode: code, stdout: stdout.trim(), stderr: stderr.trim() });
33
+ if (forceKillTimer)
34
+ clearTimeout(forceKillTimer);
35
+ resolve(timedOut
36
+ ? { exitCode: null, stdout: stdout.trim(), stderr: 'ACTION_TIMEOUT' }
37
+ : { exitCode: code, stdout: stdout.trim(), stderr: stderr.trim() });
27
38
  });
28
39
  child.on('error', (err) => {
29
40
  if (timer)
30
41
  clearTimeout(timer);
42
+ if (forceKillTimer)
43
+ clearTimeout(forceKillTimer);
31
44
  reject(err);
32
45
  });
33
46
  });
@@ -1,3 +1,4 @@
1
+ import { remainingBudget } from '../operation-budget.js';
1
2
  import { execFile, spawn, execSync, spawnSync } from "child_process";
2
3
  import { promises as fsPromises } from 'fs';
3
4
  import path from 'path';
@@ -94,6 +95,7 @@ export function validateBundleId(bundleId) {
94
95
  export function execCommand(args, deviceId = "booted") {
95
96
  return new Promise((resolve, reject) => {
96
97
  // Use spawn for better stream control and consistency with Android implementation
98
+ const budgetMs = remainingBudget();
97
99
  const child = spawn(getXcrunCmd(), args);
98
100
  let stdout = '';
99
101
  let stderr = '';
@@ -109,7 +111,7 @@ export function execCommand(args, deviceId = "booted") {
109
111
  }
110
112
  const DEFAULT_XCRUN_LOG_TIMEOUT = parseInt(process.env.MCP_XCRUN_LOG_TIMEOUT || '', 10) || 30000; // env (ms) or default 30s
111
113
  const DEFAULT_XCRUN_CMD_TIMEOUT = parseInt(process.env.MCP_XCRUN_TIMEOUT || '', 10) || 60000; // env (ms) or default 60s
112
- const timeoutMs = args.includes('log') ? DEFAULT_XCRUN_LOG_TIMEOUT : DEFAULT_XCRUN_CMD_TIMEOUT; // choose appropriate timeout
114
+ const timeoutMs = budgetMs || (args.includes('log') ? DEFAULT_XCRUN_LOG_TIMEOUT : DEFAULT_XCRUN_CMD_TIMEOUT); // choose appropriate timeout
113
115
  const timeout = setTimeout(() => {
114
116
  child.kill();
115
117
  reject(new Error(`Command timed out after ${timeoutMs}ms: ${getXcrunCmd()} ${args.join(' ')}`));
@@ -130,10 +132,11 @@ export function execCommand(args, deviceId = "booted") {
130
132
  });
131
133
  }
132
134
  export function execCommandWithDiagnostics(args, deviceId = "booted") {
135
+ const budgetMs = remainingBudget();
133
136
  // Run synchronously to capture stdout/stderr and exitCode reliably for diagnostics
134
137
  const DEFAULT_XCRUN_LOG_TIMEOUT = parseInt(process.env.MCP_XCRUN_LOG_TIMEOUT || '', 10) || 30000;
135
138
  const DEFAULT_XCRUN_CMD_TIMEOUT = parseInt(process.env.MCP_XCRUN_TIMEOUT || '', 10) || 60000;
136
- const timeoutMs = args.includes('log') ? DEFAULT_XCRUN_LOG_TIMEOUT : DEFAULT_XCRUN_CMD_TIMEOUT;
139
+ const timeoutMs = budgetMs || (args.includes('log') ? DEFAULT_XCRUN_LOG_TIMEOUT : DEFAULT_XCRUN_CMD_TIMEOUT);
137
140
  const res = spawnSync(getXcrunCmd(), args, { encoding: 'utf8', timeout: timeoutMs });
138
141
  const runResult = {
139
142
  exitCode: typeof res.status === 'number' ? res.status : null,
@@ -0,0 +1,25 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ const budgets = new AsyncLocalStorage();
3
+ export function hasFiniteBudget() { return Number.isFinite(budgets.getStore()?.deadline); }
4
+ export function runWithBudget(timeoutMs, operation) {
5
+ const parent = budgets.getStore();
6
+ const budget = { deadline: Math.min(parent?.deadline ?? Infinity, performance.now() + timeoutMs), dispatchStarted: false, parent };
7
+ return budgets.run(budget, () => operation(budget));
8
+ }
9
+ export function remainingBudget(fallback = 0) {
10
+ const budget = budgets.getStore();
11
+ if (!budget || !Number.isFinite(budget.deadline))
12
+ return fallback;
13
+ const remaining = Math.floor(budget.deadline - performance.now());
14
+ if (remaining <= 0)
15
+ throw new Error('ACTION_TIMEOUT');
16
+ return fallback > 0 ? Math.min(fallback, remaining) : remaining;
17
+ }
18
+ export function markDispatchStarted() {
19
+ remainingBudget();
20
+ let budget = budgets.getStore();
21
+ while (budget) {
22
+ budget.dispatchStarted = true;
23
+ budget = budget.parent;
24
+ }
25
+ }
@@ -1,3 +1,6 @@
1
+ import { retainConnectedDeviceSnapshots } from '../observe/snapshot-metadata.js';
2
+ import { measure } from './timing.js';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
1
4
  import { listAndroidDevices } from "./android/utils.js";
2
5
  import { listIOSDevices } from "./ios/utils.js";
3
6
  function parseNumericVersion(v) {
@@ -32,9 +35,17 @@ export async function listDevices(platform, appId) {
32
35
  }
33
36
  return iosDeviceLister(appId);
34
37
  }
38
+ const deviceContext = new AsyncLocalStorage();
39
+ export function withResolvedDevice(device, operation) {
40
+ return deviceContext.run(device, operation);
41
+ }
35
42
  export async function resolveTargetDevice(opts) {
36
43
  const { platform, appId, prefer, deviceId } = opts;
37
- const devices = await listDevices(platform, appId);
44
+ const current = deviceContext.getStore();
45
+ if (current && current.platform === platform && (!deviceId || current.id === deviceId))
46
+ return current;
47
+ const devices = await measure('device_resolution_ms', () => listDevices(platform, appId));
48
+ retainConnectedDeviceSnapshots(platform, devices.map(device => device.id));
38
49
  // During unit tests (no adb/xcrun available), provide a lightweight mock device so
39
50
  // the observe/interact unit tests can run without real devices.
40
51
  if ((!devices || devices.length === 0) && (process.env.NODE_ENV === 'test' || process.env.MCP_TEST_MOCK_DEVICES === '1')) {
@@ -0,0 +1,19 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ const timings = new AsyncLocalStorage();
3
+ export function withTiming(values, operation) { return timings.run(values, operation); }
4
+ export function recordTiming(component, milliseconds) {
5
+ const values = timings.getStore();
6
+ if (values)
7
+ values[component] = (values[component] ?? 0) + milliseconds;
8
+ }
9
+ export async function measure(component, operation) {
10
+ const start = performance.now();
11
+ try {
12
+ return await operation();
13
+ }
14
+ finally {
15
+ const values = timings.getStore();
16
+ if (values)
17
+ values[component] = (values[component] ?? 0) + performance.now() - start;
18
+ }
19
+ }
@@ -1,3 +1,4 @@
1
+ import { remainingBudget } from '../operation-budget.js';
1
2
  import crypto from 'crypto';
2
3
  const ANDROID_STRUCTURAL_TYPES = ['Window', 'Application', 'View', 'ViewGroup', 'LinearLayout', 'FrameLayout', 'RelativeLayout', 'ScrollView', 'RecyclerView', 'TextView', 'ImageView'];
3
4
  const IOS_STRUCTURAL_TYPES = ['Window', 'Application', 'View', 'ViewController', 'UITableView', 'UICollectionView', 'UILabel', 'UIImageView', 'UIView', 'UIWindow', 'UIStackView', 'UITextView', 'UITableViewCell'];
@@ -140,6 +141,7 @@ export async function scrollToElementShared(opts) {
140
141
  const duration = 300;
141
142
  let scrollsPerformed = 0;
142
143
  for (let i = 0; i < maxScrolls; i++) {
144
+ remainingBudget();
143
145
  const { x1, y1, x2, y2 } = computeCoords();
144
146
  try {
145
147
  await swipe(x1, y1, x2, y2, duration, deviceId);
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to the **Mobile Debug MCP** project will be documented in this file.
4
4
 
5
+ ## [0.32.1]
6
+ - Further performance enhancements
7
+
8
+ ## [0.32.0]
9
+ - Added bounded interaction verification modes, compact responses, selector-based taps, and batched journeys to reduce MCP interaction latency.
10
+
5
11
  ## [0.31.0]
6
12
  - Added tool discovery/automation
7
13
 
package/docs/ROADMAP.md CHANGED
@@ -11,6 +11,7 @@
11
11
  - Actionability Resolution
12
12
  - Environment Auto-Configuration and Toolchain Discovery
13
13
  - Adjustable Control Support
14
+ - UI-Tree Observation Performance (Android implemented; iOS latency unmeasured)
14
15
 
15
16
  ## Todo
16
17
 
@@ -21,6 +22,14 @@
21
22
  - Pinch to Zoom
22
23
  - Advanced Trace Correlation and Analysis
23
24
 
25
+ # UI-Tree Observation Performance
26
+
27
+ **Status:** Android implemented and benchmarked; iOS latency remains unmeasured — [Specification 019](specs/019-ui-tree-observation-performance/spec.md)
28
+
29
+ Measure physical hierarchy reads and their cost, then reduce the measured hot path while preserving fresh post-action and asynchronous UI evidence. Validate the change against the existing static journey and a controlled network-backed screen before claiming a speedup.
30
+
31
+ Exit criteria and performance gates are defined in Specification 019.
32
+
24
33
  # Stronger State Verification
25
34
 
26
35
  ## Rationale
@@ -0,0 +1,26 @@
1
+ # Interaction latency benchmark
2
+
3
+ Run after `npm run verify`:
4
+
5
+ ```sh
6
+ MCP_ADB_TIMEOUT=30000 LATENCY_DEVICE_ID=emulator-5554 npx tsx test/device/automated/latency/benchmark.ts
7
+ ```
8
+
9
+ Use the named Pixel 9 Pro Android 16 emulator with SDK platform 36, build-tools
10
+ 36.0.0, and a Java installation capable of `javac --release 8`. Set its display
11
+ to 720x1600 at density 240 to keep repeated UI hierarchy reads responsive; the
12
+ report records the image, API, display size, density, and Android command timeout.
13
+ The harness builds and signs its own offline fixture. It resets that fixture
14
+ only; no other app data is cleared.
15
+
16
+ The sequential protocol compares the legacy handle-based public tool flow with
17
+ full/debug and light/compact journeys through a spawned stdio MCP server. Each
18
+ mode has three warm-ups and ten measured runs. Screenshot and screenshot-free
19
+ debug snapshot calls are measured separately. JSON and Markdown reports record
20
+ fixture hash, environment, request sequence, failures, and percentiles. Failed
21
+ runs do not enter percentiles. Raw observations and device serials are not saved.
22
+ See [iOS simulator validation](ios-validation.md) for the interaction checks.
23
+
24
+ The process exits unsuccessfully on >10% full p95 regression or <30% light p50
25
+ improvement. A failed run is reported and excluded from percentiles as the spec
26
+ requires; inspect its selector and tool or step before accepting the result.