mobile-debug-mcp 0.34.0 → 0.35.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 (432) hide show
  1. package/.agents/skills/mcp-builder/SKILL.md +44 -64
  2. package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
  3. package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
  4. package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
  5. package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
  6. package/.agents/skills/test-authoring/SKILL.md +7 -6
  7. package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
  8. package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
  9. package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
  10. package/.github/copilot-instructions.md +14 -36
  11. package/.github/workflows/ci.yml +4 -4
  12. package/AGENTS.md +12 -3
  13. package/CONTRIBUTING.md +79 -0
  14. package/README.md +156 -129
  15. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  16. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  17. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  18. package/agent-plugin/.mcp.json +1 -1
  19. package/agent-plugin/README.md +16 -6
  20. package/agent-plugin/mcp.json +1 -1
  21. package/agent-plugin/plugin.json +1 -1
  22. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +27 -2
  23. package/dist/contracts/execution-controls.js +24 -0
  24. package/dist/contracts/selector.js +8 -0
  25. package/dist/contracts/target.js +15 -0
  26. package/dist/contracts/ui.js +1 -0
  27. package/dist/debugger/android.js +2 -1
  28. package/dist/debugger/diagnostics.js +54 -0
  29. package/dist/debugger/protocol.js +33 -0
  30. package/dist/debugger/sessions.js +104 -62
  31. package/dist/debugger/worker.js +53 -42
  32. package/dist/interact/adjustment/adjustment-context.js +41 -0
  33. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  34. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  35. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  36. package/dist/interact/adjustment/adjustment-result.js +19 -0
  37. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  38. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  39. package/dist/interact/adjustment/adjustment.js +52 -0
  40. package/dist/interact/adjustment/control-geometry.js +87 -0
  41. package/dist/interact/adjustment/control-values.js +136 -0
  42. package/dist/interact/adjustment/slider-labels.js +84 -0
  43. package/dist/interact/assertions/screen-assertion.js +37 -0
  44. package/dist/interact/assertions/state-assertion-context.js +35 -0
  45. package/dist/interact/assertions/state-assertion.js +76 -0
  46. package/dist/interact/assertions/state-comparison.js +141 -0
  47. package/dist/interact/assertions/visibility-assertion.js +34 -0
  48. package/dist/interact/conditional-action.js +12 -7
  49. package/dist/interact/index.js +65 -3090
  50. package/dist/interact/input-service.js +1 -0
  51. package/dist/interact/interaction-trace.js +21 -0
  52. package/dist/interact/model.js +1 -0
  53. package/dist/interact/observation.js +1 -0
  54. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  55. package/dist/interact/targeting/candidate-result.js +23 -0
  56. package/dist/interact/targeting/candidate-scoring.js +50 -0
  57. package/dist/interact/targeting/element-helpers.js +216 -0
  58. package/dist/interact/targeting/find-element.js +65 -0
  59. package/dist/interact/targeting/nearby-control.js +56 -0
  60. package/dist/interact/targeting/resolved-elements.js +21 -0
  61. package/dist/interact/targeting/tap-target.js +17 -0
  62. package/dist/interact/targeting/target-scope.js +104 -0
  63. package/dist/interact/waits/legacy-log-wait.js +38 -0
  64. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  65. package/dist/interact/waits/legacy-wait-state.js +34 -0
  66. package/dist/interact/waits/legacy-wait.js +48 -0
  67. package/dist/interact/waits/screen-wait.js +49 -0
  68. package/dist/interact/waits/ui-change-helpers.js +157 -0
  69. package/dist/interact/waits/ui-change-state.js +18 -0
  70. package/dist/interact/waits/ui-change-wait.js +77 -0
  71. package/dist/interact/waits/ui-wait-condition.js +56 -0
  72. package/dist/interact/waits/ui-wait-state.js +38 -0
  73. package/dist/interact/waits/ui-wait.js +106 -0
  74. package/dist/manage/android.js +55 -51
  75. package/dist/manage/index.js +38 -75
  76. package/dist/manage/ios/build-types.js +1 -0
  77. package/dist/manage/ios/build.js +210 -0
  78. package/dist/manage/ios/project-discovery.js +29 -0
  79. package/dist/manage/ios/scheme.js +18 -0
  80. package/dist/manage/ios.js +42 -256
  81. package/dist/observe/android/context.js +1 -0
  82. package/dist/observe/android/current-screen.js +55 -0
  83. package/dist/observe/android/fingerprint.js +13 -0
  84. package/dist/observe/android/log-stream.js +132 -0
  85. package/dist/observe/android/logs.js +107 -0
  86. package/dist/observe/android/screenshot.js +70 -0
  87. package/dist/observe/android/ui-tree.js +107 -0
  88. package/dist/observe/android.js +16 -520
  89. package/dist/observe/evidence.js +114 -75
  90. package/dist/observe/index.js +16 -263
  91. package/dist/observe/ios/context.js +16 -0
  92. package/dist/observe/ios/fingerprint.js +12 -0
  93. package/dist/observe/ios/log-stream.js +105 -0
  94. package/dist/observe/ios/logs.js +168 -0
  95. package/dist/observe/ios/screenshot.js +22 -0
  96. package/dist/observe/ios/ui-elements.js +241 -0
  97. package/dist/observe/ios/ui-tree.js +137 -0
  98. package/dist/observe/ios.js +17 -715
  99. package/dist/observe/logs/model.js +1 -0
  100. package/dist/{network → observe/network}/index.js +2 -2
  101. package/dist/observe/readiness-service.js +4 -2
  102. package/dist/observe/readiness.js +29 -10
  103. package/dist/observe/screenshot-encoding.js +41 -0
  104. package/dist/observe/snapshot-collection.js +79 -0
  105. package/dist/observe/snapshot-logs.js +30 -0
  106. package/dist/observe/snapshot-metadata.js +43 -31
  107. package/dist/observe/snapshot-semantic.js +115 -0
  108. package/dist/observe/system-trace.js +22 -13
  109. package/dist/runtime/action-results.js +270 -0
  110. package/dist/runtime/clock.js +4 -0
  111. package/dist/server/common.js +1 -256
  112. package/dist/server/definitions/assert.js +162 -0
  113. package/dist/server/definitions/diagnostic.js +118 -0
  114. package/dist/server/definitions/input.js +274 -0
  115. package/dist/server/definitions/manage.js +194 -0
  116. package/dist/server/definitions/observe.js +149 -0
  117. package/dist/server/definitions/target.js +138 -0
  118. package/dist/server/definitions/wait.js +105 -0
  119. package/dist/server/handlers/action-execution.js +163 -0
  120. package/dist/server/handlers/correlation.js +23 -0
  121. package/dist/server/{debugger-handlers.js → handlers/debugger-handlers.js} +22 -12
  122. package/dist/server/handlers/debugger-response.js +9 -0
  123. package/dist/server/handlers/diagnostic.js +69 -0
  124. package/dist/server/handlers/handler-types.js +1 -0
  125. package/dist/server/handlers/input.js +154 -0
  126. package/dist/server/handlers/interact.js +113 -0
  127. package/dist/server/handlers/journey-assertions.js +35 -0
  128. package/dist/server/handlers/journey.js +147 -0
  129. package/dist/server/handlers/manage.js +119 -0
  130. package/dist/server/handlers/observe.js +95 -0
  131. package/dist/server/handlers/payload.js +4 -0
  132. package/dist/server/handlers/tool-handlers.js +160 -0
  133. package/dist/server/schemas/condition-schema.js +2 -0
  134. package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +11 -6
  135. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
  136. package/dist/server/tool-definitions.js +58 -1135
  137. package/dist/server-core.js +2 -2
  138. package/dist/system/android-status.js +101 -0
  139. package/dist/system/android.js +37 -27
  140. package/dist/system/index.js +13 -439
  141. package/dist/system/ios-status.js +132 -0
  142. package/dist/system/ios.js +11 -8
  143. package/dist/system/model.js +1 -0
  144. package/dist/system/probes.js +105 -0
  145. package/dist/system/status-summary.js +43 -0
  146. package/dist/system/unsupported-status.js +52 -0
  147. package/dist/utils/android/commands.js +89 -0
  148. package/dist/utils/android/gradle.js +139 -0
  149. package/dist/utils/android/logs.js +103 -0
  150. package/dist/utils/android/ui-tree.js +233 -0
  151. package/dist/utils/android/utils.js +6 -564
  152. package/dist/utils/cli/idb/idb-helper.js +4 -6
  153. package/dist/utils/cli/idb/install-idb.js +13 -9
  154. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  155. package/dist/utils/ios/utils.js +45 -46
  156. package/dist/utils/java.js +65 -49
  157. package/dist/utils/resolve-device.js +4 -2
  158. package/dist/utils/ui/index.js +26 -26
  159. package/docs/CHANGELOG.md +44 -0
  160. package/docs/README.md +38 -0
  161. package/docs/benchmarks/README.md +18 -0
  162. package/docs/benchmarks/interaction-latency/README.md +14 -0
  163. package/docs/benchmarks/interaction-latency/final.md +4 -1
  164. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  165. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  166. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  167. package/docs/code-structure.md +118 -0
  168. package/docs/specs/001-state-verification/spec.md +16 -14
  169. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  170. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  171. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  172. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  173. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  174. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  175. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  176. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  177. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  178. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  179. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  180. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  181. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  182. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  183. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  184. package/docs/specs/016-environment-tools/spec.md +20 -20
  185. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  186. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  187. package/docs/specs/020-conditional-actions/intent.md +5 -3
  188. package/docs/specs/020-conditional-actions/plan.md +8 -9
  189. package/docs/specs/020-conditional-actions/spec.md +4 -2
  190. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  191. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  192. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  193. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  194. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  195. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  196. package/docs/specs/023-android-system-traces/intent.md +3 -2
  197. package/docs/specs/023-android-system-traces/plan.md +5 -7
  198. package/docs/specs/023-android-system-traces/spec.md +3 -1
  199. package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
  200. package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
  201. package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
  202. package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
  203. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  204. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  205. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  206. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  207. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  208. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  209. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  210. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  211. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  212. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  213. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  214. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  215. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  216. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  217. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  218. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  219. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  220. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  221. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  222. package/docs/tools/TOOLS.md +31 -18
  223. package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
  224. package/docs/tools/debugger.md +76 -0
  225. package/docs/tools/interact.md +42 -20
  226. package/docs/tools/manage.md +25 -44
  227. package/docs/tools/observe.md +55 -23
  228. package/docs/tools/system-traces.md +9 -1
  229. package/docs/tools/system.md +12 -5
  230. package/package.json +11 -8
  231. package/scripts/architecture-graph.mjs +69 -0
  232. package/scripts/check-architecture.mjs +47 -0
  233. package/scripts/code-quality-baseline.json +1 -0
  234. package/scripts/code-quality.mjs +161 -0
  235. package/src/contracts/execution-controls.ts +26 -0
  236. package/src/contracts/selector.ts +17 -0
  237. package/src/contracts/target.ts +19 -0
  238. package/src/contracts/ui.ts +94 -0
  239. package/src/debugger/android.ts +2 -1
  240. package/src/debugger/diagnostics.ts +43 -0
  241. package/src/debugger/guard.ts +6 -1
  242. package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
  243. package/src/debugger/protocol.ts +37 -0
  244. package/src/debugger/sessions.ts +314 -155
  245. package/src/debugger/worker.ts +51 -38
  246. package/src/interact/adjustment/adjustment-context.ts +83 -0
  247. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  248. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  249. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  250. package/src/interact/adjustment/adjustment-result.ts +34 -0
  251. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  252. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  253. package/src/interact/adjustment/adjustment.ts +49 -0
  254. package/src/interact/adjustment/control-geometry.ts +120 -0
  255. package/src/interact/adjustment/control-values.ts +141 -0
  256. package/src/interact/adjustment/slider-labels.ts +84 -0
  257. package/src/interact/assertions/screen-assertion.ts +39 -0
  258. package/src/interact/assertions/state-assertion-context.ts +48 -0
  259. package/src/interact/assertions/state-assertion.ts +64 -0
  260. package/src/interact/assertions/state-comparison.ts +168 -0
  261. package/src/interact/assertions/visibility-assertion.ts +41 -0
  262. package/src/interact/conditional-action.ts +18 -12
  263. package/src/interact/index.ts +76 -3492
  264. package/src/interact/input-service.ts +11 -0
  265. package/src/interact/interaction-trace.ts +36 -0
  266. package/src/interact/model.ts +109 -0
  267. package/src/interact/observation.ts +7 -0
  268. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  269. package/src/interact/targeting/candidate-result.ts +24 -0
  270. package/src/interact/targeting/candidate-scoring.ts +42 -0
  271. package/src/interact/targeting/element-helpers.ts +243 -0
  272. package/src/interact/targeting/find-element.ts +47 -0
  273. package/src/interact/targeting/nearby-control.ts +46 -0
  274. package/src/interact/targeting/resolved-elements.ts +20 -0
  275. package/src/interact/targeting/tap-target.ts +22 -0
  276. package/src/interact/targeting/target-scope.ts +121 -0
  277. package/src/interact/waits/legacy-log-wait.ts +30 -0
  278. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  279. package/src/interact/waits/legacy-wait-state.ts +48 -0
  280. package/src/interact/waits/legacy-wait.ts +34 -0
  281. package/src/interact/waits/screen-wait.ts +37 -0
  282. package/src/interact/waits/ui-change-helpers.ts +184 -0
  283. package/src/interact/waits/ui-change-state.ts +26 -0
  284. package/src/interact/waits/ui-change-wait.ts +59 -0
  285. package/src/interact/waits/ui-wait-condition.ts +52 -0
  286. package/src/interact/waits/ui-wait-state.ts +55 -0
  287. package/src/interact/waits/ui-wait.ts +84 -0
  288. package/src/manage/android.ts +56 -50
  289. package/src/manage/index.ts +63 -93
  290. package/src/manage/ios/build-types.ts +14 -0
  291. package/src/manage/ios/build.ts +230 -0
  292. package/src/manage/ios/project-discovery.ts +26 -0
  293. package/src/manage/ios/scheme.ts +16 -0
  294. package/src/manage/ios.ts +65 -298
  295. package/src/observe/android/context.ts +1 -0
  296. package/src/observe/android/current-screen.ts +63 -0
  297. package/src/observe/android/fingerprint.ts +13 -0
  298. package/src/observe/android/log-stream.ts +127 -0
  299. package/src/observe/android/logs.ts +110 -0
  300. package/src/observe/android/screenshot.ts +79 -0
  301. package/src/observe/android/ui-tree.ts +113 -0
  302. package/src/observe/android.ts +17 -531
  303. package/src/observe/evidence.ts +122 -70
  304. package/src/observe/index.ts +24 -286
  305. package/src/observe/ios/context.ts +22 -0
  306. package/src/observe/ios/fingerprint.ts +12 -0
  307. package/src/observe/ios/log-stream.ts +103 -0
  308. package/src/observe/ios/logs.ts +179 -0
  309. package/src/observe/ios/screenshot.ts +26 -0
  310. package/src/observe/ios/ui-elements.ts +266 -0
  311. package/src/observe/ios/ui-tree.ts +143 -0
  312. package/src/observe/ios.ts +18 -749
  313. package/src/observe/logs/model.ts +11 -0
  314. package/src/{network → observe/network}/index.ts +2 -2
  315. package/src/observe/readiness-service.ts +5 -3
  316. package/src/observe/readiness.ts +39 -18
  317. package/src/observe/screenshot-encoding.ts +32 -0
  318. package/src/observe/snapshot-collection.ts +69 -0
  319. package/src/observe/snapshot-logs.ts +26 -0
  320. package/src/observe/snapshot-metadata.ts +51 -31
  321. package/src/observe/snapshot-semantic.ts +153 -0
  322. package/src/observe/system-trace.ts +24 -15
  323. package/src/runtime/action-results.ts +376 -0
  324. package/src/runtime/clock.ts +9 -0
  325. package/src/server/common.ts +9 -359
  326. package/src/server/definitions/assert.ts +164 -0
  327. package/src/server/definitions/diagnostic.ts +120 -0
  328. package/src/server/definitions/input.ts +276 -0
  329. package/src/server/definitions/manage.ts +196 -0
  330. package/src/server/definitions/observe.ts +151 -0
  331. package/src/server/definitions/target.ts +140 -0
  332. package/src/server/definitions/wait.ts +107 -0
  333. package/src/server/handlers/action-execution.ts +164 -0
  334. package/src/server/handlers/correlation.ts +20 -0
  335. package/src/server/handlers/debugger-handlers.ts +39 -0
  336. package/src/server/handlers/debugger-response.ts +7 -0
  337. package/src/server/handlers/diagnostic.ts +79 -0
  338. package/src/server/handlers/handler-types.ts +7 -0
  339. package/src/server/handlers/input.ts +173 -0
  340. package/src/server/handlers/interact.ts +135 -0
  341. package/src/server/handlers/journey-assertions.ts +39 -0
  342. package/src/server/handlers/journey.ts +132 -0
  343. package/src/server/handlers/manage.ts +141 -0
  344. package/src/server/handlers/observe.ts +116 -0
  345. package/src/server/handlers/payload.ts +36 -0
  346. package/src/server/handlers/tool-handlers.ts +159 -0
  347. package/src/server/schemas/condition-schema.ts +2 -0
  348. package/src/server/{debugger-schema.ts → schemas/debugger-schema.ts} +13 -7
  349. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
  350. package/src/server/{system-trace-schema.ts → schemas/system-trace-schema.ts} +3 -3
  351. package/src/server/tool-definitions.ts +67 -1143
  352. package/src/server-core.ts +2 -2
  353. package/src/server.ts +1 -1
  354. package/src/system/android-status.ts +124 -0
  355. package/src/system/android.ts +33 -21
  356. package/src/system/index.ts +21 -525
  357. package/src/system/ios-status.ts +147 -0
  358. package/src/system/ios.ts +14 -10
  359. package/src/system/model.ts +41 -0
  360. package/src/system/probes.ts +114 -0
  361. package/src/system/status-summary.ts +47 -0
  362. package/src/system/unsupported-status.ts +55 -0
  363. package/src/types.ts +16 -106
  364. package/src/utils/android/commands.ts +81 -0
  365. package/src/utils/android/gradle.ts +132 -0
  366. package/src/utils/android/logs.ts +103 -0
  367. package/src/utils/android/ui-tree.ts +239 -0
  368. package/src/utils/android/utils.ts +16 -556
  369. package/src/utils/cli/idb/check-idb.ts +4 -4
  370. package/src/utils/cli/idb/idb-helper.ts +6 -8
  371. package/src/utils/cli/idb/install-idb.ts +15 -9
  372. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  373. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  374. package/src/utils/ios/utils.ts +57 -61
  375. package/src/utils/java.ts +62 -43
  376. package/src/utils/resolve-device.ts +9 -6
  377. package/src/utils/ui/index.ts +28 -28
  378. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  379. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  380. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  381. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  382. package/test/device/automated/debugger/runtime.android.test.ts +19 -3
  383. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  384. package/test/device/automated/observe/system-trace.smoke.ts +1 -1
  385. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  386. package/test/fixtures/contracts/tool-definitions.json +3536 -0
  387. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  388. package/test/fixtures/coroutine-app/README.md +48 -0
  389. package/test/fixtures/coroutine-app/build.ts +35 -0
  390. package/test/fixtures/debugger-app/MainActivity.java +21 -0
  391. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  392. package/test/fixtures/debugger-app/README.md +52 -0
  393. package/test/fixtures/debugger-app/build.ts +25 -15
  394. package/test/fixtures/latency-journey-app/README.md +2 -0
  395. package/test/fixtures/readiness-app/README.md +2 -0
  396. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  397. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  398. package/test/unit/debugger/diagnostics.test.ts +80 -0
  399. package/test/unit/debugger/guards.test.ts +4 -0
  400. package/test/unit/debugger/preparation.test.ts +32 -0
  401. package/test/unit/debugger/protocol.test.ts +15 -0
  402. package/test/unit/debugger/sessions.test.ts +1 -1
  403. package/test/unit/interact/adjustment_service.test.ts +42 -0
  404. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  405. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  406. package/test/unit/interact/element_helpers.test.ts +19 -0
  407. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  408. package/test/unit/interact/state_comparison.test.ts +20 -0
  409. package/test/unit/interact/target_scope.test.ts +35 -0
  410. package/test/unit/network/get_network_activity.test.ts +1 -1
  411. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  412. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  413. package/test/unit/quality/architecture.test.ts +60 -0
  414. package/test/unit/quality/code-quality.test.ts +58 -0
  415. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  416. package/test/unit/server/conditional_actions.test.ts +1 -1
  417. package/test/unit/server/conditional_schema.test.ts +2 -2
  418. package/test/unit/server/debugger-response.test.ts +63 -0
  419. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  420. package/dist/server/tool-handlers.js +0 -1000
  421. package/docs/ROADMAP.md +0 -646
  422. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  423. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  424. package/docs/specs/baseline-spec-v0.md +0 -312
  425. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  426. package/src/server/debugger-handlers.ts +0 -29
  427. package/src/server/tool-handlers.ts +0 -1008
  428. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  429. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  430. /package/dist/server/{system-trace-schema.js → schemas/system-trace-schema.js} +0 -0
  431. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  432. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -2,8 +2,8 @@ import { remainingBudget } from '../operation-budget.js'
