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
@@ -1,6 +1,21 @@
1
+ import { debuggerTools } from './debugger-schema.js'
2
+ import { systemTraceTools } from './system-trace-schema.js'
1
3
  import { journeyStepsJson } from './journey-schema.js'
4
+ import { conditionalTools, expectedStateJson } from './condition-schema.js'
5
+ import { readinessPropertiesJson } from './readiness-schema.js'
2
6
 
3
7
  export const toolDefinitions: any[] = [
8
+ ...debuggerTools,
9
+ ...systemTraceTools,
10
+ ...['get_app_readiness', 'wait_for_app_ready'].map(name => ({
11
+ name,
12
+ description: `${name === 'get_app_readiness' ? 'Observe readiness with at most two fresh UI-tree samples.' : 'Wait within a single bounded budget for settled UI evidence.'} Reports loading, transitioning, settled or unknown with evidence and limitations. Managed Android debugger pauses are reported from debugger evidence; unresponsive detection remains unsupported. Settled normalized foreground UI is not proof of app identity, responsiveness or backend completion.`,
13
+ inputSchema: { type: 'object', additionalProperties: false, required: ['platform'], properties: {
14
+ ...readinessPropertiesJson,
15
+ timeoutMs: { ...readinessPropertiesJson.timeoutMs, default: name === 'get_app_readiness' ? 15000 : 30000 },
16
+ ...(name === 'wait_for_app_ready' ? { pollIntervalMs: { type: 'integer', minimum: 100, maximum: 1000, default: 500 } } : {})
17
+ } }
18
+ })),
4
19
  {
5
20
  name: 'start_app',
6
21
  description: `Purpose:
@@ -1130,6 +1145,26 @@ Returns { requests: [], count: 0 } when no credible network signals are found.`,
1130
1145
 
1131
1146
  // Keep the additive execution controls consistent across all action tools.
1132
1147
  for (const tool of toolDefinitions) {
1148
+ if (['wait_for_ui', 'wait_for_ui_change'].includes(tool.name)) {
1149
+ tool.inputSchema.properties.requireReady = { type: 'boolean', default: false }
1150
+ tool.description += '\nSet requireReady=true to require both the existing predicate and settled UI readiness, using the same fresh samples and one timeout budget. The readiness summary explains uncertainty. Default predicate semantics are unchanged.'
1151
+ }
1152
+ if (tool.name === 'get_ui_tree') tool.description += '\nSuccessful observations include snapshot_session_id and snapshot_revision for same-device conditional input. Compact errors preserve the error and cannot authorize an expectation.'
1153
+ if (tool.name === 'run_journey') tool.description += '\nTap steps may include expectedState from a successful same-device get_ui_tree. A failed condition blocks that input, retains fresh condition evidence even in compact mode, and stops subsequent steps. Conditions are not journey defaults or supported on other step types.'
1154
+ if (conditionalTools.has(tool.name)) {
1155
+ tool.inputSchema.properties.expectedState = expectedStateJson
1156
+ tool.inputSchema.properties.platform ??= { type: 'string', enum: ['android'] }
1157
+ tool.inputSchema.allOf = [...(tool.inputSchema.allOf ?? []), {
1158
+ if: { required: ['expectedState'] },
1159
+ then: { required: ['platform', 'deviceId'], properties: {
1160
+ platform: { enum: ['type_text', 'press_back'].includes(tool.name) ? ['android'] : ['android', 'ios'] },
1161
+ deviceId: { type: 'string', minLength: 1, pattern: '\\S', not: { enum: ['booted', 'default'] } }
1162
+ } }
1163
+ }]
1164
+ tool.description += '\n\nConditional input: optionally supply expectedState { snapshotSessionId, snapshotRevision, timeoutMs? } from a successful get_ui_tree on the same explicit platform/deviceId. A fresh normalized UI-tree check runs immediately before input, even with verification disabled. PRECONDITION_FAILED or PRECONDITION_UNAVAILABLE means no input was dispatched; inspect condition evidence and re-observe before deciding what to do next. Compact failures include the fresh UI tree. This is not atomic with OS input delivery.'
1165
+ } else {
1166
+ tool.inputSchema.allOf = [...(tool.inputSchema.allOf ?? []), { not: { required: ['expectedState'] } }]
1167
+ }
1133
1168
  if (['start_app', 'restart_app', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back'].includes(tool.name)) {
1134
1169
  Object.assign(tool.inputSchema.properties, {
1135
1170
  verificationMode: { type: 'string', enum: ['none', 'light', 'full'], default: 'full' },
@@ -1141,6 +1176,7 @@ for (const tool of toolDefinitions) {
1141
1176
  if (tool.name === 'tap_element') Object.assign(tool.inputSchema, { oneOf: [{ required: ['elementId'], not: { required: ['selector'] } }, { required: ['selector'], not: { required: ['elementId'] } }] })
1142
1177
  if (tool.name === 'run_journey') Object.assign(tool.inputSchema.properties, {
1143
1178
  steps: journeyStepsJson,
1179
+ systemTraceId: { type: 'string', format: 'uuid', description: 'Explicit active same-device system trace. Invalid references prevent all journey input. Capture lifetime is controlled separately.' },
1144
1180
  captureOnFailure: { type: 'boolean', default: false },
1145
1181
  defaults: { type: 'object', additionalProperties: false, properties: {
1146
1182
  verificationMode: { type: 'string', enum: ['none', 'light', 'full'] },
@@ -1148,3 +1184,16 @@ for (const tool of toolDefinitions) {
1148
1184
  } }
1149
1185
  })
1150
1186
  }
1187
+
1188
+ // Correlation is explicit; an omitted reference creates an independent observation.
1189
+ for (const tool of toolDefinitions) {
1190
+ if (['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'read_log_stream'].includes(tool.name)) {
1191
+ Object.assign(tool.inputSchema.properties, { actionRef: {
1192
+ type: 'object', additionalProperties: false, required: ['sessionId', 'actionId'], properties: {
1193
+ sessionId: { type: 'string', minLength: 1, maxLength: 128 },
1194
+ actionId: { type: 'string', minLength: 1, maxLength: 128 }
1195
+ }
1196
+ } })
1197
+ tool.description += '\nReturns evidence metadata with acquisition intervals and conservative freshness. Optional actionRef links a known same-session/device action; unavailable references collect independently. Device log times are not comparable with host dispatch times.'
1198
+ }
1199
+ }
@@ -1,7 +1,18 @@
1
+ import { handleDebugger } from './debugger-handlers.js'
2
+ import { debuggerContracts } from './debugger-schema.js'
3
+ import { debuggerSessions } from '../debugger/sessions.js'
4
+ import { withDebuggerGuard, guardDispatched } from '../debugger/guard.js'
5
+ import { systemTraces, TraceError } from '../observe/system-trace.js'
6
+ import { systemTraceTools, traceTargetSchema, traceStartSchema, traceControlSchema } from './system-trace-schema.js'
7
+ import { currentEvidenceCorrelationId, bindSystemTrace, withEvidence, currentEvidenceActionId, validateActionRef, markEvidenceVerification, refreshJourneyEvidence, completeJourneyVerification } from '../observe/evidence.js'
1
8
  import { measure, withTiming } from '../utils/timing.js'
2
9
  import { randomUUID } from 'node:crypto'
3
10
  import { runWithBudget } from '../utils/operation-budget.js'
4
11
  import { journeySchema, tapElementControlsSchema } from './journey-schema.js'
12
+ import { readinessSchema, readinessWaitSchema } from './readiness-schema.js'
13
+ import { getAppReadiness, withReadyWait } from '../observe/readiness-service.js'
14
+ import { validateExpectedState } from './condition-schema.js'
15
+ import { conditionFailureCode, createConditionalAction, notifyActionStart, withConditionalAction } from '../interact/conditional-action.js'
5
16
  import { resolveTargetDevice, withResolvedDevice } from '../utils/resolve-device.js'
6
17
  import type {
7
18
  InstallAppResponse,
@@ -36,6 +47,8 @@ import {
36
47
  wrapToolError
37
48
  } from './common.js'
38
49
 
50
+ const systemTraceToolNames = new Set(systemTraceTools.map(tool => tool.name))
51
+
39
52
  type PlatformArg = 'android' | 'ios'
40
53
  type ProjectTypeArg = 'native' | 'kmp' | 'react-native' | 'flutter'
41
54
  type ExpectElementSelectorArg = { text?: string, resource_id?: string, accessibility_id?: string, contains?: boolean }
@@ -76,17 +89,42 @@ function compactAction(result: any, verificationMode: VerificationMode, started
76
89
  }
77
90
  }
78
91
 
92
+ function correlatedHandler(name: string, handler: ToolHandler, action = false, journey = false): ToolHandler {
93
+ return async args => {
94
+ const ref = validateActionRef(args.actionRef)
95
+ const { result, evidence } = await withEvidence({ action, journey, ref }, async () => {
96
+ try { return await handler(args) }
97
+ catch (error) { return error instanceof TraceError ? { ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true } : wrapToolError(name, error) }
98
+ })
99
+ const item = result.content.find(item => item.type === 'text')
100
+ if (item?.text) {
101
+ const payload = JSON.parse(item.text)
102
+ payload.evidence = evidence
103
+ item.text = JSON.stringify(payload)
104
+ }
105
+ return result
106
+ }
107
+ }
108
+
79
109
  function actionHandler(type: string, execute: ToolHandler): ToolHandler {
80
- return async (args) => {
110
+ return correlatedHandler(type, async (args) => {
81
111
  const timing: Record<string, number> = {}
82
112
  return withTiming(timing, async () => {
83
113
  const started = performance.now()
84
114
  const controls = interactionControls(args)
115
+ const expected = validateExpectedState(type, args)
116
+ const condition = expected ? createConditionalAction(args.platform as PlatformArg, args.deviceId as string, expected) : undefined
85
117
  const budgetMs = controls.actionTimeoutMs ?? ({ start_app: 10000, restart_app: 15000, scroll_to_element: 10000 }[type] ?? 5000)
86
118
  let dispatchStarted = false
87
119
  let result: any
88
120
  const dispatchWithBudget = (timeoutMs: number) => runWithBudget(timeoutMs, async (budget) => {
89
- try { return payloadOf(await execute(args)) }
121
+ try {
122
+ if (!condition) return payloadOf(await execute(args))
123
+ return await withConditionalAction(condition, async () => {
124
+ const device = await resolveTargetDevice({ platform: condition.platform, deviceId: condition.deviceId })
125
+ return withResolvedDevice(device, async () => payloadOf(await execute(args)))
126
+ })
127
+ }
90
128
  finally { dispatchStarted = budget.dispatchStarted }
91
129
  })
92
130
  try {
@@ -95,13 +133,24 @@ function actionHandler(type: string, execute: ToolHandler): ToolHandler {
95
133
  ? await dispatchWithBudget(Infinity) : await dispatchWithBudget(budgetMs)
96
134
  if (result.success) dispatchStarted = true
97
135
  } catch (error) {
98
- if (controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined && !/TIMEOUT|timed out/i.test(String(error))) return wrapToolError(type, error)
136
+ if (!condition && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined && !/TIMEOUT|timed out/i.test(String(error))) return wrapToolError(type, error)
99
137
  result = { success: false, failure_code: /TIMEOUT|timed out/i.test(String(error)) ? 'ACTION_TIMEOUT' : 'UNKNOWN', error: String(error) }
100
138
  }
139
+ const blocked = condition && ['mismatch', 'unavailable'].includes(condition.result.status)
140
+ if (blocked) {
141
+ dispatchStarted = false
142
+ result = buildActionExecutionResult({
143
+ actionType: type, sourceModule: 'server', device: condition.result.observed?.device,
144
+ selector: result?.target?.selector ?? null, resolved: result?.target?.resolved,
145
+ success: false, uiFingerprintBefore: result?.ui_fingerprint_before ?? null, uiFingerprintAfter: null,
146
+ failure: { failureCode: conditionFailureCode(condition), retryable: false },
147
+ traceSteps: [{ stage: 'verify', timestamp: Date.now(), result: 'failure', attempt_index: 0, metadata: { phase: 'precondition', dispatch_started: false } }]
148
+ })
149
+ }
101
150
  if (result.failure_code === 'TIMEOUT' || /ACTION_TIMEOUT|timeout/i.test(result.error ?? result.reason ?? result.details?.error ?? '')) result.failure_code = 'ACTION_TIMEOUT'
102
151
  if (result.failure_code === 'ACTION_TIMEOUT') result.retryable = !dispatchStarted
103
152
  if (!result.success && typeof result.retryable !== 'boolean') result.retryable = !dispatchStarted
104
- result.action_id ??= randomUUID()
153
+ result.action_id ??= currentEvidenceActionId() ?? randomUUID()
105
154
  result.action_type ??= type
106
155
  recordObservationEvent({
107
156
  step_id: args._stepId as string | undefined,
@@ -117,6 +166,7 @@ function actionHandler(type: string, execute: ToolHandler): ToolHandler {
117
166
  const tree = await withObservationContext({ stepId: args._stepId as string | undefined, purpose: 'verification' }, () =>
118
167
  measure('post_observation_ms', () => runWithBudget(controls.verificationTimeoutMs, () =>
119
168
  ToolsObserve.getUITreeHandler({ platform: (args.platform as PlatformArg | undefined) ?? result.device?.platform, deviceId: (args.deviceId as string | undefined) ?? result.device?.id })))).catch(() => null) as any
169
+ markEvidenceVerification()
120
170
  observationAvailable = !!tree && !tree.error
121
171
  recordObservationEvent({
122
172
  verification_budget_ms: controls.verificationTimeoutMs,
@@ -126,10 +176,18 @@ function actionHandler(type: string, execute: ToolHandler): ToolHandler {
126
176
  })
127
177
  if (observationAvailable && tree.device) delta = ToolsObserve.getLatestStateDelta(tree.device.platform, tree.device.id) ?? undefined
128
178
  }
179
+ if (!blocked && controls.verificationMode === 'full' && dispatchStarted) markEvidenceVerification()
129
180
  const compact = compactAction(result, controls.verificationMode, dispatchStarted, delta, performance.now() - started)
181
+ if (blocked) compact.verification.status = 'not_requested'
130
182
  if (result.success && controls.verificationMode === 'light' && !observationAvailable) compact.verification.status = 'unavailable'
131
183
  const evidence = {
132
184
  ...compact,
185
+ ...(condition ? { condition: controls.responseMode === 'compact' && condition.result.status === 'matched'
186
+ ? { ...condition.result, observed: condition.result.observed ? {
187
+ snapshot_session_id: condition.result.observed.snapshot_session_id,
188
+ snapshot_revision: condition.result.observed.snapshot_revision,
189
+ captured_at_ms: condition.result.observed.captured_at_ms
190
+ } : null } : condition.result } : {}),
133
191
  ...(controls.verificationMode === 'light' ? { state_delta_available: !!delta } : {}),
134
192
  ...(result.success && controls.verificationMode === 'light' && !observationAvailable ? { verification_diagnostic: { code: 'VERIFICATION_UNAVAILABLE', retryable: true } } : {})
135
193
  }
@@ -138,7 +196,7 @@ function actionHandler(type: string, execute: ToolHandler): ToolHandler {
138
196
  effective_controls: { ...controls, actionTimeoutMs: args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined ? null : budgetMs, timeout_bound: !(args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined) }
139
197
  }, controls.responseMode === 'debug')
140
198
  })
141
- }
199
+ }, true)
142
200
  }
143
201
  const handleStartApp = actionHandler('start_app', executeStartApp)
144
202
  const handleRestartApp = actionHandler('restart_app', executeRestartApp)
@@ -155,7 +213,7 @@ async function executeStartApp(args: ToolCallArgs) {
155
213
  const deviceId = getStringArg(args, 'deviceId')
156
214
  const { verificationMode, actionTimeoutMs } = interactionControls(args)
157
215
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
158
- ToolsNetwork.notifyActionStart()
216
+ notifyActionStart()
159
217
  const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().startApp(appId, deviceId, actionTimeoutMs ?? (verificationMode === 'full' && getStringArg(args, 'responseMode') === undefined && args._journey !== true ? undefined : 10000)) : new iOSManage().startApp(appId, deviceId, verificationMode === 'full')))
160
218
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
161
219
  const result = buildActionExecutionResult({
@@ -194,7 +252,7 @@ async function executeRestartApp(args: ToolCallArgs) {
194
252
  const deviceId = getStringArg(args, 'deviceId')
195
253
  const { verificationMode, actionTimeoutMs } = interactionControls(args)
196
254
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
197
- ToolsNetwork.notifyActionStart()
255
+ notifyActionStart()
198
256
  const bounded = args._journey === true || verificationMode !== 'full' || args.responseMode !== undefined || actionTimeoutMs !== undefined
199
257
  const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().restartApp(appId, deviceId, bounded, actionTimeoutMs ?? 15000) : new iOSManage().restartApp(appId, deviceId, verificationMode === 'full', bounded, actionTimeoutMs ?? 15000)))
200
258
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
@@ -425,10 +483,12 @@ async function handleAdjustControl(args: ToolCallArgs) {
425
483
  platform,
426
484
  deviceId
427
485
  })
486
+ markEvidenceVerification()
428
487
  return wrapResponse(res)
429
488
  }
430
489
 
431
490
  async function handleWaitForUI(args: ToolCallArgs) {
491
+ if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean') throw new Error('INVALID_ARGUMENT: requireReady must be boolean')
432
492
  const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
433
493
  const condition = (getStringArg(args, 'condition') as 'exists' | 'not_exists' | 'visible' | 'clickable' | undefined) ?? 'exists'
434
494
  const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000
@@ -437,11 +497,14 @@ async function handleWaitForUI(args: ToolCallArgs) {
437
497
  const retry = getObjectArg<WaitForUiRetryArg>(args, 'retry')
438
498
  const platform = getStringArg(args, 'platform') as PlatformArg | undefined
439
499
  const deviceId = getStringArg(args, 'deviceId')
440
- const res = await ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform, deviceId })
500
+ const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform: targetPlatform, deviceId: targetId })
501
+ const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms,
502
+ device => execute(device.platform as PlatformArg, device.id), (code, elapsed, samples) => ({ status: 'timeout', error: { code, message: 'Readiness wait could not complete' }, metrics: { latency_ms: elapsed, poll_count: samples, attempts: samples ? 1 : 0 }, requested: { selector, condition, match }, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } })) : await execute()
441
503
  return wrapResponse(res)
442
504
  }
443
505
 
444
506
  async function handleWaitForUIChange(args: ToolCallArgs) {
507
+ if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean') throw new Error('INVALID_ARGUMENT: requireReady must be boolean')
445
508
  const platform = getStringArg(args, 'platform') as PlatformArg | undefined
446
509
  const deviceId = getStringArg(args, 'deviceId')
447
510
  const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000
@@ -449,7 +512,9 @@ async function handleWaitForUIChange(args: ToolCallArgs) {
449
512
  const expected_change = getStringArg(args, 'expected_change') as 'hierarchy_diff' | 'text_change' | 'state_change' | undefined
450
513
  const scope = getStringArg(args, 'scope') as 'screen' | 'subtree' | undefined
451
514
  const target = getStringArg(args, 'target')
452
- const res = await ToolsInteract.waitForUIChangeHandler({ platform, deviceId, timeout_ms, stability_window_ms, expected_change, scope, target })
515
+ const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIChangeHandler({ platform: targetPlatform, deviceId: targetId, timeout_ms, stability_window_ms, expected_change, scope, target })
516
+ const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms,
517
+ device => execute(device.platform as PlatformArg, device.id), (code, elapsed) => ({ success: false, timeout: true, observed_change: null, elapsed_ms: elapsed, reason: code, error: { code, message: 'Readiness wait could not complete' } })) : await execute()
453
518
  return wrapResponse(res)
454
519
  }
455
520
 
@@ -472,7 +537,7 @@ async function executeTap(args: ToolCallArgs) {
472
537
  const deviceId = getStringArg(args, 'deviceId')
473
538
  const { verificationMode, actionTimeoutMs } = interactionControls(args)
474
539
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
475
- ToolsNetwork.notifyActionStart()
540
+ notifyActionStart()
476
541
  const res = await measure('dispatch_ms', () => ToolsInteract.tapHandler({ platform, x, y, deviceId, timeoutMs: actionTimeoutMs }))
477
542
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
478
543
  const result = buildActionExecutionResult({
@@ -498,14 +563,15 @@ async function executeTapElement(args: ToolCallArgs) {
498
563
  if (selector) {
499
564
  const waitFor = getObjectArg<WaitForUiMatchArg>(args, 'waitFor') as any
500
565
  const targetStarted = performance.now()
566
+ const resolutionTimeoutMs = waitFor?.timeoutMs ?? actionTimeoutMs ?? 5000
501
567
  const resolution = await withObservationContext({ stepId: args._stepId as string | undefined, purpose: 'target' }, () =>
502
- measure('target_resolution_ms', () => runWithBudget(waitFor?.timeoutMs ?? 1500, () => ToolsInteract.waitForUIHandler({ selector, condition: waitFor?.condition ?? 'clickable', timeout_ms: waitFor?.timeoutMs ?? 1500, poll_interval_ms: waitFor?.pollIntervalMs ?? 100, match: waitFor?.match, singleObservation: true, singleAttempt: !waitFor, rejectAmbiguous: !waitFor, platform: getStringArg(args, 'platform') as PlatformArg | undefined, deviceId: getStringArg(args, 'deviceId') })))) as any
568
+ measure('target_resolution_ms', () => runWithBudget(resolutionTimeoutMs, () => ToolsInteract.waitForUIHandler({ selector, condition: waitFor?.condition ?? 'clickable', timeout_ms: resolutionTimeoutMs, poll_interval_ms: waitFor?.pollIntervalMs ?? 100, match: waitFor?.match, singleObservation: true, singleAttempt: !waitFor, rejectAmbiguous: !waitFor, resolveTapTarget: true, platform: getStringArg(args, 'platform') as PlatformArg | undefined, deviceId: getStringArg(args, 'deviceId') })))) as any
503
569
  recordObservationEvent({ step_id: args._stepId as string | undefined, purpose: 'target', target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: resolution.status })
504
570
  if (resolution.status !== 'success' || !resolution.element?.elementId) return wrapResponse({ success: false, action_type: 'tap_element', failure_code: resolution.error?.code ?? 'ELEMENT_NOT_FOUND', retryable: true })
505
571
  resolvedId = resolution.element.elementId
506
572
  freshTree = resolution._tree
507
573
  }
508
- ToolsNetwork.notifyActionStart()
574
+ notifyActionStart()
509
575
  if (freshTree) recordObservationConsumer(freshTree, 'target', args._stepId as string | undefined)
510
576
  const res = await measure('dispatch_ms', () => ToolsInteract.tapElementHandler({ elementId: resolvedId!, freshTree, verificationMode, timeoutMs: actionTimeoutMs, platform: getStringArg(args, 'platform') as PlatformArg | undefined, deviceId: getStringArg(args, 'deviceId') })) as any
511
577
  return wrapResponse(res)
@@ -554,13 +620,22 @@ async function handleRunJourney(args: ToolCallArgs) {
554
620
  const steps = parsed.data.steps as Array<Record<string, any>>
555
621
  const device = await resolveTargetDevice({ platform, deviceId: parsed.data.deviceId })
556
622
  const deviceId = device.id
557
- return withResolvedDevice(device, async () => {
623
+ const finishTrace = parsed.data.systemTraceId
624
+ ? await systemTraces.link(parsed.data.systemTraceId, device, currentEvidenceCorrelationId()!) : undefined
625
+ if (parsed.data.systemTraceId) bindSystemTrace(parsed.data.systemTraceId)
626
+ try {
627
+ return await withResolvedDevice(device, async () => {
558
628
  const started = performance.now()
559
629
  const results: any[] = []
560
630
  let stoppedAt: string | null = null
561
631
  let pendingLightStep: number | null = null
562
632
  for (let index = 0; index < steps.length; index++) {
563
633
  const step = steps[index]
634
+ if (platform === 'android' && debuggerSessions.paused(deviceId)) {
635
+ results.push({ id: step.id, type: step.type, status: 'failed', success: false, result: { success: false, failure_code: 'DEBUGGER_PAUSED', dispatch_started: false, delivery_status: 'not_delivered' } })
636
+ stoppedAt = step.id as string
637
+ break
638
+ }
564
639
  const requestedVerification = step.verificationMode ?? defaults?.verificationMode ?? 'full'
565
640
  const next = steps[index + 1]
566
641
  const lightCoveredByAssertion = step.type === 'tap' && responseMode === 'compact' && requestedVerification === 'light' && next?.type === 'assert' &&
@@ -595,10 +670,12 @@ async function handleRunJourney(args: ToolCallArgs) {
595
670
  } catch (error) {
596
671
  result = { success: false, failure_code: step.type === 'assert' ? 'ASSERTION_TIMEOUT' : 'STEP_FAILED', error: error instanceof Error ? error.message : String(error) }
597
672
  }
673
+ if (platform === 'android' && debuggerSessions.paused(deviceId)) { result.success = false; result.failure_code = 'DEBUGGER_PAUSED'; result.retryable = false }
598
674
  if (step.type === 'assert' && !result?.success && /TIMEOUT/.test(result?.failure_code ?? '')) result.failure_code = 'ASSERTION_TIMEOUT'
599
675
  const success = !!result?.success
600
676
  if (lightCoveredByAssertion) {
601
- result.verification = { mode: 'light', status: success ? 'unavailable' : 'failed' }
677
+ result.verification = { mode: 'light', status: success ? 'unavailable' :
678
+ (['mismatch', 'unavailable'].includes(result.condition?.status) ? 'not_requested' : 'failed') }
602
679
  if (success) {
603
680
  result.verification_diagnostic = { code: 'VERIFICATION_UNAVAILABLE', retryable: true }
604
681
  pendingLightStep = results.length
@@ -608,6 +685,7 @@ async function handleRunJourney(args: ToolCallArgs) {
608
685
  const pending = results[pendingLightStep].result
609
686
  if (typeof result?.observed?.matched_count === 'number') {
610
687
  pending.verification.status = 'observed'
688
+ if (pending.evidence) completeJourneyVerification(pending.evidence.action_id)
611
689
  delete pending.verification_diagnostic
612
690
  const delta = ToolsObserve.getLatestStateDelta(platform, deviceId)
613
691
  if (delta) { pending.state_delta = delta; pending.state_delta_available = true }
@@ -630,8 +708,10 @@ async function handleRunJourney(args: ToolCallArgs) {
630
708
  for (const step of results) for (const [key, value] of Object.entries(step.result?.timing ?? {})) {
631
709
  if (key !== 'total_ms' && typeof value === 'number') componentTiming[key] = (componentTiming[key] ?? 0) + value
632
710
  }
633
- return wrapResponse({ success: stoppedAt === null, stopped_at_step_id: stoppedAt, steps: results, ...(failureSnapshot ? { failure_snapshot: failureSnapshot } : {}), timing: responseMode === 'compact' ? { total_ms: performance.now() - started } : { ...componentTiming, total_ms: performance.now() - started } }, responseMode === 'debug')
711
+ refreshJourneyEvidence(results)
712
+ return wrapResponse({ ...(finishTrace ? { system_trace: await finishTrace() } : {}), success: stoppedAt === null, stopped_at_step_id: stoppedAt, steps: results, ...(failureSnapshot ? { failure_snapshot: failureSnapshot } : {}), timing: responseMode === 'compact' ? { total_ms: performance.now() - started } : { ...componentTiming, total_ms: performance.now() - started } }, responseMode === 'debug')
634
713
  })
714
+ } finally { await finishTrace?.() }
635
715
  }
636
716
 
637
717
  async function executeSwipe(args: ToolCallArgs) {
@@ -644,7 +724,7 @@ async function executeSwipe(args: ToolCallArgs) {
644
724
  const deviceId = getStringArg(args, 'deviceId')
645
725
  const { verificationMode, actionTimeoutMs } = interactionControls(args)
646
726
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
647
- ToolsNetwork.notifyActionStart()
727
+ notifyActionStart()
648
728
  const res = await measure('dispatch_ms', () => ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId, timeoutMs: actionTimeoutMs }))
649
729
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
650
730
  const result = buildActionExecutionResult({
@@ -668,7 +748,7 @@ async function executeScrollToElement(args: ToolCallArgs) {
668
748
  const deviceId = getStringArg(args, 'deviceId')
669
749
  const { verificationMode } = interactionControls(args)
670
750
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
671
- ToolsNetwork.notifyActionStart()
751
+ notifyActionStart()
672
752
  const res = await measure('dispatch_ms', () => ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId }))
673
753
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
674
754
  const result = buildActionExecutionResult({
@@ -697,7 +777,7 @@ async function executeTypeText(args: ToolCallArgs) {
697
777
  const deviceId = getStringArg(args, 'deviceId')
698
778
  const { verificationMode, actionTimeoutMs } = interactionControls(args)
699
779
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
700
- ToolsNetwork.notifyActionStart()
780
+ notifyActionStart()
701
781
  const res = await measure('dispatch_ms', () => ToolsInteract.typeTextHandler({ text, deviceId, timeoutMs: actionTimeoutMs }))
702
782
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
703
783
  const result = buildActionExecutionResult({
@@ -716,7 +796,7 @@ async function executePressBack(args: ToolCallArgs) {
716
796
  const deviceId = getStringArg(args, 'deviceId')
717
797
  const { verificationMode, actionTimeoutMs } = interactionControls(args)
718
798
  const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
719
- ToolsNetwork.notifyActionStart()
799
+ notifyActionStart()
720
800
  const res = await measure('dispatch_ms', () => ToolsInteract.pressBackHandler({ deviceId, timeoutMs: actionTimeoutMs }))
721
801
  const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
722
802
  const result = buildActionExecutionResult({
@@ -780,7 +860,43 @@ async function handleGetNetworkActivity(args: ToolCallArgs) {
780
860
  return wrapResponse(result)
781
861
  }
782
862
 
863
+ async function handleSystemTrace(name: string, args: ToolCallArgs) {
864
+ const schema = name === 'start_system_trace' ? traceStartSchema : name === 'get_system_trace_capabilities' ? traceTargetSchema : traceControlSchema
865
+ const parsed = schema.safeParse(args)
866
+ if (!parsed.success) throw new TraceError('INVALID_ARGUMENT')
867
+ if (name === 'get_system_trace_capabilities' || name === 'start_system_trace') {
868
+ const target = parsed.data as { platform: PlatformArg; deviceId?: string; durationMs?: number }
869
+ if (target.platform !== 'android') {
870
+ if (name === 'start_system_trace') throw new TraceError('UNSUPPORTED_PLATFORM')
871
+ return wrapResponse({ supported: false, backend: 'perfetto', device: null, prerequisites: {}, reasons: ['UNSUPPORTED_PLATFORM'], enabled_sources: [], unavailable_categories: [], limitations: ['Android only'] })
872
+ }
873
+ let device
874
+ try { device = await resolveTargetDevice(target) }
875
+ catch {
876
+ if (name === 'start_system_trace') throw new TraceError('TRACE_UNSUPPORTED')
877
+ return wrapResponse({ supported: false, backend: 'perfetto', device: null, prerequisites: { device: false }, reasons: ['DEVICE_UNAVAILABLE'], enabled_sources: [], unavailable_categories: [], limitations: ['An authorised Android device is required'] })
878
+ }
879
+ if (name === 'get_system_trace_capabilities') return wrapResponse(await systemTraces.capabilities(device.id))
880
+ const result = await systemTraces.start(device.id, target.durationMs)
881
+ return { ...wrapResponse(result), ...(result.state === 'failed' ? { isError: true } : {}) }
882
+ }
883
+ const operation = name === 'stop_system_trace' ? 'stop' : name === 'cancel_system_trace' ? 'cancel' : 'status'
884
+ const result = await systemTraces.control((parsed.data as { traceId: string }).traceId, operation)
885
+ return { ...wrapResponse(result), ...(['failed', 'cancelled'].includes(result.state) ? { isError: true } : {}) }
886
+ }
887
+
783
888
  export const toolHandlers: Record<string, ToolHandler> = {
889
+ ...Object.fromEntries(systemTraceTools.map(({ name }) => [name, (args: ToolCallArgs) => handleSystemTrace(name, args)])),
890
+ get_app_readiness: async args => {
891
+ const parsed = readinessSchema.safeParse(args)
892
+ if (!parsed.success) throw new Error('INVALID_ARGUMENT: invalid readiness arguments')
893
+ return wrapResponse(await getAppReadiness(parsed.data))
894
+ },
895
+ wait_for_app_ready: async args => {
896
+ const parsed = readinessWaitSchema.safeParse(args)
897
+ if (!parsed.success) throw new Error('INVALID_ARGUMENT: invalid readiness arguments')
898
+ return wrapResponse(await getAppReadiness(parsed.data, true))
899
+ },
784
900
  start_app: handleStartApp,
785
901
  terminate_app: handleTerminateApp,
786
902
  restart_app: handleRestartApp,
@@ -818,7 +934,12 @@ export const toolHandlers: Record<string, ToolHandler> = {
818
934
  get_network_activity: handleGetNetworkActivity
819
935
  }
820
936
 
821
- export async function handleToolCall(name: string, args: ToolCallArgs = {}) {
937
+ for (const name of ['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'read_log_stream', 'run_journey', 'adjust_control']) {
938
+ toolHandlers[name] = correlatedHandler(name, toolHandlers[name], name === 'adjust_control', name === 'run_journey')
939
+ }
940
+
941
+ export async function handleToolCall(name: string, args: ToolCallArgs = {}, signal?: AbortSignal) {
942
+ if (Object.hasOwn(debuggerContracts, name)) return handleDebugger(name, args, signal)
822
943
  const handler = toolHandlers[name]
823
944
  if (!handler) throw new Error(`Unknown tool: ${name}`)
824
945
 
@@ -838,8 +959,48 @@ export async function handleToolCall(name: string, args: ToolCallArgs = {}) {
838
959
  }
839
960
  return withTiming(measured, async () => {
840
961
  try {
841
- return addTiming(await handler(args))
962
+ if (!systemTraceToolNames.has(name)) validateExpectedState(name, args)
963
+ const guarded = ['start_app', 'terminate_app', 'restart_app', 'reset_app_data', 'install_app', 'build_and_install', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back', 'adjust_control', 'run_journey', 'wait_for_ui', 'wait_for_ui_change', 'wait_for_screen_change', 'expect_screen', 'expect_element_visible', 'expect_state'].includes(name)
964
+ if (guarded && args.platform !== 'ios' && debuggerSessions.hasActiveSessions()) {
965
+ const device = await resolveTargetDevice({ platform: 'android', deviceId: args.deviceId as string | undefined })
966
+ return withResolvedDevice(device, () => withDebuggerGuard('android', device.id, async () => {
967
+ const before = debuggerSessions.paused(device.id)
968
+ if (before) return wrapResponse({ success: false, failure_code: 'DEBUGGER_PAUSED', delivery_status: 'not_delivered', dispatch_started: false, debugger: before })
969
+ let response: ToolCallResult
970
+ try { response = await handler(args) } catch (error) {
971
+ if (!debuggerSessions.paused(device.id)) throw error
972
+ response = wrapResponse({ success: false })
973
+ }
974
+ const pause = debuggerSessions.paused(device.id)
975
+ if (pause) {
976
+ const item = response.content.find(item => item.type === 'text')
977
+ if (item?.text) {
978
+ const payload = JSON.parse(item.text)
979
+ Object.assign(payload, { success: false, failure_code: 'DEBUGGER_PAUSED', retryable: false,
980
+ dispatch_started: guardDispatched(), delivery_status: payload.success ? 'delivered' : guardDispatched() ? 'unknown' : 'not_delivered',
981
+ verification: { status: 'blocked', reason: 'DEBUGGER_PAUSED' }, debugger: pause })
982
+ item.text = JSON.stringify(payload)
983
+ }
984
+ }
985
+ return addTiming(response)
986
+ }))
987
+ }
988
+ const response = await handler(args)
989
+ if (['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'get_current_screen', 'read_log_stream'].includes(name) && args.platform !== 'ios') {
990
+ const item = response.content.find(item => item.type === 'text')
991
+ if (item?.text) {
992
+ try {
993
+ const payload = JSON.parse(item.text)
994
+ const device = payload.device ?? payload.evidence?.device
995
+ const id = device?.id ?? args.deviceId
996
+ const pause = id && device?.platform !== 'ios' ? debuggerSessions.paused(String(id)) : null
997
+ if (pause) item.text = JSON.stringify({ ...payload, debugger: pause })
998
+ } catch { /* Non-JSON observation */ }
999
+ }
1000
+ }
1001
+ return addTiming(response)
842
1002
  } catch (error) {
1003
+ if (error instanceof TraceError) return { ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true }
843
1004
  console.error(`Error executing tool ${name}:`, error)
844
1005
  return addTiming(wrapToolError(name, error))
845
1006
  }
@@ -16,7 +16,7 @@ export { wrapResponse, toolDefinitions, handleToolCall }
16
16
 
17
17
  export const serverInfo = {
18
18
  name: 'mobile-debug-mcp',
19
- version: '0.32.4'
19
+ version: '0.34.0'
20
20
  }
21
21
 
22
22
  export function createServer() {
@@ -46,9 +46,9 @@ export function createServer() {
46
46
  tools: toolDefinitions
47
47
  }))
48
48
 
49
- server.setRequestHandler(CallToolRequestSchema, async (request: SchemaOutput<typeof CallToolRequestSchema>) => {
49
+ server.setRequestHandler(CallToolRequestSchema, async (request: SchemaOutput<typeof CallToolRequestSchema>, extra) => {
50
50
  const { name, arguments: args } = request.params
51
- return handleToolCall(name, args as Record<string, unknown>)
51
+ return handleToolCall(name, args as Record<string, unknown>, extra.signal)
52
52
  })
53
53
 
54
54
  return server
package/src/server.ts 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'
@@ -13,6 +15,28 @@ if (process.env.MOBILE_DEBUG_MCP_STARTUP_HEALTHCHECK === '1') {
13
15
 
14
16
  const transport = new StdioServerTransport()
15
17
 
18
+ let shuttingDown = false
19
+ async function shutdown() {
20
+ if (shuttingDown) return
21
+ shuttingDown = true
22
+ const deadline = setTimeout(() => { console.error('System trace shutdown cleanup timed out; owned device captures remain duration-bounded.'); process.exit(1) }, 10000)
23
+ let exitCode = 0
24
+ try {
25
+ const [remaining, debuggers] = await Promise.all([systemTraces.shutdown(), debuggerSessions.shutdown()])
26
+ if (debuggers.length) { exitCode = 1; console.error('Debugger shutdown cleanup incomplete', debuggers.map(s => s.cleanup)) }
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
+ } catch { exitCode = 1; console.error('Server shutdown cleanup failed.') }
33
+ finally { clearTimeout(deadline); process.exit(exitCode) }
34
+ }
35
+ server.onclose = () => { void shutdown() }
36
+ process.stdin.on('end', () => { void shutdown() })
37
+ process.on('SIGINT', () => { void shutdown() })
38
+ process.on('SIGTERM', () => { void shutdown() })
39
+
16
40
  async function main() {
17
41
  await (server as any).connect(transport)
18
42
  }
@@ -33,6 +33,7 @@ interface ToolEntry {
33
33
  }
34
34
 
35
35
  interface CapabilityEntry {
36
+ probeRequired?: string
36
37
  availability: boolean
37
38
  selectedProvider: string | null
38
39
  blockingFailure: DiagnosticFailure | null
@@ -41,7 +42,7 @@ interface CapabilityEntry {
41
42
  interface PlatformEntry {
42
43
  status: PlatformStatus
43
44
  tools: Record<string, ToolEntry>
44
- providers: Record<string, { available: boolean; selected: boolean; tool: string | null }>
45
+ providers: Record<string, { available: boolean; selected: boolean; tool: string | null; probeRequired?: string }>
45
46
  capabilities: Record<string, CapabilityEntry>
46
47
  devices: Array<Record<string, unknown>>
47
48
  failures: DiagnosticFailure[]
@@ -289,7 +290,7 @@ function buildAndroidPlatform(android: Awaited<ReturnType<typeof checkAndroid>>,
289
290
  },
290
291
  providers: {
291
292
  adb: { available: adbAvailable, selected: true, tool: 'adb' },
292
- jdwp: { available: adbAvailable && android.devices > 0, selected: false, tool: 'adb' }
293
+ jdwp: { available: false, selected: false, tool: 'adb', probeRequired: 'get_debugger_capabilities' }
293
294
  },
294
295
  capabilities: {
295
296
  device_discovery: {
@@ -323,9 +324,10 @@ function buildAndroidPlatform(android: Awaited<ReturnType<typeof checkAndroid>>,
323
324
  blockingFailure: logAvailable ? null : adbFailure ?? envFailure
324
325
  },
325
326
  debug: {
326
- availability: adbAvailable && android.devices > 0,
327
- selectedProvider: adbAvailable && android.devices > 0 ? 'JDWP' : null,
328
- blockingFailure: adbAvailable && android.devices > 0 ? null : deviceFailure ?? adbFailure ?? envFailure
327
+ probeRequired: 'get_debugger_capabilities',
328
+ availability: false,
329
+ selectedProvider: null,
330
+ blockingFailure: deviceFailure ?? adbFailure ?? envFailure
329
331
  }
330
332
  },
331
333
  devices: deviceEntries,