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.
- package/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +14 -0
- package/.cursor-plugin/marketplace.json +16 -0
- package/.github/copilot-instructions.md +7 -0
- package/.github/workflows/ci.yml +21 -0
- package/AGENTS.md +21 -10
- package/README.md +69 -4
- package/agent-plugin/.claude-plugin/plugin.json +20 -0
- package/agent-plugin/.codex-plugin/plugin.json +34 -0
- package/agent-plugin/.cursor-plugin/plugin.json +23 -0
- package/agent-plugin/.mcp.json +14 -0
- package/agent-plugin/README.md +9 -3
- package/agent-plugin/assets/mobile-debug-tools.svg +28 -0
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -1
- package/dist/debugger/android.js +78 -0
- package/dist/debugger/guard.js +30 -0
- package/dist/debugger/sessions.js +262 -0
- package/dist/debugger/worker.js +165 -0
- package/dist/interact/android.js +10 -4
- package/dist/interact/conditional-action.js +68 -0
- package/dist/interact/index.js +70 -14
- package/dist/interact/ios.js +14 -8
- package/dist/manage/android.js +4 -3
- package/dist/manage/ios.js +3 -2
- package/dist/observe/android.js +25 -4
- package/dist/observe/evidence.js +280 -0
- package/dist/observe/index.js +33 -6
- package/dist/observe/ios.js +25 -6
- package/dist/observe/readiness-service.js +124 -0
- package/dist/observe/readiness.js +88 -0
- package/dist/observe/recording.js +165 -0
- package/dist/observe/snapshot-metadata.js +10 -2
- package/dist/observe/system-trace-android.js +135 -0
- package/dist/observe/system-trace.js +285 -0
- package/dist/server/common.js +9 -0
- package/dist/server/condition-schema.js +32 -0
- package/dist/server/debugger-handlers.js +38 -0
- package/dist/server/debugger-schema.js +42 -0
- package/dist/server/journey-schema.js +4 -2
- package/dist/server/readiness-schema.js +12 -0
- package/dist/server/system-trace-schema.js +17 -0
- package/dist/server/tool-definitions.js +51 -0
- package/dist/server/tool-handlers.js +294 -105
- package/dist/server-core.js +3 -3
- package/dist/server.js +34 -0
- package/dist/system/index.js +5 -4
- package/dist/utils/exec.js +2 -0
- package/dist/utils/ios/utils.js +4 -4
- package/dist/utils/java.js +14 -12
- package/dist/utils/operation-budget.js +26 -2
- package/dist/utils/resolve-device.js +9 -0
- package/docs/CHANGELOG.md +15 -0
- package/docs/specs/020-conditional-actions/intent.md +62 -0
- package/docs/specs/020-conditional-actions/plan.md +214 -0
- package/docs/specs/020-conditional-actions/spec.md +301 -0
- package/docs/specs/021-app-readiness-signals/intent.md +40 -0
- package/docs/specs/021-app-readiness-signals/plan.md +102 -0
- package/docs/specs/021-app-readiness-signals/spec.md +144 -0
- package/docs/specs/022-correlated-evidence/device-evidence.json +60 -0
- package/docs/specs/022-correlated-evidence/intent.md +37 -0
- package/docs/specs/022-correlated-evidence/modul8-evidence.json +1062 -0
- package/docs/specs/022-correlated-evidence/modul8-selector-fix-evidence.json +336 -0
- package/docs/specs/022-correlated-evidence/plan.md +171 -0
- package/docs/specs/022-correlated-evidence/spec.md +123 -0
- package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
- package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
- package/docs/specs/023-android-system-traces/intent.md +40 -0
- package/docs/specs/023-android-system-traces/plan.md +117 -0
- package/docs/specs/023-android-system-traces/spec.md +120 -0
- package/docs/specs/024-android-heap-dump-capture/intent.md +39 -0
- package/docs/specs/025-android-runtime-debugger/intent.md +40 -0
- package/docs/specs/025-android-runtime-debugger/plan.md +104 -0
- package/docs/specs/025-android-runtime-debugger/spec.md +182 -0
- package/docs/specs/process.md +213 -0
- package/docs/tools/TOOLS.md +5 -0
- package/docs/tools/debugger.md +104 -0
- package/docs/tools/evidence.md +82 -0
- package/docs/tools/interact.md +125 -2
- package/docs/tools/observe.md +70 -0
- package/docs/tools/system-traces.md +126 -0
- package/package.json +1 -1
- package/scripts/agent-plugin.mjs +68 -8
- package/src/debugger/android.ts +65 -0
- package/src/debugger/guard.ts +26 -0
- package/src/debugger/sessions.ts +168 -0
- package/src/debugger/worker.ts +122 -0
- package/src/interact/android.ts +10 -4
- package/src/interact/conditional-action.ts +75 -0
- package/src/interact/index.ts +57 -28
- package/src/interact/ios.ts +14 -8
- package/src/manage/android.ts +4 -3
- package/src/manage/ios.ts +3 -2
- package/src/observe/android.ts +33 -6
- package/src/observe/evidence.ts +262 -0
- package/src/observe/index.ts +32 -8
- package/src/observe/ios.ts +32 -8
- package/src/observe/readiness-service.ts +94 -0
- package/src/observe/readiness.ts +84 -0
- package/src/observe/snapshot-metadata.ts +12 -1
- package/src/observe/system-trace-android.ts +133 -0
- package/src/observe/system-trace.ts +218 -0
- package/src/server/common.ts +7 -0
- package/src/server/condition-schema.ts +32 -0
- package/src/server/debugger-handlers.ts +29 -0
- package/src/server/debugger-schema.ts +37 -0
- package/src/server/journey-schema.ts +4 -2
- package/src/server/readiness-schema.ts +12 -0
- package/src/server/system-trace-schema.ts +17 -0
- package/src/server/tool-definitions.ts +49 -0
- package/src/server/tool-handlers.ts +182 -21
- package/src/server-core.ts +3 -3
- package/src/server.ts +24 -0
- package/src/system/index.ts +7 -5
- package/src/types.ts +30 -1
- package/src/utils/exec.ts +2 -0
- package/src/utils/ios/utils.ts +4 -4
- package/src/utils/java.ts +13 -12
- package/src/utils/operation-budget.ts +24 -2
- package/src/utils/resolve-device.ts +10 -0
- package/test/device/automated/debugger/runtime.android.test.ts +203 -0
- package/test/device/automated/observe/correlated-evidence.smoke.ts +70 -0
- package/test/device/automated/observe/system-trace.smoke.ts +104 -0
- package/test/device/manual/interact/conditional_actions.manual.ts +95 -0
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +63 -0
- package/test/device/manual/interact/readiness.manual.ts +99 -0
- package/test/fixtures/debugger-app/MainActivity.java +46 -0
- package/test/fixtures/debugger-app/NoLocals.java +7 -0
- package/test/fixtures/debugger-app/README.md +13 -0
- package/test/fixtures/debugger-app/build.ts +29 -0
- package/test/fixtures/readiness-app/MainActivity.java +23 -0
- package/test/fixtures/readiness-app/README.md +12 -0
- package/test/fixtures/readiness-app/build.ts +37 -0
- package/test/fixtures/readiness-app/main.m +27 -0
- package/test/helpers/perfetto.ts +59 -0
- package/test/unit/debugger/guards.test.ts +46 -0
- package/test/unit/debugger/native.test.ts +33 -0
- package/test/unit/debugger/sessions.test.ts +83 -0
- package/test/unit/observe/android_tree_acquisition.test.ts +2 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +42 -32
- package/test/unit/observe/compact_tree_error.test.ts +24 -0
- package/test/unit/observe/conditional_snapshots.test.ts +33 -0
- package/test/unit/observe/correlated-evidence.test.ts +128 -0
- package/test/unit/observe/ios_tree_acquisition.test.ts +2 -0
- package/test/unit/observe/readiness.test.ts +41 -0
- package/test/unit/observe/readiness_native.test.ts +68 -0
- package/test/unit/observe/system-trace-android.test.ts +52 -0
- package/test/unit/observe/system-trace.test.ts +166 -0
- package/test/unit/server/conditional_actions.test.ts +299 -0
- package/test/unit/server/conditional_schema.test.ts +59 -0
- package/test/unit/server/correlated-evidence.test.ts +194 -0
- package/test/unit/server/readiness.test.ts +170 -0
- package/test/unit/server/selector_tap_target.test.ts +79 -0
- package/test/unit/server/system-trace.test.ts +64 -0
- package/test/unit/server/tap_selector.test.ts +2 -0
- package/worker/DebuggerWorker.java +160 -0
- package/skills/mobile-debug-tools-usage/SKILL.md +0 -96
- /package/{skills → .agents/skills}/README.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/SKILL.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/references/build-flags.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/references/diagnostics-schema.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/references/toolchain-details.md +0 -0
- /package/{skills → .agents/skills}/spec-review/SKILL.md +0 -0
- /package/{skills → .agents/skills}/spec-review/references/spec-review-checklist.md +0 -0
- /package/{skills → .agents/skills}/spec-review/references/spec-review-template.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/SKILL.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/references/repo-test-layout.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/references/test-authoring-workflow.md +0 -0
- /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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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' :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/server-core.ts
CHANGED
|
@@ -16,7 +16,7 @@ export { wrapResponse, toolDefinitions, handleToolCall }
|
|
|
16
16
|
|
|
17
17
|
export const serverInfo = {
|
|
18
18
|
name: 'mobile-debug-mcp',
|
|
19
|
-
version: '0.
|
|
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
|
}
|
package/src/system/index.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
probeRequired: 'get_debugger_capabilities',
|
|
328
|
+
availability: false,
|
|
329
|
+
selectedProvider: null,
|
|
330
|
+
blockingFailure: deviceFailure ?? adbFailure ?? envFailure
|
|
329
331
|
}
|
|
330
332
|
},
|
|
331
333
|
devices: deviceEntries,
|