2
2
  import crypto from 'crypto'
3
3
  import { GetUITreeResponse, GetCurrentScreenResponse, UIElement, SwipeResponse } from '../../types.js'
4
4
 
5
- const ANDROID_STRUCTURAL_TYPES = ['Window','Application','View','ViewGroup','LinearLayout','FrameLayout','RelativeLayout','ScrollView','RecyclerView','TextView','ImageView']
6
- const IOS_STRUCTURAL_TYPES = ['Window','Application','View','ViewController','UITableView','UICollectionView','UILabel','UIImageView','UIView','UIWindow','UIStackView','UITextView','UITableViewCell']
5
+ const ANDROID_STRUCTURAL_TYPES = ['Window', 'Application', 'View', 'ViewGroup', 'LinearLayout', 'FrameLayout', 'RelativeLayout', 'ScrollView', 'RecyclerView', 'TextView', 'ImageView']
6
+ const IOS_STRUCTURAL_TYPES = ['Window', 'Application', 'View', 'ViewController', 'UITableView', 'UICollectionView', 'UILabel', 'UIImageView', 'UIView', 'UIWindow', 'UIStackView', 'UITextView', 'UITableViewCell']
7
7
 
8
8
  function isDynamicText(t?: string): boolean {
9
9
  if (!t) return false
@@ -23,15 +23,15 @@ function normalizeElement(e: UIElement) {
23
23
  resourceId: (e.resourceId || '').toString(),
24
24
  text: typeof e.text === 'string' ? (isDynamicText(e.text) ? '' : e.text.trim().toLowerCase()) : '',
25
25
  contentDesc: (e.contentDescription || '').toString(),
26
- bounds: Array.isArray(e.bounds) ? e.bounds.slice(0,4).map((n:any)=>Number(n)||0) : [0,0,0,0]
26
+ bounds: Array.isArray(e.bounds) ? e.bounds.slice(0, 4).map((n: number) => Number(n) || 0) : [0, 0, 0, 0]
27
27
  }
28
28
  }
