mobile-debug-mcp 0.32.4 → 0.34.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 (170) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +14 -0
  3. package/.cursor-plugin/marketplace.json +16 -0
  4. package/.github/copilot-instructions.md +7 -0
  5. package/.github/workflows/ci.yml +21 -0
  6. package/AGENTS.md +21 -10
  7. package/README.md +69 -4
  8. package/agent-plugin/.claude-plugin/plugin.json +20 -0
  9. package/agent-plugin/.codex-plugin/plugin.json +34 -0
  10. package/agent-plugin/.cursor-plugin/plugin.json +23 -0
  11. package/agent-plugin/.mcp.json +14 -0
  12. package/agent-plugin/README.md +9 -3
  13. package/agent-plugin/assets/mobile-debug-tools.svg +28 -0
  14. package/agent-plugin/mcp.json +1 -1
  15. package/agent-plugin/plugin.json +1 -1
  16. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -1
  17. package/dist/debugger/android.js +78 -0
  18. package/dist/debugger/guard.js +30 -0
  19. package/dist/debugger/sessions.js +262 -0
  20. package/dist/debugger/worker.js +165 -0
  21. package/dist/interact/android.js +10 -4
  22. package/dist/interact/conditional-action.js +68 -0
  23. package/dist/interact/index.js +70 -14
  24. package/dist/interact/ios.js +14 -8
  25. package/dist/manage/android.js +4 -3
  26. package/dist/manage/ios.js +3 -2
  27. package/dist/observe/android.js +25 -4
  28. package/dist/observe/evidence.js +280 -0
  29. package/dist/observe/index.js +33 -6
  30. package/dist/observe/ios.js +25 -6
  31. package/dist/observe/readiness-service.js +124 -0
  32. package/dist/observe/readiness.js +88 -0
  33. package/dist/observe/recording.js +165 -0
  34. package/dist/observe/snapshot-metadata.js +10 -2
  35. package/dist/observe/system-trace-android.js +135 -0
  36. package/dist/observe/system-trace.js +285 -0
  37. package/dist/server/common.js +9 -0
  38. package/dist/server/condition-schema.js +32 -0
  39. package/dist/server/debugger-handlers.js +38 -0
  40. package/dist/server/debugger-schema.js +42 -0
  41. package/dist/server/journey-schema.js +4 -2
  42. package/dist/server/readiness-schema.js +12 -0
  43. package/dist/server/system-trace-schema.js +17 -0
  44. package/dist/server/tool-definitions.js +51 -0
  45. package/dist/server/tool-handlers.js +294 -105
  46. package/dist/server-core.js +3 -3
  47. package/dist/server.js +34 -0
  48. package/dist/system/index.js +5 -4
  49. package/dist/utils/exec.js +2 -0
  50. package/dist/utils/ios/utils.js +4 -4
  51. package/dist/utils/java.js +14 -12
  52. package/dist/utils/operation-budget.js +26 -2
  53. package/dist/utils/resolve-device.js +9 -0
  54. package/docs/CHANGELOG.md +15 -0
  55. package/docs/specs/020-conditional-actions/intent.md +62 -0
  56. package/docs/specs/020-conditional-actions/plan.md +214 -0
  57. package/docs/specs/020-conditional-actions/spec.md +301 -0
  58. package/docs/specs/021-app-readiness-signals/intent.md +40 -0
  59. package/docs/specs/021-app-readiness-signals/plan.md +102 -0
  60. package/docs/specs/021-app-readiness-signals/spec.md +144 -0
  61. package/docs/specs/022-correlated-evidence/device-evidence.json +60 -0
  62. package/docs/specs/022-correlated-evidence/intent.md +37 -0
  63. package/docs/specs/022-correlated-evidence/modul8-evidence.json +1062 -0
  64. package/docs/specs/022-correlated-evidence/modul8-selector-fix-evidence.json +336 -0
  65. package/docs/specs/022-correlated-evidence/plan.md +171 -0
  66. package/docs/specs/022-correlated-evidence/spec.md +123 -0
  67. package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
  68. package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
  69. package/docs/specs/023-android-system-traces/intent.md +40 -0
  70. package/docs/specs/023-android-system-traces/plan.md +117 -0
  71. package/docs/specs/023-android-system-traces/spec.md +120 -0
  72. package/docs/specs/024-android-heap-dump-capture/intent.md +39 -0
  73. package/docs/specs/025-android-runtime-debugger/intent.md +40 -0
  74. package/docs/specs/025-android-runtime-debugger/plan.md +104 -0
  75. package/docs/specs/025-android-runtime-debugger/spec.md +182 -0
  76. package/docs/specs/process.md +213 -0
  77. package/docs/tools/TOOLS.md +5 -0
  78. package/docs/tools/debugger.md +104 -0
  79. package/docs/tools/evidence.md +82 -0
  80. package/docs/tools/interact.md +125 -2
  81. package/docs/tools/observe.md +70 -0
  82. package/docs/tools/system-traces.md +126 -0
  83. package/package.json +1 -1
  84. package/scripts/agent-plugin.mjs +68 -8
  85. package/src/debugger/android.ts +65 -0
  86. package/src/debugger/guard.ts +26 -0
  87. package/src/debugger/sessions.ts +168 -0
  88. package/src/debugger/worker.ts +122 -0
  89. package/src/interact/android.ts +10 -4
  90. package/src/interact/conditional-action.ts +75 -0
  91. package/src/interact/index.ts +57 -28
  92. package/src/interact/ios.ts +14 -8
  93. package/src/manage/android.ts +4 -3
  94. package/src/manage/ios.ts +3 -2
  95. package/src/observe/android.ts +33 -6
  96. package/src/observe/evidence.ts +262 -0
  97. package/src/observe/index.ts +32 -8
  98. package/src/observe/ios.ts +32 -8
  99. package/src/observe/readiness-service.ts +94 -0
  100. package/src/observe/readiness.ts +84 -0
  101. package/src/observe/snapshot-metadata.ts +12 -1
  102. package/src/observe/system-trace-android.ts +133 -0
  103. package/src/observe/system-trace.ts +218 -0
  104. package/src/server/common.ts +7 -0
  105. package/src/server/condition-schema.ts +32 -0
  106. package/src/server/debugger-handlers.ts +29 -0
  107. package/src/server/debugger-schema.ts +37 -0
  108. package/src/server/journey-schema.ts +4 -2
  109. package/src/server/readiness-schema.ts +12 -0
  110. package/src/server/system-trace-schema.ts +17 -0
  111. package/src/server/tool-definitions.ts +49 -0
  112. package/src/server/tool-handlers.ts +182 -21
  113. package/src/server-core.ts +3 -3
  114. package/src/server.ts +24 -0
  115. package/src/system/index.ts +7 -5
  116. package/src/types.ts +30 -1
  117. package/src/utils/exec.ts +2 -0
  118. package/src/utils/ios/utils.ts +4 -4
  119. package/src/utils/java.ts +13 -12
  120. package/src/utils/operation-budget.ts +24 -2
  121. package/src/utils/resolve-device.ts +10 -0
  122. package/test/device/automated/debugger/runtime.android.test.ts +203 -0
  123. package/test/device/automated/observe/correlated-evidence.smoke.ts +70 -0
  124. package/test/device/automated/observe/system-trace.smoke.ts +104 -0
  125. package/test/device/manual/interact/conditional_actions.manual.ts +95 -0
  126. package/test/device/manual/interact/modul8_selector_tap.manual.ts +63 -0
  127. package/test/device/manual/interact/readiness.manual.ts +99 -0
  128. package/test/fixtures/debugger-app/MainActivity.java +46 -0
  129. package/test/fixtures/debugger-app/NoLocals.java +7 -0
  130. package/test/fixtures/debugger-app/README.md +13 -0
  131. package/test/fixtures/debugger-app/build.ts +29 -0
  132. package/test/fixtures/readiness-app/MainActivity.java +23 -0
  133. package/test/fixtures/readiness-app/README.md +12 -0
  134. package/test/fixtures/readiness-app/build.ts +37 -0
  135. package/test/fixtures/readiness-app/main.m +27 -0
  136. package/test/helpers/perfetto.ts +59 -0
  137. package/test/unit/debugger/guards.test.ts +46 -0
  138. package/test/unit/debugger/native.test.ts +33 -0
  139. package/test/unit/debugger/sessions.test.ts +83 -0
  140. package/test/unit/observe/android_tree_acquisition.test.ts +2 -0
  141. package/test/unit/observe/capture_debug_snapshot.test.ts +42 -32
  142. package/test/unit/observe/compact_tree_error.test.ts +24 -0
  143. package/test/unit/observe/conditional_snapshots.test.ts +33 -0
  144. package/test/unit/observe/correlated-evidence.test.ts +128 -0
  145. package/test/unit/observe/ios_tree_acquisition.test.ts +2 -0
  146. package/test/unit/observe/readiness.test.ts +41 -0
  147. package/test/unit/observe/readiness_native.test.ts +68 -0
  148. package/test/unit/observe/system-trace-android.test.ts +52 -0
  149. package/test/unit/observe/system-trace.test.ts +166 -0
  150. package/test/unit/server/conditional_actions.test.ts +299 -0
  151. package/test/unit/server/conditional_schema.test.ts +59 -0
  152. package/test/unit/server/correlated-evidence.test.ts +194 -0
  153. package/test/unit/server/readiness.test.ts +170 -0
  154. package/test/unit/server/selector_tap_target.test.ts +79 -0
  155. package/test/unit/server/system-trace.test.ts +64 -0
  156. package/test/unit/server/tap_selector.test.ts +2 -0
  157. package/worker/DebuggerWorker.java +160 -0
  158. package/skills/mobile-debug-tools-usage/SKILL.md +0 -96
  159. /package/{skills → .agents/skills}/README.md +0 -0
  160. /package/{skills → .agents/skills}/mcp-builder/SKILL.md +0 -0
  161. /package/{skills → .agents/skills}/mcp-builder/references/build-flags.md +0 -0
  162. /package/{skills → .agents/skills}/mcp-builder/references/diagnostics-schema.md +0 -0
  163. /package/{skills → .agents/skills}/mcp-builder/references/toolchain-details.md +0 -0
  164. /package/{skills → .agents/skills}/spec-review/SKILL.md +0 -0
  165. /package/{skills → .agents/skills}/spec-review/references/spec-review-checklist.md +0 -0
  166. /package/{skills → .agents/skills}/spec-review/references/spec-review-template.md +0 -0
  167. /package/{skills → .agents/skills}/test-authoring/SKILL.md +0 -0
  168. /package/{skills → .agents/skills}/test-authoring/references/repo-test-layout.md +0 -0
  169. /package/{skills → .agents/skills}/test-authoring/references/test-authoring-workflow.md +0 -0
  170. /package/{skills → .agents/skills}/test-authoring/references/test-quality-checklist.md +0 -0
@@ -6,7 +6,7 @@ import { handleToolCall } from './server/tool-handlers.js';
6
6
  export { wrapResponse, toolDefinitions, handleToolCall };
7
7
  export const serverInfo = {
8
8
  name: 'mobile-debug-mcp',
9
- version: '0.32.4'
9
+ version: '0.34.0'
10
10
  };
11
11
  export function createServer() {
12
12
  const server = new Server(serverInfo, {
@@ -27,9 +27,9 @@ export function createServer() {
27
27
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
28
28
  tools: toolDefinitions
29
29
  }));
30
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
30
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
31
31
  const { name, arguments: args } = request.params;
32
- return handleToolCall(name, args);
32
+ return handleToolCall(name, args, extra.signal);
33
33
  });
34
34
  return server;
35
35
  }
package/dist/server.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { debuggerSessions } from './debugger/sessions.js';
3
+ import { systemTraces } from './observe/system-trace.js';
2
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
5
  import { createServer } from './server-core.js';
4
6
  import { getSystemStatus } from './system/index.js';
@@ -9,6 +11,38 @@ if (process.env.MOBILE_DEBUG_MCP_STARTUP_HEALTHCHECK === '1') {
9
11
  }).catch((e) => console.warn('[startup] healthcheck failed:', e instanceof Error ? e.message : String(e)));