29
29
 
30
30
  export function computeScreenFingerprint(tree: GetUITreeResponse, current: GetCurrentScreenResponse | null, platform: 'android' | 'ios', limit: number = 50): { fingerprint: string | null; activity?: string; error?: string } {
31
31
  try {
32
- if (!tree || (tree as any).error) return { fingerprint: null, error: (tree as any).error }
32
+ if (!tree || tree.error) return { fingerprint: null, error: tree.error }
33
33
 
34
- const activity = current && (current.activity || (current as any).shortActivity) ? (current.activity || (current as any).shortActivity) : ''
34
+ const activity = current && (current.activity || current.shortActivity) ? (current.activity || current.shortActivity) : ''
35
35
 
36
36
  const candidates: UIElement[] = (tree.elements || []).filter(e => {
37
37
  if (!e) return false
@@ -48,7 +48,7 @@ export function computeScreenFingerprint(tree: GetUITreeResponse, current: GetCu
48
48
 
49
49
  const filteredNormalized = normalized.filter(e => (e.text && e.text.length > 0) || (e.resourceId && e.resourceId.length > 0) || (e.contentDesc && e.contentDesc.length > 0))
50
50
 
51
- filteredNormalized.sort((a,b) => {
51
+ filteredNormalized.sort((a, b) => {
52
52
  const ay = (a.bounds && a.bounds[1]) || 0
53
53
  const by = (b.bounds && b.bounds[1]) || 0
54
54
  if (ay !== by) return ay - by
@@ -61,7 +61,7 @@ export function computeScreenFingerprint(tree: GetUITreeResponse, current: GetCu
61
61
 
62
62
  const payload = {
63
63
  activity: platform === 'android' ? (activity || '') : '',
64
- resolution: (tree as any).resolution || { width: 0, height: 0 },
64
+ resolution: tree.resolution || { width: 0, height: 0 },
65
65
  elements: limited.map(e => ({ type: e.type, resourceId: e.resourceId, text: e.text, contentDesc: e.contentDesc }))
66
66
  }
67
67
 
@@ -87,29 +87,10 @@ export async function scrollToElementShared(opts: {
87
87
  }): Promise<{ success: boolean; reason?: string; element?: Partial<UIElement>; scrollsPerformed: number }> {
88
88
  const { selector, direction = 'down', maxScrolls = 10, scrollAmount = 0.7, deviceId, fetchTree, swipe, stabilizationDelayMs = 350 } = opts
89
89
 
90
- const matchElement = (el?: UIElement) => {
91
- if (!el) return false
92
- if (selector.text !== undefined && selector.text !== el.text) return false
93
- if (selector.resourceId !== undefined && selector.resourceId !== el.resourceId) return false
94
- if (selector.contentDesc !== undefined && selector.contentDesc !== el.contentDescription) return false
95
- if (selector.className !== undefined && selector.className !== el.type) return false
96
- return true
97
- }
98
-
99
- const isVisible = (el?: UIElement, resolution?: GetUITreeResponse['resolution']) => {
100
- if (!el) return false
101
- if (el.visible === false) return false
102
- if (!el.bounds || !resolution || !resolution.width || !resolution.height) return (el.visible === undefined ? true : !!el.visible)
103
- const [left, top, right, bottom] = el.bounds
104
- const withinY = bottom > 0 && top < resolution.height
105
- const withinX = right > 0 && left < resolution.width
106
- return withinX && withinY
107
- }
108
-
109
90
  const findVisibleMatch = (elements?: UIElement[], resolution?: GetUITreeResponse['resolution']) => {
110
91
  if (!Array.isArray(elements)) return null
111
92
  for (const e of elements) {
112
- if (matchElement(e) && isVisible(e, resolution)) return e
93
+ if (matchElement(selector, e) && isVisible(e, resolution)) return e
113
94
  }
114
95
  return null
115
96
  }
@@ -158,7 +139,7 @@ export async function scrollToElementShared(opts: {
158
139
  try {
159
140
  await swipe(x1, y1, x2, y2, duration, deviceId)
160
141
  } catch (e) {
161
- try { console.warn(`scrollToElement swipe failed: ${e instanceof Error ? e.message : String(e)}`) } catch {}
142
+ try { console.warn(`scrollToElement swipe failed: ${e instanceof Error ? e.message : String(e)}`) } catch { }
162
143
  }
163
144
 
164
145
  scrollsPerformed++
@@ -181,3 +162,22 @@ export async function scrollToElementShared(opts: {
181
162
 
182
163
  return { success: false, reason: 'Element not found after scrolling', scrollsPerformed: scrollsPerformed }
183
164
  }
165
+
166
+ function matchElement(selector: ScrollSelector, el?: UIElement) {
167
+ if (!el) return false
168
+ if (selector.text !== undefined && selector.text !== el.text) return false
169
+ if (selector.resourceId !== undefined && selector.resourceId !== el.resourceId) return false
170
+ if (selector.contentDesc !== undefined && selector.contentDesc !== el.contentDescription) return false
171
+ if (selector.className !== undefined && selector.className !== el.type) return false
172
+ return true
173
+ }
174
+
175
+ function isVisible(el?: UIElement, resolution?: GetUITreeResponse['resolution']) {
176
+ if (!el) return false
177
+ if (el.visible === false) return false
178
+ if (!el.bounds || !resolution || !resolution.width || !resolution.height) return (el.visible === undefined ? true : !!el.visible)
179
+ const [left, top, right, bottom] = el.bounds
180
+ const withinY = bottom > 0 && top < resolution.height
181
+ const withinX = right > 0 && left < resolution.width
182
+ return withinX && withinY
183
+ }
@@ -0,0 +1,27 @@
1
+ /** ADB lists every device even with -s. Compare only this serial, independent of order. */
2
+ export function deviceForwards(table: string, deviceId: string): string[] {
3
+ return table.split('\n').map(row => row.trim().split(/\s+/))
4
+ .filter(columns => columns[0] === deviceId)
5
+ .map(columns => columns.join(' ')).sort()
6
+ }
7
+
8
+ /** Cleanup is part of the assessment; a finally failure must never follow a PASS. */
9
+ export async function reportAfterCleanup<T>(
10
+ work: () => Promise<T>,
11
+ cleanup: () => Promise<void>,
12
+ report: (status: 'PASS' | 'FAIL', evidence?: T, error?: unknown) => void,
13
+ ): Promise<void> {
14
+ let evidence: T | undefined
15
+ let failed = false
16
+ let failure: unknown
17
+ try { evidence = await work() } catch (error) { failed = true; failure = error }
18
+ try { await cleanup() } catch (error) {
19
+ failure = failed ? new AggregateError([failure, error], `${String(failure)}; cleanup: ${String(error)}`) : error
20
+ failed = true
21
+ }
22
+ if (failed) {
23
+ report('FAIL', undefined, failure)
24
+ throw failure
25
+ }
26
+ report('PASS', evidence)
27
+ }
@@ -0,0 +1,140 @@
1
+ import assert from 'node:assert/strict'
2
+ import { execFileSync, spawn } from 'node:child_process'
3
+ import { createHash } from 'node:crypto'
4
+ import { readFile, readdir } from 'node:fs/promises'
5
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
6
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
7
+ import { coroutineAppId } from '../../../fixtures/coroutine-app/build.js'
8
+
9
+ type Location = { line: number; method: string; source: string; class_name?: string }
10
+ export type Stop = { stop_id: string; thread_id: string; stops: { location: Location }[] }
11
+ type Frame = { frame_id: string; location: Location }
12
+ type Reply = {
13
+ success: boolean; debug_session_id: string; supported: boolean; next_cursor: number; overflow: boolean
14
+ events: (Stop & { event: string })[]; error: { code: string }; failure_code: string; dispatch_started: boolean
15
+ result: { breakpoint_id: string; status: string; frames: Frame[]; locals: { name: string; value: unknown }[]; availability: string }
16
+ elements: { text?: string; bounds: number[] }[]
17
+ cleanup: { status: string; remaining_resources: unknown[] }
18
+ }
19
+ export class CoroutineJourney {
20
+ readonly client = new Client({ name: 'coroutine-validation', version: '1.0.0' })
21
+ readonly interruption = new AbortController()
22
+ session = ''
23
+ cursor = 0
24
+ observations: unknown[] = []
25
+ constructor(readonly device: string, readonly source: string) {}
26
+ adb(...args: string[]) {
27
+ return execFileSync(process.env.ADB_PATH ?? 'adb', ['-s', this.device, ...args], { encoding: 'utf8', timeout: 15000 })
28
+ }
29
+ async connect() {
30
+ await this.client.connect(new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' }))
31
+ }
32
+ async call(name: string, args: Record<string, unknown> = {}) {
33
+ console.log('Coroutine MCP', name)
34
+ const reply = await this.client.callTool({ name, arguments: { ...args, ...(this.session ? { debugSessionId: this.session } : {}) } }, undefined, { timeout: 45000, ...(name === 'detach_debugger' ? {} : { signal: this.interruption.signal }) })
35
+ const content = reply.content.find(item => item.type === 'text')
36
+ assert.ok(content?.type === 'text')
37
+ return JSON.parse(content.text) as Reply
38
+ }
39
+ line(marker: string) {
40
+ const index = this.source.split('\n').findIndex(row => row.includes('// ' + marker))
41
+ assert.ok(index >= 0, marker)
42
+ return index + 1
43
+ }
44
+ async attach() {
45
+ this.cursor = 0
46
+ const capabilities = await this.call('get_debugger_capabilities', { platform: 'android', deviceId: this.device, appId: coroutineAppId })
47
+ assert.equal(capabilities.supported, true, JSON.stringify(capabilities))
48
+ const result = await this.call('attach_debugger', { platform: 'android', deviceId: this.device, appId: coroutineAppId, timeoutMs: 30000 })
49
+ this.session = result.debug_session_id ?? ''
50
+ assert.equal(result.success, true, JSON.stringify(result))
51
+ }
52
+ async detach() {
53
+ if (!this.session) return
54
+ const result = await this.call('detach_debugger')
55
+ assert.equal(result.cleanup.status, 'complete', JSON.stringify(result))
56
+ assert.deepEqual(result.cleanup.remaining_resources, [])
57
+ this.session = ''
58
+ }
59
+ async breakpoint(marker: string, suffix = '') {
60
+ const result = await this.call('set_debugger_breakpoint', { className: coroutineAppId + '.MainActivity' + suffix, line: this.line(marker), responseMode: 'compact' })
61
+ assert.ok(['verified', 'pending'].includes(result.result.status), JSON.stringify(result))
62
+ return result.result.breakpoint_id
63
+ }
64
+ async remove(breakpointId: string) {
65
+ assert.equal((await this.call('remove_debugger_breakpoint', { breakpointId })).success, true)
66
+ }
67
+ async stop(marker?: string, attempts = 10): Promise<Stop | undefined> {
68
+ for (let index = 0; index < attempts; index++) {
69
+ const result = await this.call('wait_for_debugger_event', { afterSequence: this.cursor, timeoutMs: 1000, responseMode: 'compact' })
70
+ assert.equal(result.success, true, JSON.stringify(result))
71
+ assert.equal(result.overflow, false)
72
+ this.cursor = result.next_cursor
73
+ const stop = result.events.find(event => event.event === 'stopped')
74
+ if (!stop) continue
75
+ if (marker) {
76
+ assert.equal(stop.stops[0].location.line, this.line(marker), JSON.stringify(stop))
77
+ assert.equal(stop.stops[0].location.source, 'MainActivity.kt')
78
+ }
79
+ this.observations.push({ marker, stop })
80
+ return stop
81
+ }
82
+ if (marker) throw new Error('Expected source stop: ' + marker)
83
+ }
84
+ async inspect(stop: Stop, label: string) {
85
+ const stack = await this.call('get_debugger_stack', { stopId: stop.stop_id, threadId: stop.thread_id, responseMode: 'compact' })
86
+ assert.equal(stack.success, true, JSON.stringify(stack))
87
+ const frame = stack.result.frames[0]
88
+ const locals = await this.call('get_debugger_locals', { stopId: stop.stop_id, frameId: frame.frame_id, responseMode: 'compact' })
89
+ assert.equal(locals.success, true, JSON.stringify(locals))
90
+ this.observations.push({ label, frames: stack.result.frames, locals: locals.result })
91
+ return { frameId: frame.frame_id, locals: locals.result.locals }
92
+ }
93
+ async resume(stop: Stop) {
94
+ assert.equal((await this.call('resume_debugger', { stopId: stop.stop_id })).success, true)
95
+ }
96
+ async step(stop: Stop, kind: string) {
97
+ const result = await this.call('step_debugger', { stopId: stop.stop_id, threadId: stop.thread_id, kind })
98
+ assert.equal(result.success, true, JSON.stringify(result))
99
+ }
100
+ async tree() { return this.call('get_ui_tree', { platform: 'android', deviceId: this.device }) }
101
+ async visible(text: string) {
102
+ for (let index = 0; index < 10; index++) {
103
+ if ((await this.tree()).elements.some(element => element.text === text)) return
104
+ }
105
+ throw new Error('Fresh UI did not show ' + text + ': ' + JSON.stringify(await this.tree()))
106
+ }
107
+ async tap(text: string) {
108
+ const button = (await this.tree()).elements.find(element => element.text?.toLowerCase() === text.toLowerCase())
109
+ assert.ok(button, 'UI button ' + text)
110
+ const [left, top, right, bottom] = button.bounds
111
+ // Input stays asynchronous: its shell process may wait while the VM is paused.
112
+ const child = spawn(process.env.ADB_PATH ?? 'adb', ['-s', this.device, 'shell', 'input', 'tap', String(Math.floor((left + right) / 2)), String(Math.floor((top + bottom) / 2))], { stdio: 'ignore' })
113
+ const timer = setTimeout(() => child.kill(), 15000)
114
+ const done = new Promise<void>((resolve, reject) => {
115
+ child.once('error', reject)
116
+ child.once('exit', code => { clearTimeout(timer); code === 0 ? resolve() : reject(new Error('Fixture tap failed: ' + code)) })
117
+ })
118
+ void done.catch(() => {})
119
+ return { done }
120
+ }
121
+ async cancelledWait() {
122
+ const controller = new AbortController()
123
+ const waiting = this.client.callTool({ name: 'wait_for_debugger_event', arguments: { debugSessionId: this.session, afterSequence: this.cursor, timeoutMs: 1000 } }, undefined, { signal: controller.signal, timeout: 5000 })
124
+ const timer = setTimeout(() => controller.abort(), 100)
125
+ try { await assert.rejects(waiting) } finally { clearTimeout(timer) }
126
+ assert.equal((await this.call('get_debugger_state')).success, true)
127
+ }
128
+ }
129
+ export async function fingerprint(directory: string): Promise<string> {
130
+ const hash = createHash('sha256')
131
+ async function visit(folder: string): Promise<void> {
132
+ for (const entry of (await readdir(folder, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
133
+ const file = folder + '/' + entry.name
134
+ if (entry.isDirectory()) await visit(file)
135
+ else hash.update(file).update(await readFile(file))
136
+ }
137
+ }
138
+ await visit(directory)
139
+ return hash.digest('hex')
140
+ }
@@ -0,0 +1,169 @@
1
+ import assert from 'node:assert/strict'
2
+ import { execFileSync, spawnSync } from 'node:child_process'
3
+ import { createHash } from 'node:crypto'
4
+ import { readFile, readdir, rm } from 'node:fs/promises'
5
+ import { buildCoroutineFixture, coroutineAppId } from '../../../fixtures/coroutine-app/build.js'
6
+ import { CoroutineJourney, fingerprint, type Stop } from './coroutine-support.js'
7
+ import { deviceForwards, reportAfterCleanup } from './coroutine-lifecycle.js'
8
+
9
+ const device = process.env.MCP_COROUTINE_DEVICE
10
+ const source = await readFile(new URL('../../../fixtures/coroutine-app/MainActivity.kt', import.meta.url), 'utf8')
11
+ const hash = (value: string | Uint8Array) => createHash('sha256').update(value).digest('hex')
12
+
13
+ async function requiredStop(journey: CoroutineJourney, marker?: string) {
14
+ const stop = await journey.stop(marker)
15
+ assert.ok(stop, 'Expected debugger stop')
16
+ return stop
17
+ }
18
+ async function guards(journey: CoroutineJourney, stop: Stop, frameId: string) {
19
+ const blocked = await journey.call('tap', { platform: 'android', deviceId: journey.device, x: 5, y: 5 })
20
+ assert.equal(blocked.failure_code, 'DEBUGGER_PAUSED')
21
+ assert.equal(blocked.dispatch_started, false)
22
+ await journey.resume(stop)
23
+ const stale = await journey.call('get_debugger_locals', { stopId: stop.stop_id, frameId })
24
+ assert.equal(stale.error.code, 'DEBUGGER_STALE_REFERENCE')
25
+ }
26
+ async function completion(journey: CoroutineJourney) {
27
+ await journey.attach()
28
+ const beforeBp = await journey.breakpoint('BREAK_BEFORE')
29
+ const afterBp = await journey.breakpoint('BREAK_AFTER')
30
+ const dispatchBp = await journey.breakpoint('BREAK_DISPATCH', '$completeWork$3')
31
+ const helperBp = await journey.breakpoint('BREAK_HELPER')
32
+ const resultBp = await journey.breakpoint('BREAK_RESULT', '$completeWork$3')
33
+ const tap = await journey.tap('Complete run')
34
+ const before = await requiredStop(journey, 'BREAK_BEFORE')
35
+ const values = await journey.inspect(before, 'before suspension')
36
+ assert.equal(values.locals.find(value => value.name === 'known')?.value, '42')
37
+ assert.equal(values.locals.find(value => value.name === 'firstThread')?.value, 'fixture-first')
38
+ await journey.remove(beforeBp)
39
+ await guards(journey, before, values.frameId)
40
+ await tap.done
41
+ await journey.visible('Coroutine waiting')
42
+ await journey.cancelledWait()
43
+ const release = await journey.tap('Release gate')
44
+ const after = await requiredStop(journey, 'BREAK_AFTER')
45
+ const afterValues = await journey.inspect(after, 'after suspension')
46
+ // A spill may not be represented as a shallow local; retain that observation.
47
+ const known = afterValues.locals.find(value => value.name === 'known')
48
+ if (known) assert.equal(known.value, '42')
49
+ await journey.remove(afterBp)
50
+ await journey.resume(after)
51
+ const dispatched = await requiredStop(journey, 'BREAK_DISPATCH')
52
+ const dispatcherValues = await journey.inspect(dispatched, 'second dispatcher')
53
+ assert.equal(dispatcherValues.locals.find(value => value.name === 'dispatcherThread')?.value, 'fixture-second')
54
+ await journey.remove(dispatchBp)
55
+ await journey.resume(dispatched)
56
+ const helper = await requiredStop(journey, 'BREAK_HELPER')
57
+ await journey.inspect(helper, 'nested child helper')
58
+ await journey.remove(helperBp)
59
+ await journey.step(helper, 'into')
60
+ const into = await requiredStop(journey, 'BREAK_INCREMENT')
61
+ await journey.step(into, 'over')
62
+ const over = await requiredStop(journey, 'BREAK_INCREMENT_RETURN')
63
+ await journey.step(over, 'out')
64
+ const out = await requiredStop(journey)
65
+ assert.equal(out.stops[0].location.method, 'helper')
66
+ await journey.resume(out)
67
+ const result = await requiredStop(journey, 'BREAK_RESULT')
68
+ const resultValues = await journey.inspect(result, 'nested result')
69
+ assert.equal(resultValues.locals.find(value => value.name === 'answer')?.value, '44')
70
+ await journey.remove(resultBp)
71
+ await journey.resume(result)
72
+ await release.done
73
+ await journey.detach()
74
+ await journey.visible('Coroutine result 44')
75
+ journey.observations.push({ journey: 'completion, breakpoint workaround, synchronous steps, guards, cancelled wait', status: 'PASS' })
76
+ }
77
+ async function boundaryStep(journey: CoroutineJourney) {
78
+ await journey.attach()
79
+ const bp = await journey.breakpoint('BREAK_SUSPEND')
80
+ const tap = await journey.tap('Complete run')
81
+ const stop = await requiredStop(journey, 'BREAK_SUSPEND')
82
+ await journey.remove(bp)
83
+ await journey.step(stop, 'over')
84
+ const next = await journey.stop(undefined, 3)
85
+ if (next) await journey.inspect(next, 'suspension step observed stop')
86
+ journey.observations.push({ journey: 'suspension boundary step', status: 'PASS', observation: next ?? 'No stop within three one-second polls; no coroutine-following claim' })
87
+ await journey.detach()
88
+ await tap.done
89
+ await journey.visible('Coroutine waiting')
90
+ const release = await journey.tap('Release gate')
91
+ await release.done
92
+ await journey.visible('Coroutine result 44')
93
+ }
94
+ async function cancellation(journey: CoroutineJourney) {
95
+ await journey.attach()
96
+ const bp = await journey.breakpoint('BREAK_CANCEL', '$cancelWork$2$child$1')
97
+ const start = await journey.tap('Cancellation run')
98
+ await start.done
99
+ await journey.visible('Cancellation waiting')
100
+ const cancel = await journey.tap('Cancel job')
101
+ const stop = await requiredStop(journey, 'BREAK_CANCEL')
102
+ await journey.inspect(stop, 'cancelled child finally')
103
+ await journey.remove(bp)
104
+ await journey.resume(stop)
105
+ await cancel.done
106
+ await journey.detach()
107
+ await journey.visible('Coroutine cancelled')
108
+ journey.observations.push({ journey: 'job cancellation, child finally, cancelAndJoin and recovery', status: 'PASS' })
109
+ }
110
+ async function evidence(directory: string, artifact: string, journey: CoroutineJourney) {
111
+ const javap = process.env.JAVA_HOME ? process.env.JAVA_HOME + '/bin/javap' : 'javap'
112
+ const classes = (await readdir(directory + '/classes/dev/mobiledebugmcp/coroutinefixture')).filter(file => file.endsWith('.class')).sort()
113
+ const metadata = execFileSync(javap, ['-p', '-c', '-l', '-classpath', directory + '/classes', ...classes.map(file => coroutineAppId + '.' + file.slice(0, -6))], { encoding: 'utf8', timeout: 15000 })
114
+ for (const name of ['known', 'after', 'answer']) assert.ok(metadata.includes(name), 'Compiler variable metadata: ' + name)
115
+ for (const marker of ['BREAK_BEFORE', 'BREAK_AFTER', 'BREAK_DISPATCH', 'BREAK_CANCEL']) assert.ok(metadata.includes('line ' + journey.line(marker) + ':'), marker)
116
+ const version = spawnSync(process.env.MCP_KOTLINC ?? 'kotlinc', ['-version'], { encoding: 'utf8', timeout: 15000 })
117
+ assert.equal(version.status, 0, 'Kotlin compiler version probe')
118
+ const compiler = (version.stdout + version.stderr).trim()
119
+ const dependencies = []
120
+ for (const name of ['MCP_KOTLIN_STDLIB', 'MCP_COROUTINES_CORE']) dependencies.push({ name, file: process.env[name], sha256: hash(await readFile(process.env[name]!)) })
121
+ return { source_sha256: hash(source), apk_sha256: hash(await readFile(artifact)), dist_sha256: await fingerprint('dist'), worker_sha256: await fingerprint('src/debugger/java'), compiler, dependencies, metadata_sha256: hash(metadata), compiler_metadata: metadata }
122
+ }
123
+ async function run(deviceId: string) {
124
+ const journey = new CoroutineJourney(deviceId, source)
125
+ assert.equal(journey.adb('shell', 'pm', 'list', 'packages', coroutineAppId).trim(), '', 'Refusing to replace pre-existing fixture package')
126
+ let fixture: ReturnType<typeof buildCoroutineFixture> | undefined
127
+ let installed = false
128
+ const interrupt = () => journey.interruption.abort()
129
+ process.once('SIGINT', interrupt)
130
+ process.once('SIGTERM', interrupt)
131
+ const forwards = () => deviceForwards(journey.adb('forward', '--list'), deviceId)
132
+ const baselineForwards = forwards()
133
+ await reportAfterCleanup(async () => {
134
+ try { fixture = buildCoroutineFixture() } catch (error) {
135
+ console.error('BLOCKED coroutine fixture build: check documented local compiler, jars and SDK prerequisites')
136
+ throw error
137
+ }
138
+ const fingerprints = await evidence(fixture.directory, fixture.artifact, journey)
139
+ await journey.connect()
140
+ installed = true // A failed install may have left a partial owned package.
141
+ journey.adb('install', fixture.artifact)
142
+ journey.adb('shell', 'am', 'start', '-W', '-n', coroutineAppId + '/.MainActivity')
143
+ await journey.visible('Coroutine ready')
144
+ await completion(journey)
145
+ await boundaryStep(journey)
146
+ journey.adb('shell', 'am', 'force-stop', coroutineAppId)
147
+ journey.adb('shell', 'am', 'start', '-W', '-n', coroutineAppId + '/.MainActivity')
148
+ await journey.visible('Coroutine ready')
149
+ await cancellation(journey)
150
+ await journey.detach()
151
+ assert.deepEqual(forwards(), baselineForwards)
152
+ return { api: journey.adb('shell', 'getprop', 'ro.build.version.sdk').trim(), runtime: process.version, ...fingerprints }
153
+ }, async () => {
154
+ process.removeListener('SIGINT', interrupt)
155
+ process.removeListener('SIGTERM', interrupt)
156
+ try { await journey.detach() } finally {
157
+ try { await journey.client.close() } finally {
158
+ try { if (installed) journey.adb('uninstall', coroutineAppId) } finally { if (fixture) await rm(fixture.directory, { recursive: true, force: true }) }
159
+ }
160
+ }
161
+ assert.deepEqual(forwards(), baselineForwards, 'Owned forward cleanup')
162
+ }, (status, fingerprints, error) => {
163
+ const record = JSON.stringify({ status, device: deviceId, ...fingerprints, observations: journey.observations, ...(error ? { error: error instanceof Error ? error.message : String(error) } : {}) })
164
+ if (status === 'PASS') console.log(record)
165
+ else console.error(record)
166
+ })
167
+ }
168
+ if (!device) console.log('UNRUN coroutine debugger fixture: set MCP_COROUTINE_DEVICE, MCP_KOTLINC, MCP_KOTLIN_STDLIB and MCP_COROUTINES_CORE')
169
+ else await run(device)
@@ -0,0 +1,127 @@
1
+ import assert from 'node:assert/strict'
2
+ import { execFileSync, spawn, spawnSync } from 'node:child_process'
3
+ import { createHash } from 'node:crypto'
4
+ import { readFile, readdir, rm } from 'node:fs/promises'
5
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
6
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
7
+ import { buildDebuggerFixture, debuggerAppId } from '../../../fixtures/debugger-app/build.js'
8
+
9
+ const deviceId = process.env.MCP_KOTLIN_DEVICE
10
+ if (!deviceId) {
11
+ console.log('UNRUN Kotlin debugger fixture: set MCP_KOTLIN_DEVICE, MCP_KOTLINC and MCP_KOTLIN_STDLIB')
12
+ } else {
13
+ const fixture = buildDebuggerFixture(true)
14
+ const adbPath = process.env.ADB_PATH ?? 'adb'
15
+ const adb = (...args: string[]) => execFileSync(adbPath, ['-s', deviceId, ...args], { encoding: 'utf8', timeout: 15000 })
16
+ const client = new Client({ name: 'kotlin-locals-validation', version: '1.0.0' })
17
+ let debugSessionId = ''
18
+ let cursor = 0
19
+ const call = async (name: string, args: Record<string, unknown>) => {
20
+ const reply = await client.callTool({ name, arguments: args }, undefined, { timeout: 45000 })
21
+ const text = reply.content.find(item => item.type === 'text')
22
+ assert.ok(text?.type === 'text')
23
+ return JSON.parse(text.text)
24
+ }
25
+ const source = await readFile(new URL('../../../fixtures/debugger-app/MainActivity.kt', import.meta.url))
26
+ const line = (marker: string) => source.toString().split('\n').findIndex(row => row.includes(marker)) + 1
27
+ const hash = createHash('sha256')
28
+ const fingerprint = async (directory: string): Promise<void> => {
29
+ for (const entry of (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
30
+ const file = directory + '/' + entry.name
31
+ if (entry.isDirectory()) await fingerprint(file)
32
+ else hash.update(file).update(await readFile(file))
33
+ }
34
+ }
35
+ const waitStop = async (expectedLine: number) => {
36
+ for (let i = 0; i < 10; i++) {
37
+ const response = await call('wait_for_debugger_event', { debugSessionId, afterSequence: cursor, timeoutMs: 1000, responseMode: 'compact' })
38
+ assert.equal(response.overflow, false)
39
+ cursor = response.next_cursor
40
+ const event = response.events.find((item: any) => item.event === 'stopped')
41
+ if (event) {
42
+ assert.equal(event.stops[0].location.line, expectedLine)
43
+ assert.equal(event.stops[0].location.method, 'calculate')
44
+ assert.equal(event.stops[0].location.source, 'MainActivity.kt')
45
+ return event
46
+ }
47
+ }
48
+ throw new Error('Expected Kotlin source stop not observed')
49
+ }
50
+ const inspect = async (stop: any) => {
51
+ const stack = await call('get_debugger_stack', { debugSessionId, stopId: stop.stop_id, threadId: stop.thread_id, responseMode: 'compact' })
52
+ const frameId = stack.result.frames[0].frame_id
53
+ const locals = await call('get_debugger_locals', { debugSessionId, stopId: stop.stop_id, frameId, responseMode: 'compact' })
54
+ assert.equal(locals.success, true)
55
+ return { locals: locals.result.locals as { name: string; value: string }[], frameId, availability: locals.result.availability }
56
+ }
57
+ try {
58
+ const forwardsBefore = adb('forward', '--list').trim()
59
+ const javap = process.env.JAVA_HOME ? process.env.JAVA_HOME + '/bin/javap' : 'javap'
60
+ const bytecode = execFileSync(javap, ['-p', '-c', '-l', '-classpath', fixture.directory + '/classes', `${debuggerAppId}.MainActivity`], { encoding: 'utf8' })
61
+ const scopes = Object.fromEntries(['seed', 'known', 'answer'].map(name => {
62
+ const match = bytecode.match(new RegExp('\\s+(\\d+)\\s+(\\d+)\\s+\\d+\\s+' + name + '\\s+I'))
63
+ assert.ok(match, 'Compiler emits a variable scope for ' + name)
64
+ return [name, { start: Number(match[1]), length: Number(match[2]) }]
65
+ }))
66
+ assert.equal(scopes.seed.start, 0)
67
+ assert.ok(scopes.known.start > 0 && scopes.answer.start > scopes.known.start)
68
+ await fingerprint('dist'); await fingerprint('src/debugger/java')
69
+ const version = spawnSync(process.env.MCP_KOTLINC ?? 'kotlinc', ['-version'], { encoding: 'utf8' })
70
+ assert.equal(version.status, 0)
71
+ const compiler = (version.stdout + version.stderr).match(/kotlinc-jvm [^\r\n]+/)?.[0]
72
+ assert.ok(compiler, 'Kotlin compiler identifies its version')
73
+ await client.connect(new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' }))
74
+ adb('install', '-r', fixture.artifact)
75
+ adb('shell', 'am', 'start', '-W', '-n', `${debuggerAppId}/.MainActivity`)
76
+ const ready = await call('get_ui_tree', { platform: 'android', deviceId })
77
+ const button = ready.elements.find((element: any) => /^kotlin calculate$/i.test(element.text ?? ''))
78
+ assert.ok(button, 'Fresh UI identifies the isolated Kotlin calculation: ' + JSON.stringify(ready))
79
+ const attached = await call('attach_debugger', { platform: 'android', deviceId, appId: debuggerAppId, timeoutMs: 30000 })
80
+ debugSessionId = attached.debug_session_id ?? ''
81
+ assert.equal(attached.success, true, JSON.stringify(attached))
82
+ const breakpoints = []
83
+ for (const marker of ['BREAK_KOTLIN_BEFORE', 'BREAK_KOTLIN_KNOWN', 'BREAK_KOTLIN_RESULT']) {
84
+ const breakpoint = await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.MainActivity`, line: line(marker), responseMode: 'compact' })
85
+ assert.equal(breakpoint.result.status, 'verified', JSON.stringify(breakpoint))
86
+ breakpoints.push(breakpoint.result.breakpoint_id)
87
+ }
88
+ const bounds = button.bounds
89
+ const tap = spawn(adbPath, ['-s', deviceId, 'shell', 'input', 'tap', String(Math.floor((bounds[0] + bounds[2]) / 2)), String(Math.floor((bounds[1] + bounds[3]) / 2))], { stdio: 'ignore' })
90
+ const tapDone = new Promise<void>((resolve, reject) => { tap.on('error', reject); tap.on('exit', code => code === 0 ? resolve() : reject(new Error('Fixture tap failed'))) })
91
+ void tapDone.catch(() => {})
92
+ const before = await waitStop(line('BREAK_KOTLIN_BEFORE'))
93
+ const beforeValues = await inspect(before)
94
+ assert.equal(beforeValues.locals.find(value => value.name === 'seed')?.value, '7')
95
+ assert.equal(beforeValues.locals.find(value => value.name === 'known'), undefined, 'Local is not initialized/in scope at its assignment')
96
+ await call('resume_debugger', { debugSessionId, stopId: before.stop_id, responseMode: 'compact' })
97
+ const known = await waitStop(line('BREAK_KOTLIN_KNOWN'))
98
+ const knownValues = await inspect(known)
99
+ assert.equal(knownValues.locals.find(value => value.name === 'known')?.value, '42')
100
+ assert.equal(knownValues.locals.find(value => value.name === 'answer'), undefined)
101
+ const stale = await call('get_debugger_locals', { debugSessionId, stopId: before.stop_id, frameId: beforeValues.frameId, responseMode: 'compact' })
102
+ assert.equal(stale.error.code, 'DEBUGGER_STALE_REFERENCE')
103
+ await call('resume_debugger', { debugSessionId, stopId: known.stop_id, responseMode: 'compact' })
104
+ const result = await waitStop(line('BREAK_KOTLIN_RESULT'))
105
+ const resultValues = await inspect(result)
106
+ assert.equal(resultValues.locals.find(value => value.name === 'known')?.value, '42')
107
+ assert.equal(resultValues.locals.find(value => value.name === 'answer')?.value, '43')
108
+ for (const breakpointId of breakpoints) await call('remove_debugger_breakpoint', { debugSessionId, breakpointId, responseMode: 'compact' })
109
+ await call('resume_debugger', { debugSessionId, stopId: result.stop_id, responseMode: 'compact' })
110
+ await tapDone
111
+ const detached = await call('detach_debugger', { debugSessionId, responseMode: 'compact' })
112
+ assert.equal(detached.cleanup.status, 'complete')
113
+ assert.deepEqual(detached.cleanup.remaining_resources, [])
114
+ assert.equal(adb('forward', '--list').trim(), forwardsBefore)
115
+ const recovered = await call('get_ui_tree', { platform: 'android', deviceId })
116
+ assert.ok(recovered.elements.some((element: any) => element.text === 'Kotlin result 43'), 'Fresh hierarchy proves execution recovery')
117
+ console.log(JSON.stringify({ status: 'PASS', server_sha256: hash.digest('hex'), source_sha256: createHash('sha256').update(source).digest('hex'), apk_sha256: createHash('sha256').update(await readFile(fixture.artifact)).digest('hex'), compiler, bytecode_scopes: scopes, device: deviceId, api: adb('shell', 'getprop', 'ro.build.version.sdk').trim(), runtime: process.version, lines: [line('BREAK_KOTLIN_BEFORE'), line('BREAK_KOTLIN_KNOWN'), line('BREAK_KOTLIN_RESULT')], observations: ['seed=7; known absent before assignment', 'known=42; answer absent before assignment', 'known=42; answer=43', 'old frame rejected as stale', 'fresh UI Kotlin result 43 after detach'], cleanup: 'owned forwards removed; fixture package and temporary files removed in finally' }))
118
+ } finally {
119
+ if (debugSessionId) {
120
+ const cleanup = await call('detach_debugger', { debugSessionId }).catch(() => null)
121
+ if (cleanup?.cleanup.status !== 'complete') console.error('UNRESOLVED Kotlin debugger cleanup', JSON.stringify(cleanup?.cleanup ?? null))
122
+ }
123
+ try { await client.close() } finally {
124
+ try { adb('uninstall', debuggerAppId) } finally { await rm(fixture.directory, { recursive: true, force: true }) }
125
+ }
126
+ }
127
+ }
@@ -1,3 +1,5 @@
1
+ import { debuggerResponse } from '../../../../src/server/handlers/debugger-response.js'
2
+ import { debuggerContracts } from '../../../../src/server/schemas/debugger-schema.js'
1
3
  import assert from 'node:assert/strict'
2
4
  import { execFileSync, spawn } from 'node:child_process'
3
5
  import { readFile, readdir, rm, writeFile } from 'node:fs/promises'
@@ -14,10 +16,23 @@ if (!process.env.MCP_DEBUGGER_DEVICE) {
14
16
  const adb = (...args: string[]) => execFileSync(process.env.ADB_PATH || 'adb', ['-s', deviceId, ...args], { encoding: 'utf8', timeout: 15000 })
15
17
  const client = new Client({ name: 'android-debugger-smoke', version: '1.0.0' })
16
18
  const transport = new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' })
19
+ const compactMode = process.env.MCP_DEBUGGER_RESPONSE_MODE === 'compact'
20
+ const measurements: { tool: string; debug_bytes: number; compact_bytes: number }[] = []
17
21
  const call = async (name: string, args: Record<string, unknown>) => {
18
- const response = await client.callTool({ name, arguments: args }, undefined, { timeout: 45000 })
22
+ const response = await client.callTool({ name, arguments: { ...args, ...(debuggerContracts[name] && compactMode ? { responseMode: 'compact' } : {}) } }, undefined, { timeout: 45000 })
19
23
  const text = (response.content as any[]).find(c => c.type === 'text')!.text!
20
- return JSON.parse(text)
24
+ const payload = JSON.parse(text)
25
+ if (debuggerContracts[name]) {
26
+ const command = debuggerContracts[name].command
27
+ if (compactMode && !['attach', 'state', 'capabilities'].includes(command) && payload.debug_session_id != null) {
28
+ for (const field of ['device', 'target', 'capabilities']) assert.ok(!Object.hasOwn(payload, field))
29
+ }
30
+ if (!compactMode) {
31
+ const compact = debuggerResponse(command, 'compact', payload)
32
+ measurements.push({ tool: name, debug_bytes: Buffer.byteLength(JSON.stringify(response)), compact_bytes: Buffer.byteLength(JSON.stringify({ ...response, content: [{ type: 'text', text: JSON.stringify(compact, null, 2) }] })) })
33
+ }
34
+ }
35
+ return payload
21
36
  }
22
37
  const hash = createHash('sha256')
23
38
  const fingerprint = async (directory: string): Promise<void> => {
@@ -27,7 +42,7 @@ if (!process.env.MCP_DEBUGGER_DEVICE) {
27
42
  else hash.update(file).update(await readFile(file))
28
43
  }
29
44
  }
30
- await fingerprint('dist'); await fingerprint('worker')
45
+ await fingerprint('dist'); await fingerprint('src/debugger/java')
31
46
  const buildHash = hash.digest('hex')
32
47
  const fixture = buildDebuggerFixture()
33
48
  const source = await readFile(new URL('../../../fixtures/debugger-app/MainActivity.java', import.meta.url), 'utf8')
@@ -193,6 +208,7 @@ if (!process.env.MCP_DEBUGGER_DEVICE) {
193
208
  assert.ok(treeAfterShutdown.elements.some((e: any) => e.text === 'Result 43'), 'fresh UI proves paused fixture resumed after server shutdown')
194
209
  } finally { await verifier.close() }
195
210
  console.log(JSON.stringify({ status: 'PASS', build_sha256: buildHash, device: deviceId, api: adb('shell', 'getprop', 'ro.build.version.sdk').trim(), runtime: process.version, transport: 'MCP stdio', extended: process.env.MCP_DEBUGGER_EXTENDED === '1' }))
211
+ console.log('Debugger response measurement', JSON.stringify({ mode: compactMode ? 'compact' : 'debug', calls: measurements, debug_bytes: measurements.reduce((sum, row) => sum + row.debug_bytes, 0), compact_bytes: measurements.reduce((sum, row) => sum + row.compact_bytes, 0) }))
196
212
  console.log('PASS Android debugger: explicit attach, source stop, stack/known locals, pause guards, into/over/out, stale frame, resume UI, detach/forward ownership')
197
213
  } finally {
198
214
  if (debugSessionId) await call('detach_debugger', { debugSessionId }).catch(() => {})