10
12
  }
11
13
  const transport = new StdioServerTransport();
14
+ let shuttingDown = false;
15
+ async function shutdown() {
16
+ if (shuttingDown)
17
+ return;
18
+ shuttingDown = true;
19
+ const deadline = setTimeout(() => { console.error('System trace shutdown cleanup timed out; owned device captures remain duration-bounded.'); process.exit(1); }, 10000);
20
+ let exitCode = 0;
21
+ try {
22
+ const [remaining, debuggers] = await Promise.all([systemTraces.shutdown(), debuggerSessions.shutdown()]);
23
+ if (debuggers.length) {
24
+ exitCode = 1;
25
+ console.error('Debugger shutdown cleanup incomplete', debuggers.map(s => s.cleanup));
26
+ }
27
+ if (remaining.length) {
28
+ exitCode = 1;
29
+ console.error('System trace shutdown cleanup incomplete:', remaining.map(s => ({ trace_id: s.trace_id, device: s.device, cleanup: s.cleanup })));
30
+ }
31
+ await server.close();
32
+ }
33
+ catch {
34
+ exitCode = 1;
35
+ console.error('Server shutdown cleanup failed.');
36
+ }
37
+ finally {
38
+ clearTimeout(deadline);
39
+ process.exit(exitCode);
40
+ }
41
+ }
42
+ server.onclose = () => { void shutdown(); };
43
+ process.stdin.on('end', () => { void shutdown(); });
44
+ process.on('SIGINT', () => { void shutdown(); });
45
+ process.on('SIGTERM', () => { void shutdown(); });
12
46
  async function main() {
13
47
  await server.connect(transport);
14
48
  }
@@ -221,7 +221,7 @@ function buildAndroidPlatform(android, hostSupported) {
221
221
  },
222
222
  providers: {
223
223
  adb: { available: adbAvailable, selected: true, tool: 'adb' },
224
- jdwp: { available: adbAvailable && android.devices > 0, selected: false, tool: 'adb' }
224
+ jdwp: { available: false, selected: false, tool: 'adb', probeRequired: 'get_debugger_capabilities' }
225
225
  },
226
226
  capabilities: {
227
227
  device_discovery: {
@@ -255,9 +255,10 @@ function buildAndroidPlatform(android, hostSupported) {
255
255
  blockingFailure: logAvailable ? null : adbFailure ?? envFailure
256
256
  },
257
257
  debug: {
258
- availability: adbAvailable && android.devices > 0,
259
- selectedProvider: adbAvailable && android.devices > 0 ? 'JDWP' : null,
260
- blockingFailure: adbAvailable && android.devices > 0 ? null : deviceFailure ?? adbFailure ?? envFailure
258
+ probeRequired: 'get_debugger_capabilities',
259
+ availability: false,
260
+ selectedProvider: null,
261
+ blockingFailure: deviceFailure ?? adbFailure ?? envFailure
261
262
  }
262
263
  },
263
264
  devices: deviceEntries,
@@ -1,9 +1,11 @@
1
+ import { checkDebuggerGuard } from '../debugger/guard.js';
1
2
  import { remainingBudget } from './operation-budget.js';
2
3
  import { spawn } from 'child_process';
3
4
  export async function execCmd(cmd, args, opts = {}) {
4
5
  const { env, cwd, shell } = opts;
5
6
  const timeout = remainingBudget(opts.timeout ?? 0);
6
7
  return new Promise((resolve, reject) => {
8
+ checkDebuggerGuard();
7
9
  const child = spawn(cmd, args, { env: { ...process.env, ...(env || {}) }, cwd, shell });
8
10
  let stdout = '';
9
11
  let stderr = '';
@@ -75,7 +75,7 @@ export async function isIDBInstalled() {
75
75
  if (!cmd)
76
76
  return false;
77
77
  try {
78
- execSync(`${cmd} list-targets --json`, { stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 });
78
+ execSync(`${cmd} list-targets --json`, { stdio: ['ignore', 'pipe', 'pipe'], timeout: remainingBudget(5000), killSignal: 'SIGKILL' });
79
79
  return true;
80
80
  }
81
81
  catch (e) {
@@ -197,7 +197,7 @@ export async function findAppBundle(dir) {
197
197
  export async function getIOSDeviceMetadata(deviceId = "booted") {
198
198
  return new Promise((resolve) => {
199
199
  // If deviceId is provided (and not "booted"), attempt to find that device among booted simulators.
200
- execFile(getXcrunCmd(), ['simctl', 'list', 'devices', 'booted', '--json'], (err, stdout) => {
200
+ execFile(getXcrunCmd(), ['simctl', 'list', 'devices', 'booted', '--json'], { timeout: remainingBudget(), killSignal: 'SIGKILL' }, (err, stdout) => {
201
201
  const fallback = {
202
202
  platform: "ios",
203
203
  id: deviceId,
@@ -240,7 +240,7 @@ export async function getIOSDeviceMetadata(deviceId = "booted") {
240
240
  export async function listIOSDevices(appId) {
241
241
  return new Promise((resolve) => {
242
242
  // Query all devices and separately query booted devices to mark them
243
- execFile(getXcrunCmd(), ['simctl', 'list', 'devices', '--json'], (err, stdout) => {
243
+ execFile(getXcrunCmd(), ['simctl', 'list', 'devices', '--json'], { timeout: remainingBudget(), killSignal: 'SIGKILL' }, (err, stdout) => {
244
244
  if (err || !stdout)
245
245
  return resolve([]);
246
246
  try {
@@ -249,7 +249,7 @@ export async function listIOSDevices(appId) {
249
249
  const out = [];
250
250
  const checks = [];
251
251
  // Get booted devices set
252
- execFile(getXcrunCmd(), ['simctl', 'list', 'devices', 'booted', '--json'], (err2, stdout2) => {
252
+ execFile(getXcrunCmd(), ['simctl', 'list', 'devices', 'booted', '--json'], { timeout: remainingBudget(), killSignal: 'SIGKILL' }, (err2, stdout2) => {
253
253
  const bootedSet = new Set();
254
254
  if (!err2 && stdout2) {
255
255
  try {
@@ -25,17 +25,19 @@ function javaVersionOf(javaBin) {
25
25
  return undefined;
26
26
  }
27
27
  }
28
- export async function detectJavaHome() {
28
+ export async function detectJavaHome(options = {}) {
29
+ const debug = (...args) => { if (!options.quiet)
30
+ console.debug(...args); };
29
31
  try {
30
32
  // 1) Honor explicit ANDROID_STUDIO_JDK env (highest priority)
31
33
  const envStudio = process.env.ANDROID_STUDIO_JDK || process.env.ANDROID_STUDIO_JBR;
32
34
  if (envStudio && existsSync(path.join(envStudio, 'bin', 'java'))) {
33
35
  const v = javaVersionOf(path.join(envStudio, 'bin', 'java'));
34
36
  if (isJavaVersionAcceptable(v)) {
35
- console.debug('[java.detect] Using ANDROID_STUDIO_JDK from env:', envStudio);
37
+ debug('[java.detect] Using ANDROID_STUDIO_JDK from env:', envStudio);
36
38
  return envStudio;
37
39
  }
38
- console.debug('[java.detect] ANDROID_STUDIO_JDK present but java -version did not match expected versions');
40
+ debug('[java.detect] ANDROID_STUDIO_JDK present but java -version did not match expected versions');
39
41
  }
40
42
  // 2) Android Studio JBR candidates (prefer these over JAVA_HOME)
41
43
  const jbrCandidates = [
@@ -53,7 +55,7 @@ export async function detectJavaHome() {
53
55
  if (existsSync(javaBin)) {
54
56
  const v = javaVersionOf(javaBin);
55
57
  if (isJavaVersionAcceptable(v)) {
56
- console.debug('[java.detect] Found Android Studio JBR at:', p);
58
+ debug('[java.detect] Found Android Studio JBR at:', p);
57
59
  return p;
58
60
  }
59
61
  }
@@ -64,13 +66,13 @@ export async function detectJavaHome() {
64
66
  const javaBin = path.join(process.env.JAVA_HOME, 'bin', 'java');
65
67
  const v = javaVersionOf(javaBin);
66
68
  if (isJavaVersionAcceptable(v)) {
67
- console.debug('[java.detect] Using JAVA_HOME from env:', process.env.JAVA_HOME);
69
+ debug('[java.detect] Using JAVA_HOME from env:', process.env.JAVA_HOME);
68
70
  return process.env.JAVA_HOME;
69
71
  }
70
- console.debug('[java.detect] Existing JAVA_HOME does not appear to be acceptable Java (17/21), will search');
72
+ debug('[java.detect] Existing JAVA_HOME does not appear to be acceptable Java (17/21), will search');
71
73
  }
72
74
  catch {
73
- console.debug('[java.detect] Failed to validate existing JAVA_HOME, searching for JDK');
75
+ debug('[java.detect] Failed to validate existing JAVA_HOME, searching for JDK');
74
76
  }
75
77
  }
76
78
  // 4) macOS explicit path for JDK 17
@@ -84,7 +86,7 @@ export async function detectJavaHome() {
84
86
  return out17;
85
87
  }
86
88
  catch (e) {
87
- console.debug('[java.detect] /usr/libexec/java_home -v 17 failed: ' + String(e));
89
+ debug('[java.detect] /usr/libexec/java_home -v 17 failed: ' + String(e));
88
90
  }
89
91
  try {
90
92
  const out21 = execSync('/usr/libexec/java_home -v 21', { stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
@@ -92,7 +94,7 @@ export async function detectJavaHome() {
92
94
  return out21;
93
95
  }
94
96
  catch (e) {
95
- console.debug('[java.detect] /usr/libexec/java_home -v 21 failed: ' + String(e));
97
+ debug('[java.detect] /usr/libexec/java_home -v 21 failed: ' + String(e));
96
98
  }
97
99
  // 6) macOS common JDK locations
98
100
  try {
@@ -105,7 +107,7 @@ export async function detectJavaHome() {
105
107
  }
106
108
  }
107
109
  catch (e) {
108
- console.debug('[java.detect] listing /Library/Java/JavaVirtualMachines failed: ' + String(e));
110
+ debug('[java.detect] listing /Library/Java/JavaVirtualMachines failed: ' + String(e));
109
111
  }
110
112
  // 7) Linux locations
111
113
  const linuxCandidates = [
@@ -122,12 +124,12 @@ export async function detectJavaHome() {
122
124
  return p;
123
125
  }
124
126
  catch (e) {
125
- console.debug(`[java.detect] checking linux candidate ${p} failed: ${String(e)}`);
127
+ debug(`[java.detect] checking linux candidate ${p} failed: ${String(e)}`);
126
128
  }
127
129
  }
128
130
  }
129
131
  catch (e) {
130
- console.debug('[java.detect] error detecting java home:', e instanceof Error ? e.message : String(e));
132
+ debug('[java.detect] error detecting java home:', e instanceof Error ? e.message : String(e));
131
133
  }
132
134
  return undefined;
133
135
  }
@@ -1,3 +1,4 @@
1
+ import { checkDebuggerGuard } from '../debugger/guard.js';
1
2
  import { AsyncLocalStorage } from 'node:async_hooks';
2
3
  const budgets = new AsyncLocalStorage();
3
4
  export function hasFiniteBudget() { return Number.isFinite(budgets.getStore()?.deadline); }
@@ -8,14 +9,37 @@ export function runWithBudget(timeoutMs, operation) {
8
9
  }
9
10
  export function remainingBudget(fallback = 0) {
10
11
  const budget = budgets.getStore();
11
- if (!budget || !Number.isFinite(budget.deadline))
12
+ let deadline = budget?.deadline ?? Infinity;
13
+ for (let ancestor = budget?.parent; ancestor; ancestor = ancestor.parent)
14
+ deadline = Math.min(deadline, ancestor.deadline);
15
+ if (!Number.isFinite(deadline))
12
16
  return fallback;
13
- const remaining = Math.floor(budget.deadline - performance.now());
17
+ const remaining = Math.floor(deadline - performance.now());
14
18
  if (remaining <= 0)
15
19
  throw new Error('ACTION_TIMEOUT');
16
20
  return fallback > 0 ? Math.min(fallback, remaining) : remaining;
17
21
  }
22
+ // Pause only this action's budget. Any enclosing operation still bounds the read.
23
+ export async function runWithSeparateBudget(timeoutMs, operation) {
24
+ remainingBudget();
25
+ const action = budgets.getStore();
26
+ const started = performance.now();
27
+ const parent = action?.parent;
28
+ const budget = { deadline: Math.min(parent?.deadline ?? Infinity, started + timeoutMs), dispatchStarted: false, parent };
29
+ try {
30
+ return await budgets.run(budget, async () => {
31
+ const result = await operation();
32
+ remainingBudget();
33
+ return result;
34
+ });
35
+ }
36
+ finally {
37
+ if (action)
38
+ action.deadline += performance.now() - started;
39
+ }
40
+ }
18
41
  export function markDispatchStarted() {
42
+ checkDebuggerGuard(true);
19
43
  remainingBudget();
20
44
  let budget = budgets.getStore();
21
45
  while (budget) {
@@ -1,3 +1,5 @@
1
+ import { checkDebuggerGuard } from '../debugger/guard.js';
2
+ import { bindEvidenceDevice } from '../observe/evidence.js';
1
3
  import { retainConnectedDeviceSnapshots } from '../observe/snapshot-metadata.js';
2
4
  import { measure } from './timing.js';
3
5
  import { AsyncLocalStorage } from 'node:async_hooks';
@@ -37,9 +39,16 @@ export async function listDevices(platform, appId) {
37
39
  }
38
40
  const deviceContext = new AsyncLocalStorage();
39
41
  export function withResolvedDevice(device, operation) {
42
+ bindEvidenceDevice(device);
40
43
  return deviceContext.run(device, operation);
41
44
  }
42
45
  export async function resolveTargetDevice(opts) {
46
+ checkDebuggerGuard();
47
+ const device = await resolveDevice(opts);
48
+ bindEvidenceDevice(device);
49
+ return device;
50
+ }
51
+ async function resolveDevice(opts) {
43
52
  const { platform, appId, prefer, deviceId } = opts;
44
53
  const current = deviceContext.getStore();
45
54
  if (current && current.platform === platform && (!deviceId || current.id === deviceId))
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to the **Mobile Debug MCP** project will be documented in this file.
4
4
 
5
+ ## Unreleased
6
+
7
+ ## [0.34.0]
8
+ - Added optional Android JDI runtime debugging: explicit device/process attach, source breakpoints/events, bounded stacks and shallow locals, stepping/resume, stale-reference rejection, pause-aware UI guards/readiness, expiry and owned detach/cleanup. Requires a host JDK 17+ and a running debuggable app; evaluation, mutation, native/coroutine and iOS debugging remain unsupported.
9
+ - Added optional bounded Android Perfetto capture with capability/start/status/stop/cancel tools, explicit journey trace linkage, local artifacts, process-ownership checks and retryable cleanup. iOS tracing and automatic trace analysis remain unsupported.
10
+ - Made readiness unit-test polling time deterministic while retaining exact sample-count and timeout assertions, eliminating host timer-rounding flakes.
11
+ - Isolated snapshot unit tests from physical device discovery and native handlers so they run without connected devices. Readiness deadline expiry now retains the last valid observation instead of intermittently resetting loading to unknown.
12
+
13
+ ## [0.33.0]
14
+ - Fixed selector taps timing out before one normal Android UI-tree acquisition: a default single lookup now uses the action budget (5 seconds). Selector taps with exists/visible waits resolve actionable containers consistently with clickable waits; hidden/disabled labels remain protected. Acquisition timeouts retain `ACTION_TIMEOUT` instead of reporting a missing element.
15
+ - Added correlated action/observation evidence, explicit `actionRef`, phase/acquisition timestamps, conservative freshness, and bounded metadata history. Journey steps share correlation IDs; snapshot components and stream reads retain their actual device scope. Device log times remain explicitly incomparable with host dispatch times; tracing is unsupported.
16
+ - Added evidence-backed foreground UI readiness observations, bounded readiness waits, and opt-in `requireReady` for existing UI waits. Tree responses include single-sample readiness without extra reads. Unresponsive/debugger-paused signals are explicitly unsupported; iOS probes and metadata reads now respect active operation budgets.
17
+ - Added optional conditional input to `tap`, `tap_element`, `swipe`, Android `type_text`/`press_back`, and journey tap steps. A fresh snapshot check at native dispatch blocks stale or uncheckable expectations and returns condition evidence, including fresh trees in compact failures.
18
+ - Added process-scoped `snapshot_session_id` metadata to UI-tree responses so conditional expectations cannot be reused across server restarts. Successful repeated observations refresh retained revision history timestamps.
19
+
5
20
  ## [0.32.4]
6
21
  - Removed the optional Android UI-tree bridge runtime and its APK build tooling. Android UI-tree reads use `uiautomator dump` through ADB.
7
22
 
@@ -0,0 +1,62 @@
1
+ # Intent: conditional actions
2
+
3
+ Originator: repository maintainer (user in the current conversation).
4
+ Date: 2026-10-29.
5
+ Status: approved.
6
+
7
+ ## Original problem
8
+
9
+ “Conditional actions: Check expected state immediately before acting; stop and
10
+ return fresh evidence if it changed.”
11
+
12
+ Source: user request in the current conversation, following adoption of the
13
+ repository development process.
14
+
15
+ ## Desired outcome
16
+
17
+ An MCP client can make an action conditional on an expected UI state. The server
18
+ checks that expectation against a fresh observation of the same target device
19
+ immediately before dispatching the action. If the expectation no longer holds,
20
+ the server does not dispatch the action and returns fresh evidence so the client
21
+ can reassess its next step.
22
+
23
+ Affected users and systems: MCP clients/agents, interaction tool dispatch,
24
+ state observation, and Android/iOS adapters for supported actions.
25
+
26
+ ## Observable success criteria
27
+
28
+ - A matching expectation allows the requested action to proceed normally.
29
+ - A mismatching expectation prevents dispatch and clearly reports that the
30
+ action was not performed, together with the newly observed state.
31
+ - If the expected state cannot be checked reliably, the action is not dispatched;
32
+ the response explains the observation failure without inventing evidence.
33
+ - The check and action target the same device.
34
+ - A mismatch does not automatically retry the action or adopt the new state.
35
+ - Existing calls without a condition preserve their behaviour.
36
+
37
+ ## Constraints and exclusions
38
+
39
+ - Reuse existing action, observation, and response conventions where suitable.
40
+ - Define precise supported conditions, actions, evidence, and platform coverage
41
+ in the spec after approval of this intent.
42
+ - A pre-dispatch check cannot guarantee the UI remains unchanged between the
43
+ observation and native input delivery; do not claim atomic device execution.
44
+ - Automatic replanning, rollback, and post-action success verification are
45
+ outside this feature's scope.
46
+ - Preserve unrelated checkout changes.
47
+
48
+ ## Assumptions and open questions for specification
49
+
50
+ - Proposed compatibility approach: conditions are optional on supported actions.
51
+ - Determine the initial action set, including whether journey steps are covered.
52
+ - Determine whether expectations use screen identity, element properties,
53
+ snapshot revisions, or a supported combination.
54
+ - Define fresh mismatch evidence and how it is preserved in compact responses.
55
+ - Existing screen fingerprints deliberately omit some changing values; assess
56
+ their suitability rather than assuming they detect every relevant state change.
57
+
58
+ ## Approval
59
+
60
+ Approved by the originator in the current conversation on 2026-10-29: “yes”,
61
+ in direct response to the complete intent summary and linked artifact. Approval
62
+ covers the captured intent; specification approval remains a separate gate.
@@ -0,0 +1,214 @@
1
+ # Plan: conditional actions
2
+
3
+ Date: 2026-10-29. Status: complete — implementation and required verification passed.
4
+ Intent: [intent.md](intent.md). Specification: [spec.md](spec.md).
5
+ Both approved by the originator's separate “yes” replies in this conversation;
6
+ the latest approval authorises implementation of spec 020.
7
+
8
+ ## Implementation sequence
9
+
10
+ - [x] Inspect action wrapping, native input, observation/history, budgets, tests.
11
+ - [x] Add session metadata and retained successful-baseline lookup without changing
12
+ existing revision semantics or growing history.
13
+ - [x] Add strict condition schema, supported-tool validation, and journey tap wiring.
14
+ - [x] Add a request-scoped asynchronous pre-dispatch guard, pin the device, and
15
+ isolate the observation budget from the action budget (respecting outer budgets).
16
+ - [x] Integrate at Android/iOS input spawn boundaries; retain failure evidence
17
+ through adapter errors; defer guarded network-window reset until dispatch.
18
+ - [x] Preserve condition evidence and non-delivery semantics in compact/debug
19
+ responses, including journey stop and not-checked cases.
20
+ - [x] Add deterministic unit/adapter regression coverage and device journeys.
21
+ - [x] Update public docs/changelog and packaged agent usage guidance; sync/check plugin.
22
+ - [x] Run focused checks, maintainer verification, device suite, and focused device journeys.
23
+ - [x] Review changed code/contracts/callers and record results and limits.
24
+
25
+ ## Expected affected files
26
+
27
+ - `src/observe/snapshot-metadata.ts`, `src/observe/index.ts`, `src/types.ts`.
28
+ - New shared condition schema/guard modules under `src/` as responsibility requires.
29
+ - `src/utils/operation-budget.ts`, Android/iOS input adapters.
30
+ - `src/server/tool-definitions.ts`, `tool-handlers.ts`, `journey-schema.ts`, `common.ts`.
31
+ - Unit tests under `test/unit/server/`, `test/unit/observe/`, and adapter fixtures.
32
+ - Focused reusable device helper under `test/device/manual/` if explicit simulator
33
+ selection/state setup makes it unsuitable for automatic generic suite execution.
34
+ - `docs/tools/interact.md`, `observe.md`, `docs/CHANGELOG.md`, `README.md`, and
35
+ `agent-plugin/skills/mobile-debug-mcp/SKILL.md`.
36
+
37
+ ## Verification mapping
38
+
39
+ | Spec criteria | Verification |
40
+ | --- | --- |
41
+ | 1, 5, 8 | Real Android/iOS adapter calls using fake binaries; all supported actions, verification modes and response modes; unguarded compatibility tests |
42
+ | 2, 3 | Mutable hierarchy fixture changes text/bounds/enabled/state and changes during target resolution/adapter preparation; assert zero native input and exact final tree |
43
+ | 4 | Session, history expiry/eviction, device mismatch, error/malformed observation, timeout including late completion and child cleanup |
44
+ | 6 | Journey with earlier passed step, blocked conditional tap, later not-run step; both response modes |
45
+ | 7 | Runtime invalid-input matrix and advertised schemas; unsupported tools/steps rejected before any side effects |
46
+ | 9 | Overlapping guarded/unguarded calls and network-window reset spy; condition budget distinct from input budget; outer-budget expiry |
47
+ | 10 | Documentation and focused review against actual implementation |
48
+
49
+ Commands: focused test files via existing `tsx` runtime, then `npm run verify`,
50
+ `npm run test:device`, and focused device helper against built `dist/` modules.
51
+ Runtime: repository supports Node >=18; record actual Node/toolchain with results.
52
+ No new dependency, migration, helper APK, publication, or service is needed.
53
+
54
+ ## Device routes and readiness
55
+
56
+ Available during spec reconnaissance: Android 16 `emulator-5554` and booted iOS
57
+ 26.4 iPhone Air `F72108FF-C8D6-4C1E-980E-1AE5E5CCD27B`. The physical Pixel is
58
+ excluded from mutation checks. Tool access and fresh trees work on both simulators.
59
+ Before device runs, inspect current state and choose safe system controls; avoid
60
+ interacting with the open Modul8 subscription flow. Use an already-installed
61
+ system Settings app on Android/iOS where possible; no accounts or entitlements.
62
+
63
+ For each platform on the newly built server:
64
+
65
+ 1. Open system Settings, observe its tree and session/revision, repeat to check a
66
+ stable baseline, then perform a guarded safe navigation tap and assert destination.
67
+ 2. Observe the baseline, navigate to another Settings screen, then send a guarded
68
+ action with the old expectation; assert non-delivery and returned current tree.
69
+ Confirm the blocked action has no effect using a fresh state assertion.
70
+ 3. Run a journey with an earlier successful wait/assert, an old-revision guarded
71
+ tap, and a later action. Confirm the tap fails, later step is not run, and state
72
+ stays unchanged. Repeat relevant compact/debug evidence checks.
73
+
74
+ Exact selectors depend on the current simulator OS tree and will be recorded
75
+ with results. Restore original foreground apps where practical; do not alter
76
+ settings, app data, or subscriptions. Owner of local prerequisites: current task.
77
+ Unavailable/timeout and last-moment preparation races use controlled fake binaries;
78
+ no forced device disconnect or privileged settings change is needed. This is
79
+ reported as simulated error-path evidence, separate from real-device passes.
80
+
81
+ ## Results
82
+
83
+ Implementation is present. Node.js v23.11.0 was used locally (the supported
84
+ minimum is >=18; CI's Node 18 run has not been executed locally).
85
+
86
+ ### Automated checks
87
+
88
+ - Initial `npm run verify`: PASS, including lint, build, unit tests, plugin check.
89
+ - Focused conditional action regression: PASS using production Android/iOS input
90
+ adapters and controlled fake native binaries. Coverage includes all supported
91
+ actions, each verification/response mode, late target-preparation changes,
92
+ mismatches, unavailable/malformed observations, session/history/device rejection,
93
+ request isolation, native input failure, and journey stop. Complete native
94
+ observation/input paths also pass with fake ADB/idb, including child-close
95
+ evidence for condition timeouts. This is simulated platform evidence.
96
+ - Focused schema parity and retention tests: PASS. Advertised JSON schemas are
97
+ exercised using the existing MCP SDK's validator, alongside runtime schemas.
98
+ All elements contribute to revision checks beyond the bounded delta history.
99
+ - `npm run sync:agent-plugin`: PASS; no generated manifest differences.
100
+ - Final `npm run verify`: PASS (lint, build, 59 unit-test files, plugin consistency).
101
+ Log: `/tmp/mobile-debug-conditional-verify-final.log`.
102
+ - Initial `npm run test:device`: FAIL at iOS UI-tree smoke (`Invalid UI tree resolution`) while the original simulator was booting.
103
+ Android screenshot/log/tree wrappers passed. iOS screenshot wrapper passed;
104
+ the iOS log wrapper also reported pass but its underlying log retrieval printed
105
+ `getpwuid_r` / unavailable log-store errors, so this is not evidence of usable
106
+ iOS logs. These wrappers use automatic device selection; focused tests below
107
+ pin their simulator explicitly. Full log: `/tmp/mobile-debug-conditional-device-suite.log`.
108
+
109
+ - Final `npm run test:device`: PASS after the replacement iPhone 17 Pro finished
110
+ booting. All six Android/iOS screenshot, log, and tree smoke wrappers passed,
111
+ without the earlier iOS log-store diagnostics. Log:
112
+ `/tmp/mobile-debug-conditional-device-suite-final.log`.
113
+ - Final documentation relative-link checks and `git diff --check`: PASS.
114
+ - Final lint after the device helper's selector/start-state correction: PASS.
115
+
116
+ ### Focused Android emulator evidence
117
+
118
+ Command (against the built server over MCP stdio, no mocked tool handlers):
119
+
120
+ ```sh
121
+ node --import tsx test/device/manual/interact/conditional_actions.manual.ts \
122
+ --platform android --id emulator-5554 \
123
+ --target-text 'Network & internet' --destination-text Internet \
124
+ --output /tmp/mobile-debug-conditional-android.json
125
+ ```
126
+
127
+ Device: Android 16 sdk_gphone64_arm64, `emulator-5554`.
128
+ Build: local `dist/server.js`, handshake `mobile-debug-mcp` 0.32.4, with
129
+ `expectedState` confirmed in its advertised tool schema. This is the working-tree
130
+ build, not proof of a published release.
131
+ Starting state: system Settings root, with Network & internet visible.
132
+
133
+ | Journey | Actions / expected outcome | Observed outcome | Result |
134
+ | --- | --- | --- | --- |
135
+ | Matching snapshot | Observe Settings; conditional coordinate tap Network & internet | `matched`, delivered once; fresh tree contains Internet | PASS |
136
+ | Stale snapshot, compact | On network page, attempt safe back-navigation tap with root expectation | `PRECONDITION_FAILED`, not delivered; compact tree contains Internet; fresh tree confirms network page remains | PASS |
137
+ | Stale snapshot, debug | Repeat guarded back navigation with root expectation | Same non-delivery and unchanged destination, full debug evidence | PASS |
138
+ | Journey stop, compact | Successful wait for Internet, conditional back tap with root expectation, later tap | Earlier wait passed, guarded step failed with fresh tree, later step `not_run`; destination remained | PASS |
139
+ | Journey stop, debug | Same sequence in debug mode | Same stop and preserved evidence | PASS |
140
+
141
+ Full structured evidence: `/tmp/mobile-debug-conditional-android.json`.
142
+ The five scenarios were rerun successfully against the final built server after
143
+ the compact-error and metadata-validity fixes and the helper selector correction.
144
+ The helper is reproducible and requires a deliberately prepared safe system
145
+ Settings screen. No setting or subscription was changed. The original Modul8
146
+ dev app was brought back to the foreground after checks.
147
+
148
+ ### Focused iOS simulator evidence
149
+
150
+ The original iPhone Air simulator stalled during boot and was closed/rebooted by
151
+ the user. The replacement iPhone 17 Pro became responsive; read-only screenshot
152
+ and UI-tree checks confirmed Home and then Settings before running input checks.
153
+ No boot/restart/reset commands were issued by the agent.
154
+
155
+ Command:
156
+
157
+ ```sh
158
+ node --import tsx test/device/manual/interact/conditional_actions.manual.ts \
159
+ --platform ios --id F9243BAF-62A8-4990-8A99-17F527F56FDA \
160
+ --target-text General --destination-text About \
161
+ --output /tmp/mobile-debug-conditional-ios.json
162
+ ```
163
+
164
+ Device: iOS 26.4 iPhone 17 Pro simulator,
165
+ `F9243BAF-62A8-4990-8A99-17F527F56FDA`.
166
+ Build: the same local `dist/server.js` 0.32.4 build and MCP stdio route as Android,
167
+ with condition support confirmed in tool discovery.
168
+ Starting state: Settings root with General visible, About absent.
169
+
170
+ | Journey | Actions / expected outcome | Observed outcome | Result |
171
+ | --- | --- | --- | --- |
172
+ | Matching snapshot | Observe Settings; conditional coordinate tap General | `matched`, delivered; fresh tree contains About | PASS |
173
+ | Stale snapshot, compact | Attempt safe back-navigation tap with Settings-root expectation | `PRECONDITION_FAILED`, not delivered; compact fresh tree contains About; new observation confirms General stays visible | PASS |
174
+ | Stale snapshot, debug | Same guarded back navigation | Same non-delivery and preserved full condition tree | PASS |
175
+ | Journey stop, compact | Successful wait for About, stale conditional tap on `resource_id: BackButton`, later back tap | Wait passed; condition failed; later step `not_run`; General remained visible | PASS |
176
+ | Journey stop, debug | Same sequence in debug mode | Same stop and fresh condition evidence | PASS |
177
+
178
+ Structured evidence: `/tmp/mobile-debug-conditional-ios.json`.
179
+ An initial helper attempt selected the application container sharing the Settings
180
+ label and correctly stopped with `ELEMENT_NOT_INTERACTABLE` / `not_checked`;
181
+ this was not counted as a conditional-journey pass. The helper now prefers the
182
+ unique native resource ID and requires the destination to be absent from its
183
+ starting screen. A subsequent incorrect-start attempt was also discarded.
184
+ The final five-pass run above starts at the confirmed root and uses BackButton.
185
+
186
+ Cleanup: only navigation was exercised. No setting, permission, account, or app
187
+ data was changed. Home is restored after the smoke-suite rerun.
188
+
189
+ ### Focused quality review
190
+
191
+ Reviewed the shared condition schema and advertised definitions, action wrapper
192
+ and journey dispatch, Android/iOS native input boundaries, observation metadata
193
+ and history, budget ancestors, compact serialization, error/recovery mapping,
194
+ and associated tests/callers. The guard runs after adapter preparation, and
195
+ blocked conditions replace generic adapter failures without inventing an execute
196
+ trace or allowing recovery. Condition observation budgets pause only the current
197
+ action; enclosing deadlines still apply. Tests check no late input after timeout
198
+ and native observation child closure. No new framework or dependency was added.
199
+
200
+ Review findings fixed:
201
+
202
+ - Journey light-verification response overlay must preserve `not_requested` when
203
+ its conditional tap did not dispatch.
204
+ - Compact tree responses must retain observation errors instead of looking like
205
+ successful deltas carrying an old revision/session. Added a regression test.
206
+ - Repeated successful observations refresh retained revision timestamps, so a
207
+ recently observed stable screen does not yield an immediately unusable baseline.
208
+
209
+ Limits: live simulator checks cover conditional taps and journey stops; conditional
210
+ swipes and Android typing/back use production-adapter tests with fake native
211
+ tools. Physical devices were not used for mutation checks. UI normalization, the observation-to-OS-input race, and concurrent external
212
+ input remain explicit spec limits. No mutation testing was performed. No tests
213
+ or validation rules were weakened. Existing uncommitted process/skill relocation
214
+ work was preserved. No package was published and nothing was committed/pushed.