mobile-debug-mcp 0.35.0 → 0.37.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/.cursor-plugin/marketplace.json +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/README.md +96 -24
- package/agent-plugin/.antigravity-plugin/README.md +48 -0
- package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
- package/agent-plugin/.claude-plugin/plugin.json +2 -2
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/README.md +37 -8
- package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
- package/agent-plugin/cursor/LICENSE +201 -0
- package/agent-plugin/cursor/README.md +9 -0
- package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +261 -0
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/mcp_config.json +13 -0
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +46 -18
- package/dist/contracts/device-readiness.js +1 -0
- package/dist/debugger/guard.js +4 -4
- package/dist/debugger/ios-worker.js +62 -0
- package/dist/debugger/ios.js +57 -0
- package/dist/debugger/sessions.js +33 -16
- package/dist/debugger/worker.js +6 -4
- package/dist/interact/index.js +12 -2
- package/dist/interact/waits/ui-change-wait.js +9 -2
- package/dist/interact/waits/ui-wait.js +7 -3
- package/dist/manage/index.js +3 -0
- package/dist/manage/ios.js +2 -0
- package/dist/observe/device-readiness.js +116 -0
- package/dist/observe/evidence.js +31 -0
- package/dist/observe/ios-trace/artifact.js +63 -0
- package/dist/observe/ios-trace/capabilities.js +86 -0
- package/dist/observe/ios-trace/native.js +93 -0
- package/dist/observe/ios-trace/notification.js +35 -0
- package/dist/observe/ios-trace/process.js +98 -0
- package/dist/observe/ios-trace/sessions.js +246 -0
- package/dist/observe/readiness-service.js +22 -10
- package/dist/observe/readiness.js +34 -4
- package/dist/observe/screenshot-encoding.js +4 -0
- package/dist/observe/snapshot-collection.js +7 -2
- package/dist/observe/system-trace-error.js +7 -0
- package/dist/observe/system-trace-router.js +81 -0
- package/dist/observe/system-trace.js +4 -8
- package/dist/server/definitions/manage.js +3 -2
- package/dist/server/handlers/action-execution.js +23 -14
- package/dist/server/handlers/compact-interaction.js +106 -0
- package/dist/server/handlers/compact-state.js +47 -0
- package/dist/server/handlers/debugger-handlers.js +27 -8
- package/dist/server/handlers/diagnostic.js +12 -4
- package/dist/server/handlers/input.js +2 -0
- package/dist/server/handlers/interact.js +2 -0
- package/dist/server/handlers/interaction-diagnostics.js +62 -0
- package/dist/server/handlers/journey.js +18 -20
- package/dist/server/handlers/observe.js +2 -1
- package/dist/server/handlers/tool-handlers.js +28 -14
- package/dist/server/schemas/debugger-schema.js +10 -7
- package/dist/server/schemas/interaction-diagnostics.js +9 -0
- package/dist/server/schemas/system-trace-schema.js +14 -9
- package/dist/server/tool-definitions.js +9 -4
- package/dist/server-core.js +5 -2
- package/dist/server.js +5 -5
- package/dist/system/android.js +19 -15
- package/dist/system/index.js +2 -1
- package/dist/utils/android/utils.js +7 -16
- package/dist/utils/app-installation.js +77 -0
- package/dist/utils/benchmark-metrics.js +43 -0
- package/dist/utils/exec.js +4 -0
- package/dist/utils/ios/utils.js +7 -11
- package/dist/utils/resolve-device.js +3 -2
- package/dist/utils/timing.js +3 -3
- package/docs/CHANGELOG.md +48 -1
- package/docs/agent-plugin-installation-verification.md +39 -0
- package/docs/benchmarks/README.md +1 -0
- package/docs/benchmarks/end-to-end-latency/README.md +98 -0
- package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.json +6532 -0
- package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.md +30 -0
- package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.json +15901 -0
- package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.md +14 -0
- package/docs/benchmarks/end-to-end-latency/baseline.json +233843 -0
- package/docs/benchmarks/end-to-end-latency/baseline.md +21 -0
- package/docs/benchmarks/end-to-end-latency/investigation.md +128 -0
- package/docs/benchmarks/end-to-end-latency/ios-smoke.json +84 -0
- package/docs/benchmarks/end-to-end-latency/ios-smoke.md +9 -0
- package/docs/benchmarks/end-to-end-latency/smoke.json +19601 -0
- package/docs/benchmarks/end-to-end-latency/smoke.md +21 -0
- package/docs/code-structure.md +3 -1
- package/docs/specs/031-ios-system-traces/intent.md +38 -0
- package/docs/specs/031-ios-system-traces/investigation.md +92 -0
- package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
- package/docs/specs/031-ios-system-traces/plan.md +166 -0
- package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
- package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
- package/docs/specs/031-ios-system-traces/spec.md +135 -0
- package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
- package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
- package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
- package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
- package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
- package/docs/specs/033-device-readiness-blockers/android-evidence.json +437 -0
- package/docs/specs/033-device-readiness-blockers/android-physical-evidence.json +28 -0
- package/docs/specs/033-device-readiness-blockers/android-physical-power-evidence.json +29 -0
- package/docs/specs/033-device-readiness-blockers/intent.md +41 -0
- package/docs/specs/033-device-readiness-blockers/ios-investigation.md +55 -0
- package/docs/specs/033-device-readiness-blockers/ios-probe-evidence.json +24 -0
- package/docs/specs/033-device-readiness-blockers/ios-tool-evidence.json +73 -0
- package/docs/specs/033-device-readiness-blockers/plan.md +249 -0
- package/docs/specs/033-device-readiness-blockers/spec.md +134 -0
- package/docs/specs/034-app-installation-status/intent.md +41 -0
- package/docs/specs/034-app-installation-status/native-evidence.json +654 -0
- package/docs/specs/034-app-installation-status/plan.md +73 -0
- package/docs/specs/034-app-installation-status/spec.md +114 -0
- package/docs/specs/035-compact-interaction-results/baseline.md +45 -0
- package/docs/specs/035-compact-interaction-results/intent.md +38 -0
- package/docs/specs/035-compact-interaction-results/plan.md +43 -0
- package/docs/specs/035-compact-interaction-results/spec.md +118 -0
- package/docs/specs/035-compact-interaction-results/verification.md +111 -0
- package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-amendment.md +60 -0
- package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-plan.md +32 -0
- package/docs/specs/036-end-to-end-interaction-latency/intent.md +37 -0
- package/docs/specs/036-end-to-end-interaction-latency/plan.md +59 -0
- package/docs/specs/036-end-to-end-interaction-latency/spec.md +126 -0
- package/docs/specs/036-end-to-end-interaction-latency/verification.md +87 -0
- package/docs/tools/TOOLS.md +3 -2
- package/docs/tools/compact-interactions.md +56 -0
- package/docs/tools/correlated-evidence.md +6 -0
- package/docs/tools/debugger.md +45 -5
- package/docs/tools/interact.md +17 -8
- package/docs/tools/manage.md +10 -2
- package/docs/tools/observe.md +42 -2
- package/docs/tools/system-traces.md +81 -14
- package/docs/tools/system.md +6 -0
- package/package.json +2 -1
- package/scripts/agent-plugin.mjs +55 -5
- package/src/contracts/device-readiness.ts +8 -0
- package/src/debugger/android.ts +1 -1
- package/src/debugger/guard.ts +5 -5
- package/src/debugger/ios-worker.ts +49 -0
- package/src/debugger/ios.ts +45 -0
- package/src/debugger/lldb/breakpoints.py +51 -0
- package/src/debugger/lldb/inspection.py +109 -0
- package/src/debugger/lldb/worker.py +203 -0
- package/src/debugger/sessions.ts +24 -12
- package/src/debugger/worker.ts +7 -4
- package/src/interact/index.ts +13 -2
- package/src/interact/waits/ui-change-wait.ts +7 -2
- package/src/interact/waits/ui-wait.ts +6 -3
- package/src/manage/index.ts +2 -0
- package/src/manage/ios.ts +2 -0
- package/src/observe/device-readiness.ts +96 -0
- package/src/observe/evidence.ts +30 -1
- package/src/observe/ios-trace/artifact.ts +55 -0
- package/src/observe/ios-trace/capabilities.ts +74 -0
- package/src/observe/ios-trace/native.ts +72 -0
- package/src/observe/ios-trace/notification.ts +30 -0
- package/src/observe/ios-trace/process.ts +82 -0
- package/src/observe/ios-trace/sessions.ts +206 -0
- package/src/observe/readiness-service.ts +22 -10
- package/src/observe/readiness.ts +37 -4
- package/src/observe/screenshot-encoding.ts +4 -0
- package/src/observe/snapshot-collection.ts +7 -2
- package/src/observe/system-trace-error.ts +3 -0
- package/src/observe/system-trace-router.ts +61 -0
- package/src/observe/system-trace.ts +4 -4
- package/src/server/common.ts +2 -2
- package/src/server/definitions/manage.ts +3 -2
- package/src/server/handlers/action-execution.ts +22 -14
- package/src/server/handlers/compact-interaction.ts +91 -0
- package/src/server/handlers/compact-state.ts +43 -0
- package/src/server/handlers/debugger-handlers.ts +28 -9
- package/src/server/handlers/diagnostic.ts +11 -4
- package/src/server/handlers/input.ts +2 -0
- package/src/server/handlers/interact.ts +2 -0
- package/src/server/handlers/interaction-diagnostics.ts +50 -0
- package/src/server/handlers/journey.ts +15 -17
- package/src/server/handlers/observe.ts +2 -1
- package/src/server/handlers/payload.ts +2 -1
- package/src/server/handlers/tool-handlers.ts +29 -14
- package/src/server/schemas/debugger-schema.ts +10 -7
- package/src/server/schemas/interaction-diagnostics.ts +10 -0
- package/src/server/schemas/system-trace-schema.ts +15 -9
- package/src/server/tool-definitions.ts +8 -4
- package/src/server-core.ts +5 -2
- package/src/server.ts +4 -4
- package/src/system/android.ts +18 -11
- package/src/system/index.ts +2 -1
- package/src/types.ts +2 -0
- package/src/utils/android/utils.ts +8 -10
- package/src/utils/app-installation.ts +73 -0
- package/src/utils/benchmark-metrics.ts +42 -0
- package/src/utils/exec.ts +5 -0
- package/src/utils/ios/utils.ts +7 -10
- package/src/utils/resolve-device.ts +4 -2
- package/src/utils/timing.ts +3 -2
- package/test/device/automated/debugger/runtime.android.test.ts +5 -0
- package/test/device/automated/debugger/runtime.ios.test.ts +227 -0
- package/test/device/automated/interact/compact-results.smoke.ts +58 -0
- package/test/device/automated/latency/end-to-end-environment.ts +61 -0
- package/test/device/automated/latency/end-to-end-report.ts +90 -0
- package/test/device/automated/latency/end-to-end-session.ts +98 -0
- package/test/device/automated/latency/end-to-end.ts +149 -0
- package/test/device/automated/latency/owned-transport.ts +81 -0
- package/test/device/automated/latency/snapshot-pair.ts +143 -0
- package/test/device/automated/latency-ios/benchmark.ts +126 -0
- package/test/device/automated/observe/correlated-evidence.smoke.ts +7 -5
- package/test/device/automated/observe/device-readiness-cleanup.ts +14 -0
- package/test/device/automated/observe/device-readiness.android.smoke.ts +78 -0
- package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
- package/test/device/automated/observe/system-trace.smoke.ts +2 -2
- package/test/fixtures/contracts/tool-definitions.json +243 -47
- package/test/fixtures/ios-debugger-app/Main.swift +95 -0
- package/test/fixtures/ios-debugger-app/README.md +27 -0
- package/test/fixtures/ios-debugger-app/build.ts +21 -0
- package/test/fixtures/latency-journey-app/build.ts +3 -3
- package/test/helpers/ios-trace-shutdown.ts +29 -0
- package/test/unit/debugger/handle-guards.test.ts +89 -0
- package/test/unit/debugger/ios-preparation.test.ts +57 -0
- package/test/unit/debugger/ios.test.ts +104 -0
- package/test/unit/debugger/lldb-worker.py +166 -0
- package/test/unit/debugger/lldb-worker.test.ts +10 -0
- package/test/unit/interact/ui-change-readiness-interruption.test.ts +37 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +66 -12
- package/test/unit/observe/device-readiness-cleanup.test.ts +24 -0
- package/test/unit/observe/device-readiness.test.ts +97 -0
- package/test/unit/observe/ios-trace-native.test.ts +90 -0
- package/test/unit/observe/ios-trace-sessions.test.ts +154 -0
- package/test/unit/observe/readiness-retained-blockers.test.ts +40 -0
- package/test/unit/observe/readiness.test.ts +1 -1
- package/test/unit/observe/system-trace-router.test.ts +49 -0
- package/test/unit/quality/agent-plugin-export.test.ts +38 -0
- package/test/unit/server/compact-interaction.test.ts +87 -0
- package/test/unit/server/compact-journey-evidence.test.ts +40 -0
- package/test/unit/server/compact-review-regressions.test.ts +78 -0
- package/test/unit/server/conditional_actions.test.ts +7 -3
- package/test/unit/server/correlated-evidence.test.ts +18 -6
- package/test/unit/server/end-to-end-report.test.ts +46 -0
- package/test/unit/server/input-device-readiness.test.ts +55 -0
- package/test/unit/server/interaction-diagnostics.test.ts +45 -0
- package/test/unit/server/owned-benchmark-transport.test.ts +32 -0
- package/test/unit/server/readiness-retained-deadline.test.ts +32 -0
- package/test/unit/server/readiness.test.ts +24 -1
- package/test/unit/server/run_journey.test.ts +2 -1
- package/test/unit/server/selector_tap_target.test.ts +2 -1
- package/test/unit/server/system-trace.test.ts +18 -7
- package/test/unit/utils/app-installation.test.ts +78 -0
- package/test/unit/utils/benchmark-metrics.test.ts +36 -0
- package/test/unit/utils/resolve_device.test.ts +11 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { observeDeviceReadiness } from '../../observe/device-readiness.js';
|
|
2
|
+
import { benchmarkStage } from '../../utils/benchmark-metrics.js';
|
|
1
3
|
import { debuggerSessions } from '../../debugger/sessions.js';
|
|
2
4
|
import { systemTraces } from '../../observe/system-trace.js';
|
|
3
|
-
import { currentEvidenceCorrelationId, bindSystemTrace, refreshJourneyEvidence, completeJourneyVerification } from '../../observe/evidence.js';
|
|
5
|
+
import { currentEvidenceCorrelationId, bindSystemTrace, withJourneyStepEvidence, refreshJourneyEvidence, completeJourneyVerification, JourneyStepError } from '../../observe/evidence.js';
|
|
4
6
|
import { runWithBudget } from '../../utils/operation-budget.js';
|
|
5
7
|
import { journeySchema } from '../schemas/journey-schema.js';
|
|
6
8
|
import { resolveTargetDevice, withResolvedDevice } from '../../utils/resolve-device.js';
|
|
@@ -25,23 +27,27 @@ async function assertStep(step, platform, deviceId) {
|
|
|
25
27
|
return ToolsInteract.expectStateHandler({ selector: assertion.selector, element_id: assertion.elementId, property: assertion.property, expected: assertion.expected, platform, deviceId, stabilization_window_ms: timeoutMs, poll_interval_ms: pollIntervalMs, exact_budget: true });
|
|
26
28
|
return { success: false, failure_code: 'INVALID_ASSERTION' };
|
|
27
29
|
}
|
|
30
|
+
async function observeStep(s, operation) {
|
|
31
|
+
return s.input.responseMode === 'compact' ? withJourneyStepEvidence(operation) : operation();
|
|
32
|
+
}
|
|
28
33
|
async function executeStep(s, step, covered) {
|
|
29
34
|
const { platform, responseMode, defaults } = s.input, deviceId = s.deviceId;
|
|
30
35
|
try {
|
|
31
36
|
if (step.type === 'tap')
|
|
32
37
|
return payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, _stepId: step.id, verificationMode: covered ? 'none' : step.verificationMode ?? defaults?.verificationMode ?? 'full' }));
|
|
33
|
-
if (step.type === 'wait')
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
if (step.type === 'wait')
|
|
39
|
+
return await observeStep(s, async () => {
|
|
40
|
+
const result = (await withObservationContext({ stepId: step.id, purpose: 'target' }, () => ToolsInteract.waitForUIHandler({ selector: step.selector, condition: step.condition ?? 'exists', timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId })));
|
|
41
|
+
return { success: result.status === 'success', ...result };
|
|
42
|
+
});
|
|
37
43
|
if (step.type === 'assert')
|
|
38
|
-
return await withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget(step.timeoutMs ?? 5000, () => assertStep(step, platform, deviceId)));
|
|
44
|
+
return await observeStep(s, () => withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget(step.timeoutMs ?? 5000, () => assertStep(step, platform, deviceId))));
|
|
39
45
|
if (step.type === 'start_app')
|
|
40
46
|
return payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, _stepId: step.id, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }));
|
|
41
47
|
return { success: false, failure_code: 'UNSUPPORTED_JOURNEY_STEP' };
|
|
42
48
|
}
|
|
43
49
|
catch (error) {
|
|
44
|
-
return { success: false, failure_code: step.type === 'assert' ? 'ASSERTION_TIMEOUT' : 'STEP_FAILED', error: error instanceof Error ? error.message : String(error) };
|
|
50
|
+
return { success: false, failure_code: step.type === 'assert' ? 'ASSERTION_TIMEOUT' : 'STEP_FAILED', error: error instanceof Error ? error.message : String(error), ...(error instanceof JourneyStepError ? { evidence: error.evidence } : {}) };
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
function lightVerification(s, step, result, covered, success) {
|
|
@@ -69,24 +75,16 @@ function lightVerification(s, step, result, covered, success) {
|
|
|
69
75
|
}
|
|
70
76
|
s.pendingLightStep = null;
|
|
71
77
|
}
|
|
72
|
-
function compactAssertion(result, success) {
|
|
73
|
-
const errorCode = typeof result.error === 'object' ? result.error?.code : undefined;
|
|
74
|
-
return { success, ...(result.failure_code ? { failure_code: result.failure_code } : {}),
|
|
75
|
-
...(errorCode ? { failure_code: errorCode } : {}),
|
|
76
|
-
...(result.observed?.matched_count !== undefined ? { matched_count: result.observed.matched_count } : {}),
|
|
77
|
-
...(result.observed_fingerprint !== undefined ? { matched: success } : {}),
|
|
78
|
-
...(result.actual_state ? { observed: result.actual_state } : {}) };
|
|
79
|
-
}
|
|
80
78
|
function coveredByAssertion(s, step, next) {
|
|
81
79
|
const verification = ('verificationMode' in step ? step.verificationMode : undefined) ?? s.input.defaults?.verificationMode ?? 'full';
|
|
82
80
|
return step.type === 'tap' && s.input.responseMode === 'compact' && verification === 'light' && next?.type === 'assert' && next.assertion.kind === 'element_visible';
|
|
83
81
|
}
|
|
84
82
|
async function runSteps(s) {
|
|
85
|
-
const { platform,
|
|
83
|
+
const { platform, steps } = s.input;
|
|
86
84
|
for (let index = 0; index < steps.length; index++) {
|
|
87
85
|
const step = steps[index];
|
|
88
86
|
if (platform === 'android' && debuggerSessions.paused(s.deviceId)) {
|
|
89
|
-
s.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' } });
|
|
87
|
+
s.results.push({ id: step.id, type: step.type, status: 'failed', success: false, result: { device_readiness: await observeDeviceReadiness({ platform, id: s.deviceId }), success: false, failure_code: 'DEBUGGER_PAUSED', dispatch_started: false, delivery_status: 'not_delivered' } });
|
|
90
88
|
s.stoppedAt = step.id;
|
|
91
89
|
break;
|
|
92
90
|
}
|
|
@@ -95,7 +93,9 @@ async function runSteps(s) {
|
|
|
95
93
|
s.results.push({ id: step.id, type: step.type, status: 'not_run', success: false, reason: 'PREVIOUS_STEP_FAILED', result: null, timing: { total_ms: 0 } });
|
|
96
94
|
continue;
|
|
97
95
|
}
|
|
98
|
-
|
|
96
|
+
const result = await (step.type === 'assert' || step.type === 'wait'
|
|
97
|
+
? benchmarkStage(step.type === 'assert' ? 'assertion_ms' : 'wait_ms', () => executeStep(s, step, covered))
|
|
98
|
+
: executeStep(s, step, covered));
|
|
99
99
|
if (platform === 'android' && debuggerSessions.paused(s.deviceId)) {
|
|
100
100
|
result.success = false;
|
|
101
101
|
result.failure_code = 'DEBUGGER_PAUSED';
|
|
@@ -105,8 +105,6 @@ async function runSteps(s) {
|
|
|
105
105
|
result.failure_code = 'ASSERTION_TIMEOUT';
|
|
106
106
|
const success = !!result?.success;
|
|
107
107
|
lightVerification(s, step, result, covered, success);
|
|
108
|
-
if (responseMode === 'compact' && ['wait', 'assert'].includes(step.type))
|
|
109
|
-
result = compactAssertion(result, success);
|
|
110
108
|
s.results.push({ id: step.id, type: step.type, status: success ? 'passed' : 'failed', success, result, timing: { total_ms: performance.now() - started } });
|
|
111
109
|
if (!success)
|
|
112
110
|
s.stoppedAt = step.id;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ToolsObserve } from '../../observe/index.js';
|
|
2
|
+
import { benchmarkStage } from '../../utils/benchmark-metrics.js';
|
|
2
3
|
import { withObservationContext } from '../../observe/performance-metrics.js';
|
|
3
4
|
import { getBooleanArg, getNumberArg, getStringArg, requireStringArg, wrapResponse } from '../common.js';
|
|
4
5
|
export async function handleGetLogs(args) {
|
|
@@ -25,7 +26,7 @@ export async function handleCaptureScreenshot(args) {
|
|
|
25
26
|
const platform = requireStringArg(args, 'platform');
|
|
26
27
|
const deviceId = getStringArg(args, 'deviceId');
|
|
27
28
|
const started = performance.now();
|
|
28
|
-
const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId });
|
|
29
|
+
const res = await benchmarkStage('screenshot_capture_ms', () => ToolsObserve.captureScreenshotHandler({ platform, deviceId }));
|
|
29
30
|
const screenshotMs = performance.now() - started;
|
|
30
31
|
const mime = res.screenshot_mime || 'image/png';
|
|
31
32
|
const content = [
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { finalizeCompactInteraction } from './compact-interaction.js';
|
|
2
|
+
import { handleInteractionDiagnostics } from './interaction-diagnostics.js';
|
|
3
|
+
import { observeDeviceReadiness } from '../../observe/device-readiness.js';
|
|
4
|
+
import { ToolsInteract } from '../../interact/index.js';
|
|
1
5
|
import { handleDebugger } from './debugger-handlers.js';
|
|
2
6
|
import { debuggerContracts } from '../schemas/debugger-schema.js';
|
|
3
|
-
import { debuggerSessions } from '../../debugger/sessions.js';
|
|
7
|
+
import { debuggerSessions, iosDebuggerSessions } from '../../debugger/sessions.js';
|
|
4
8
|
import { withDebuggerGuard, guardDispatched } from '../../debugger/guard.js';
|
|
5
9
|
import { TraceError } from '../../observe/system-trace.js';
|
|
6
10
|
import { systemTraceTools } from '../schemas/system-trace-schema.js';
|
|
@@ -32,6 +36,7 @@ export const toolHandlers = {
|
|
|
32
36
|
throw new Error('INVALID_ARGUMENT: invalid readiness arguments');
|
|
33
37
|
return wrapResponse(await getAppReadiness(parsed.data, true));
|
|
34
38
|
},
|
|
39
|
+
get_interaction_diagnostics: handleInteractionDiagnostics,
|
|
35
40
|
start_app: handleStartApp,
|
|
36
41
|
terminate_app: handleTerminateApp,
|
|
37
42
|
restart_app: handleRestartApp,
|
|
@@ -71,28 +76,36 @@ export const toolHandlers = {
|
|
|
71
76
|
for (const name of ['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'read_log_stream', 'run_journey', 'adjust_control']) {
|
|
72
77
|
toolHandlers[name] = correlatedHandler(name, toolHandlers[name], name === 'adjust_control', name === 'run_journey');
|
|
73
78
|
}
|
|
79
|
+
function resolveGuardDevice(name, args) {
|
|
80
|
+
const platform = args.platform;
|
|
81
|
+
const deviceId = args.deviceId;
|
|
82
|
+
const stored = name === 'tap_element' && typeof args.elementId === 'string'
|
|
83
|
+
? ToolsInteract.getResolvedElementTarget(args.elementId, platform, deviceId) : null;
|
|
84
|
+
return resolveTargetDevice(stored ?? { platform: platform === 'ios' ? 'ios' : 'android', deviceId });
|
|
85
|
+
}
|
|
74
86
|
async function dispatchGuarded(device, args, handler, addTiming) {
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
const sessions = device.platform === 'ios' ? iosDebuggerSessions : debuggerSessions;
|
|
88
|
+
return withResolvedDevice(device, () => withDebuggerGuard(device.platform, device.id, async () => {
|
|
89
|
+
const before = sessions.paused(device.id);
|
|
77
90
|
if (before)
|
|
78
|
-
return wrapResponse({ success: false, failure_code: 'DEBUGGER_PAUSED', delivery_status: 'not_delivered', dispatch_started: false, debugger: before });
|
|
91
|
+
return addTiming(wrapResponse({ device_readiness: await observeDeviceReadiness(device), success: false, failure_code: 'DEBUGGER_PAUSED', delivery_status: 'not_delivered', dispatch_started: false, debugger: before }));
|
|
79
92
|
let response;
|
|
80
93
|
try {
|
|
81
94
|
response = await handler(args);
|
|
82
95
|
}
|
|
83
96
|
catch (error) {
|
|
84
|
-
if (!
|
|
97
|
+
if (!sessions.paused(device.id))
|
|
85
98
|
throw error;
|
|
86
99
|
response = wrapResponse({ success: false });
|
|
87
100
|
}
|
|
88
|
-
const pause =
|
|
101
|
+
const pause = sessions.paused(device.id);
|
|
89
102
|
if (pause) {
|
|
90
103
|
const item = response.content.find(item => item.type === 'text');
|
|
91
104
|
if (item?.text) {
|
|
92
105
|
const payload = JSON.parse(item.text);
|
|
93
106
|
Object.assign(payload, { success: false, failure_code: 'DEBUGGER_PAUSED', retryable: false,
|
|
94
107
|
dispatch_started: guardDispatched(), delivery_status: payload.success ? 'delivered' : guardDispatched() ? 'unknown' : 'not_delivered',
|
|
95
|
-
verification: { status: 'blocked', reason: 'DEBUGGER_PAUSED' }, debugger: pause });
|
|
108
|
+
verification: { status: 'blocked', reason: 'DEBUGGER_PAUSED' }, device_readiness: await observeDeviceReadiness(device), debugger: pause });
|
|
96
109
|
item.text = JSON.stringify(payload);
|
|
97
110
|
}
|
|
98
111
|
}
|
|
@@ -100,14 +113,15 @@ async function dispatchGuarded(device, args, handler, addTiming) {
|
|
|
100
113
|
}));
|
|
101
114
|
}
|
|
102
115
|
function attachPausedObservation(name, args, response) {
|
|
103
|
-
if (['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'get_current_screen', 'read_log_stream'].includes(name)
|
|
116
|
+
if (['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'get_current_screen', 'read_log_stream'].includes(name)) {
|
|
104
117
|
const item = response.content.find(item => item.type === 'text');
|
|
105
118
|
if (item?.text) {
|
|
106
119
|
try {
|
|
107
120
|
const payload = JSON.parse(item.text);
|
|
108
121
|
const device = payload.device ?? payload.evidence?.device;
|
|
109
122
|
const id = device?.id ?? args.deviceId;
|
|
110
|
-
const
|
|
123
|
+
const sessions = (device?.platform ?? args.platform) === 'ios' ? iosDebuggerSessions : debuggerSessions;
|
|
124
|
+
const pause = id ? sessions.paused(String(id)) : null;
|
|
111
125
|
if (pause)
|
|
112
126
|
item.text = JSON.stringify({ ...payload, debugger: pause });
|
|
113
127
|
}
|
|
@@ -125,7 +139,7 @@ export async function handleToolCall(name, args = {}, signal) {
|
|
|
125
139
|
const measured = {};
|
|
126
140
|
const addTiming = (response) => {
|
|
127
141
|
if (!['start_app', 'restart_app', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back', 'wait_for_ui', 'get_ui_tree', 'capture_screenshot', 'capture_debug_snapshot'].includes(name))
|
|
128
|
-
return response;
|
|
142
|
+
return finalizeCompactInteraction(name, args, response);
|
|
129
143
|
const item = response.content.find(item => item.type === 'text');
|
|
130
144
|
if (item?.text) {
|
|
131
145
|
try {
|
|
@@ -135,15 +149,15 @@ export async function handleToolCall(name, args = {}, signal) {
|
|
|
135
149
|
}
|
|
136
150
|
catch { /* Non-JSON text responses retain their original content. */ }
|
|
137
151
|
}
|
|
138
|
-
return response;
|
|
152
|
+
return finalizeCompactInteraction(name, args, response);
|
|
139
153
|
};
|
|
140
154
|
return withTiming(measured, async () => {
|
|
141
155
|
try {
|
|
142
156
|
if (!systemTraceToolNames.has(name))
|
|
143
157
|
validateExpectedState(name, args);
|
|
144
158
|
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);
|
|
145
|
-
if (guarded &&
|
|
146
|
-
const device = await
|
|
159
|
+
if (guarded && (debuggerSessions.hasActiveSessions() || iosDebuggerSessions.hasActiveSessions())) {
|
|
160
|
+
const device = await resolveGuardDevice(name, args);
|
|
147
161
|
return dispatchGuarded(device, args, handler, addTiming);
|
|
148
162
|
}
|
|
149
163
|
const response = await handler(args);
|
|
@@ -152,7 +166,7 @@ export async function handleToolCall(name, args = {}, signal) {
|
|
|
152
166
|
}
|
|
153
167
|
catch (error) {
|
|
154
168
|
if (error instanceof TraceError)
|
|
155
|
-
return { ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true };
|
|
169
|
+
return addTiming({ ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true });
|
|
156
170
|
console.error(`Error executing tool ${name}:`, error);
|
|
157
171
|
return addTiming(wrapToolError(name, error));
|
|
158
172
|
}
|
|
@@ -2,21 +2,22 @@ import { z } from 'zod';
|
|
|
2
2
|
const text = z.string().min(1).max(512);
|
|
3
3
|
const integer = z.number().int().safe().positive();
|
|
4
4
|
const timeout = z.number().int().min(100).max(30000);
|
|
5
|
-
const
|
|
5
|
+
const iosUdid = /^[A-Fa-f0-9]{8}(?:-[A-Fa-f0-9]{4}){3}-[A-Fa-f0-9]{12}$/;
|
|
6
|
+
const target = { platform: z.enum(['android', 'ios']), deviceId: text, appId: text.regex(/^[A-Za-z][\w-]*(?:\.[A-Za-z][\w-]*)+$/), processId: integer.optional() };
|
|
6
7
|
const session = { debugSessionId: text };
|
|
7
8
|
const stop = { ...session, stopId: text };
|
|
8
9
|
const specs = {
|
|
9
|
-
get_debugger_capabilities: { fields: target, command: 'capabilities', description: 'Probe optional
|
|
10
|
-
attach_debugger: { fields: target, command: 'attach', description: 'Attach an owned Android JDI session. Requires an explicit device and debuggable app; choose processId when ambiguous. Stops suspend all app threads. Auto-detach: 60s paused, 5m idle, 30m total.' },
|
|
10
|
+
get_debugger_capabilities: { fields: target, command: 'capabilities', description: 'Probe optional platform debugger prerequisites and matching running debuggable processes; does not attach.' },
|
|
11
|
+
attach_debugger: { fields: target, command: 'attach', description: 'Attach an owned Android JDI or iOS simulator LLDB session. Requires an explicit device and debuggable app; choose processId when ambiguous. Stops suspend all app threads. Auto-detach: 60s paused, 5m idle, 30m total.' },
|
|
11
12
|
get_debugger_state: { fields: session, command: 'state', description: 'Read debugger state, current stop and breakpoint/cleanup status.' },
|
|
12
|
-
set_debugger_breakpoint: { fields: { ...session, className: text.regex(/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/), line: integer }, command: 'set', description: 'Set an exact
|
|
13
|
+
set_debugger_breakpoint: { fields: { ...session, className: text.regex(/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/).optional(), sourceFile: text.regex(/^\/[^\0\r\n]+$/).optional(), line: integer }, command: 'set', description: 'Set an exact source breakpoint: Android className or iOS absolute sourceFile (exactly one), plus line; unloaded classes/images remain pending. No silent line relocation.' },
|
|
13
14
|
remove_debugger_breakpoint: { fields: { ...session, breakpointId: text }, command: 'remove', description: 'Remove an owned breakpoint; repeated removal succeeds.' },
|
|
14
15
|
wait_for_debugger_event: { fields: { ...session, afterSequence: z.number().int().safe().min(0).optional() }, command: 'events', description: 'Wait for bounded debugger events after a sequence cursor. Empty timeout does not affect execution; overflow is explicit.' },
|
|
15
16
|
get_debugger_stack: { fields: { ...stop, threadId: text, offset: z.number().int().min(0).max(999).optional(), limit: z.number().int().min(1).max(100).optional() }, command: 'stack', description: 'Read bounded stack frames for a stopped thread; IDs expire on resume.' },
|
|
16
|
-
get_debugger_locals: { fields: { ...stop, frameId: text }, command: 'locals', description: 'Read at most 100 shallow locals. Sensitive values returned only on request; strings truncate at 256 characters. No evaluation or field traversal.' },
|
|
17
|
+
get_debugger_locals: { fields: { ...stop, frameId: text }, command: 'locals', description: 'Read at most 100 shallow locals. Sensitive values returned only on request; strings truncate at 256 characters; iOS aggregates return type summaries. No evaluation or field traversal.' },
|
|
17
18
|
step_debugger: { fields: { ...stop, threadId: text, kind: z.enum(['into', 'over', 'out']) }, command: 'step', description: 'Acknowledge one-shot line stepping and resume. Wait for a subsequent stop event separately.' },
|
|
18
19
|
resume_debugger: { fields: stop, command: 'resume', description: 'Resume owned debugger suspension and immediately expire stopped references.' },
|
|
19
|
-
detach_debugger: { fields: session, command: 'detach', description: 'Dispose the owned debugger connection, release owned suspension and remove its
|
|
20
|
+
detach_debugger: { fields: session, command: 'detach', description: 'Dispose the owned debugger connection, release owned suspension and remove its owned transport. Idempotent; retries incomplete cleanup.' }
|
|
20
21
|
};
|
|
21
22
|
// Derive JSON schemas and runtime validation from the same small Zod field vocabulary.
|
|
22
23
|
function jsonField(schema) {
|
|
@@ -35,12 +36,14 @@ function jsonField(schema) {
|
|
|
35
36
|
throw new Error('Unsupported debugger field schema');
|
|
36
37
|
}
|
|
37
38
|
export const debuggerContracts = Object.fromEntries(Object.entries(specs).map(([name, spec]) => [name, {
|
|
38
|
-
...spec, schema: z.object({ ...spec.fields, responseMode: z.enum(['compact', 'debug']).default('debug'), timeoutMs: timeout.default(name === 'wait_for_debugger_event' ? 1000 : 10000) }).strict()
|
|
39
|
+
...spec, schema: z.object({ ...spec.fields, responseMode: z.enum(['compact', 'debug']).default('debug'), timeoutMs: timeout.default(name === 'wait_for_debugger_event' ? 1000 : 10000) }).strict().refine(a => name !== 'set_debugger_breakpoint' || (('className' in a && a.className !== undefined) !== ('sourceFile' in a && a.sourceFile !== undefined)), { message: 'Exactly one breakpoint source selector is required' }).refine(a => !('platform' in a) || a.platform !== 'ios' || ('deviceId' in a && typeof a.deviceId === 'string' && iosUdid.test(a.deviceId)), { message: 'iOS requires an explicit simulator UDID' })
|
|
39
40
|
}]));
|
|
40
41
|
export const debuggerTools = Object.entries(debuggerContracts).map(([name, spec]) => ({
|
|
41
42
|
name, description: spec.description,
|
|
42
43
|
inputSchema: {
|
|
43
44
|
type: 'object', additionalProperties: false,
|
|
45
|
+
...(['get_debugger_capabilities', 'attach_debugger'].includes(name) ? { allOf: [{ if: { properties: { platform: { const: 'ios' } } }, then: { properties: { deviceId: { pattern: iosUdid.source } } } }] } : {}),
|
|
46
|
+
...(name === 'set_debugger_breakpoint' ? { oneOf: [{ required: ['className'], not: { required: ['sourceFile'] } }, { required: ['sourceFile'], not: { required: ['className'] } }] } : {}),
|
|
44
47
|
required: Object.entries(spec.fields).filter(([, field]) => !field.isOptional()).map(([key]) => key),
|
|
45
48
|
properties: { responseMode: { type: 'string', enum: ['compact', 'debug'], default: 'debug' }, ...Object.fromEntries(Object.entries(spec.fields).map(([key, field]) => [key, jsonField(field)])), timeoutMs: { type: 'integer', minimum: 100, maximum: 30000, default: name === 'wait_for_debugger_event' ? 1000 : 10000 } }
|
|
46
49
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const diagnosticsSchema = z.object({ sessionId: z.string().uuid(), resultId: z.string().uuid() }).strict();
|
|
3
|
+
export const interactionDiagnosticsTool = {
|
|
4
|
+
name: 'get_interaction_diagnostics',
|
|
5
|
+
description: 'Read the retained original interaction or journey result without replaying input or observing the device. Pass diagnostics.session_id/result_id as sessionId/resultId. Session-local memory: ten minutes, 100 results, 16 MiB total, 1 MiB per result. Expired, evicted or restarted results are unavailable. May contain private UI text and errors; compact output is not redaction.',
|
|
6
|
+
inputSchema: { type: 'object', additionalProperties: false, required: ['sessionId', 'resultId'], properties: {
|
|
7
|
+
sessionId: { type: 'string', format: 'uuid' }, resultId: { type: 'string', format: 'uuid' }
|
|
8
|
+
} }
|
|
9
|
+
};
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
const target = { platform: z.enum(['android', 'ios']), deviceId: z.string().min(1).optional() };
|
|
3
|
-
|
|
4
|
-
export const
|
|
2
|
+
const target = { platform: z.enum(['android', 'ios']), deviceId: z.string().min(1).optional(), appId: z.string().regex(/^[a-zA-Z0-9._-]+$/).optional(), template: z.enum(['System Trace', 'Time Profiler', 'Animation Hitches', 'Hangs']).optional() };
|
|
3
|
+
const validTarget = (value) => value.platform === 'ios' ? !!value.deviceId : value.appId === undefined && value.template === undefined;
|
|
4
|
+
export const traceTargetSchema = z.object(target).strict().refine(validTarget);
|
|
5
|
+
export const traceStartSchema = z.object({ ...target, durationMs: z.number().int().min(1000).max(120000).default(30000) }).strict().refine(validTarget).refine(value => value.platform !== 'ios' || !!value.appId);
|
|
5
6
|
export const traceControlSchema = z.object({ traceId: z.string().uuid() }).strict();
|
|
6
|
-
const properties = { platform: { type: 'string', enum: ['android', 'ios'] }, deviceId: { type: 'string', minLength: 1 } };
|
|
7
|
+
const properties = { platform: { type: 'string', enum: ['android', 'ios'] }, deviceId: { type: 'string', minLength: 1 }, appId: { type: 'string', minLength: 1, pattern: '^[a-zA-Z0-9._-]+$', description: 'iOS only: running development app bundle identifier; required for iOS start.' }, template: { type: 'string', enum: ['System Trace', 'Time Profiler', 'Animation Hitches', 'Hangs'], description: 'iOS only. Default Time Profiler; initial simulator support is Time Profiler only.' } };
|
|
8
|
+
const platformRules = (start) => ({ oneOf: [
|
|
9
|
+
{ properties: { platform: { const: 'android' } }, not: { anyOf: [{ required: ['appId'] }, { required: ['template'] }] } },
|
|
10
|
+
{ properties: { platform: { const: 'ios' } }, required: start ? ['deviceId', 'appId'] : ['deviceId'] }
|
|
11
|
+
] });
|
|
7
12
|
export const systemTraceTools = [
|
|
8
|
-
{ name: 'get_system_trace_capabilities', description: 'Probe Android Perfetto prerequisites without recording.
|
|
9
|
-
inputSchema: { type: 'object', additionalProperties: false, required: ['platform'], properties } },
|
|
10
|
-
{ name: 'start_system_trace', description: 'Start bounded Android Perfetto recording, returning an owned trace_id. Run an existing journey with systemTraceId, then stop to retrieve a local artifact or cancel to discard.
|
|
11
|
-
inputSchema: { type: 'object', additionalProperties: false, required: ['platform'], properties: { ...properties, durationMs: { type: 'integer', minimum: 1000, maximum: 120000, default: 30000 } } } },
|
|
13
|
+
{ name: 'get_system_trace_capabilities', description: 'Probe Android Perfetto or iOS Instruments prerequisites without recording. iOS requires explicit booted simulator deviceId; reports template matrix and unknown profiling permissions.',
|
|
14
|
+
inputSchema: { type: 'object', additionalProperties: false, required: ['platform'], properties, ...platformRules(false) } },
|
|
15
|
+
{ name: 'start_system_trace', description: 'Start bounded Android Perfetto or iOS simulator Time Profiler recording, returning an owned trace_id after readiness. iOS requires explicit deviceId and running appId. Run an existing journey with systemTraceId, then stop to retrieve a local artifact or cancel to discard. Traces can contain sensitive system metadata, logs and paths; local user-managed retention, no upload.',
|
|
16
|
+
inputSchema: { type: 'object', additionalProperties: false, required: ['platform'], properties: { ...properties, durationMs: { type: 'integer', minimum: 1000, maximum: 120000, default: 30000 } }, ...platformRules(true) } },
|
|
12
17
|
...['get_system_trace_status', 'stop_system_trace', 'cancel_system_trace'].map(name => ({ name,
|
|
13
18
|
description: name === 'get_system_trace_status' ? 'Reconcile an owned trace and retrieve its state; finalise duration-completed capture.' :
|
|
14
|
-
name === 'stop_system_trace' ? 'Stop owned capture gracefully and retrieve a local Perfetto
|
|
19
|
+
name === 'stop_system_trace' ? 'Stop owned capture gracefully and retrieve a local Perfetto file or validated Instruments .trace bundle. Retry after a transfer/cleanup failure.' : 'Stop owned capture and discard partial files. Already completed local artifacts are retained.',
|
|
15
20
|
inputSchema: { type: 'object', additionalProperties: false, required: ['traceId'], properties: { traceId: { type: 'string', format: 'uuid' } } }
|
|
16
21
|
}))
|
|
17
22
|
];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { interactionDiagnosticsTool } from './schemas/interaction-diagnostics.js';
|
|
1
2
|
import { verificationModeJson, responseModeJson } from '../contracts/execution-controls.js';
|
|
2
3
|
import { manageTools } from './definitions/manage.js';
|
|
3
4
|
import { observeTools } from './definitions/observe.js';
|
|
@@ -51,9 +52,10 @@ const baseOrder = [
|
|
|
51
52
|
export const toolDefinitions = [
|
|
52
53
|
...debuggerTools,
|
|
53
54
|
...systemTraceTools,
|
|
55
|
+
interactionDiagnosticsTool,
|
|
54
56
|
...['get_app_readiness', 'wait_for_app_ready'].map(name => ({
|
|
55
57
|
name,
|
|
56
|
-
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.`,
|
|
58
|
+
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 and supported iOS simulator 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.`,
|
|
57
59
|
inputSchema: { type: 'object', additionalProperties: false, required: ['platform'], properties: {
|
|
58
60
|
...readinessPropertiesJson,
|
|
59
61
|
timeoutMs: { ...readinessPropertiesJson.timeoutMs, default: name === 'get_app_readiness' ? 15000 : 30000 },
|
|
@@ -67,12 +69,12 @@ for (const tool of toolDefinitions) {
|
|
|
67
69
|
tool.inputSchema.properties ??= {};
|
|
68
70
|
if (['wait_for_ui', 'wait_for_ui_change'].includes(tool.name)) {
|
|
69
71
|
tool.inputSchema.properties.requireReady = { type: 'boolean', default: false };
|
|
70
|
-
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.';
|
|
72
|
+
tool.description += '\nDevice-readiness blockers prevent readiness success; unknown/unsupported device signals remain explicit.\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.';
|
|
71
73
|
}
|
|
72
74
|
if (tool.name === 'get_ui_tree')
|
|
73
75
|
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.';
|
|
74
76
|
if (tool.name === 'run_journey')
|
|
75
|
-
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
|
|
77
|
+
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 through a diagnostics reference in compact mode, and stops subsequent steps. Conditions are not journey defaults or supported on other step types.';
|
|
76
78
|
if (conditionalTools.has(tool.name)) {
|
|
77
79
|
tool.inputSchema.properties.expectedState = expectedStateJson;
|
|
78
80
|
tool.inputSchema.properties.platform ??= { type: 'string', enum: ['android'] };
|
|
@@ -83,12 +85,13 @@ for (const tool of toolDefinitions) {
|
|
|
83
85
|
deviceId: { type: 'string', minLength: 1, pattern: '\\S', not: { enum: ['booted', 'default'] } }
|
|
84
86
|
} }
|
|
85
87
|
}];
|
|
86
|
-
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
|
|
88
|
+
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 summarize the fresh condition evidence; use get_interaction_diagnostics with the returned diagnostics reference for the retained UI tree. This is not atomic with OS input delivery.';
|
|
87
89
|
}
|
|
88
90
|
else {
|
|
89
91
|
tool.inputSchema.allOf = [...(Array.isArray(tool.inputSchema.allOf) ? tool.inputSchema.allOf : []), { not: { required: ['expectedState'] } }];
|
|
90
92
|
}
|
|
91
93
|
if (['start_app', 'restart_app', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back'].includes(tool.name)) {
|
|
94
|
+
tool.description += '\nCompact results summarize observations, condition trees, state deltas and readiness probe evidence within a byte budget. Use get_interaction_diagnostics with the returned diagnostics session_id/result_id to retrieve the original result without replaying input. Debug output is unchanged.';
|
|
92
95
|
Object.assign(tool.inputSchema.properties, {
|
|
93
96
|
verificationMode: { ...verificationModeJson, default: 'full' },
|
|
94
97
|
responseMode: { ...responseModeJson, default: 'debug' },
|
|
@@ -112,6 +115,8 @@ for (const tool of toolDefinitions) {
|
|
|
112
115
|
// Correlation is explicit; an omitted reference creates an independent observation.
|
|
113
116
|
for (const tool of toolDefinitions) {
|
|
114
117
|
tool.inputSchema.properties ??= {};
|
|
118
|
+
if (['start_app', 'restart_app', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back', 'adjust_control', 'get_app_readiness', 'wait_for_app_ready'].includes(tool.name))
|
|
119
|
+
tool.description += '\nResults include device_readiness with timestamped device blocker evidence. Android reports supported power/keyguard signals; iOS signals may be unknown or explicitly unsupported. Launch/input dispatch success does not establish app visibility or input readiness. No automatic unlock or wake is performed.';
|
|
115
120
|
if (['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'read_log_stream'].includes(tool.name)) {
|
|
116
121
|
Object.assign(tool.inputSchema.properties, { actionRef: {
|
|
117
122
|
type: 'object', additionalProperties: false, required: ['sessionId', 'actionId'], properties: {
|
package/dist/server-core.js
CHANGED
|
@@ -3,10 +3,11 @@ import { ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadRes
|
|
|
3
3
|
import { wrapResponse } from './server/common.js';
|
|
4
4
|
import { toolDefinitions } from './server/tool-definitions.js';
|
|
5
5
|
import { handleToolCall } from './server/handlers/tool-handlers.js';
|
|
6
|
+
import { withBenchmarkRequest, benchmarkStage } from './utils/benchmark-metrics.js';
|
|
6
7
|
export { wrapResponse, toolDefinitions, handleToolCall };
|
|
7
8
|
export const serverInfo = {
|
|
8
9
|
name: 'mobile-debug-mcp',
|
|
9
|
-
version: '0.
|
|
10
|
+
version: '0.37.0'
|
|
10
11
|
};
|
|
11
12
|
export function createServer() {
|
|
12
13
|
const server = new Server(serverInfo, {
|
|
@@ -29,7 +30,9 @@ export function createServer() {
|
|
|
29
30
|
}));
|
|
30
31
|
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
31
32
|
const { name, arguments: args } = request.params;
|
|
32
|
-
|
|
33
|
+
const operation = () => handleToolCall(name, args, extra.signal);
|
|
34
|
+
return withBenchmarkRequest(name, () => name.startsWith('expect_') || name.startsWith('wait_for_')
|
|
35
|
+
? benchmarkStage(name.startsWith('expect_') ? 'assertion_ms' : 'wait_ms', operation) : operation());
|
|
33
36
|
});
|
|
34
37
|
return server;
|
|
35
38
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { debuggerSessions } from './debugger/sessions.js';
|
|
2
|
+
import { debuggerSessions, iosDebuggerSessions } from './debugger/sessions.js';
|
|
3
3
|
import { systemTraces } from './observe/system-trace.js';
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
5
|
import { createServer } from './server-core.js';
|
|
@@ -7,7 +7,7 @@ import { getSystemStatus } from './system/index.js';
|
|
|
7
7
|
const server = createServer();
|
|
8
8
|
if (process.env.MOBILE_DEBUG_MCP_STARTUP_HEALTHCHECK === '1') {
|
|
9
9
|
getSystemStatus().then((res) => {
|
|
10
|
-
console.info('[startup] system status summary:', { adb: res.adbAvailable, ios: res.iosAvailable, devices: res.devices, iosDevices: res.iosDevices });
|
|
10
|
+
console.info('[startup] system status summary:', { adb: 'adbAvailable' in res ? res.adbAvailable : false, ios: 'iosAvailable' in res ? res.iosAvailable : false, devices: 'devices' in res ? res.devices : 0, iosDevices: 'iosDevices' in res ? res.iosDevices : 0 });
|
|
11
11
|
}).catch((e) => console.warn('[startup] healthcheck failed:', e instanceof Error ? e.message : String(e)));
|
|
12
12
|
}
|
|
13
13
|
const transport = new StdioServerTransport();
|
|
@@ -19,10 +19,10 @@ async function shutdown() {
|
|
|
19
19
|
const deadline = setTimeout(() => { console.error('System trace shutdown cleanup timed out; owned device captures remain duration-bounded.'); process.exit(1); }, 10000);
|
|
20
20
|
let exitCode = 0;
|
|
21
21
|
try {
|
|
22
|
-
const [remaining, debuggers] = await Promise.all([systemTraces.shutdown(), debuggerSessions.shutdown()]);
|
|
23
|
-
if (debuggers.length) {
|
|
22
|
+
const [remaining, debuggers, iosDebuggers] = await Promise.all([systemTraces.shutdown(), debuggerSessions.shutdown(), iosDebuggerSessions.shutdown()]);
|
|
23
|
+
if ((debuggers.length + iosDebuggers.length)) {
|
|
24
24
|
exitCode = 1;
|
|
25
|
-
console.error('Debugger shutdown cleanup incomplete', debuggers.map(s => s.cleanup));
|
|
25
|
+
console.error('Debugger shutdown cleanup incomplete', [...debuggers, ...iosDebuggers].map(s => s.cleanup));
|
|
26
26
|
}
|
|
27
27
|
if (remaining.length) {
|
|
28
28
|
exitCode = 1;
|
package/dist/system/android.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { probeInstallation, validAppId } from '../utils/app-installation.js';
|
|
1
2
|
import { execSync } from 'child_process';
|
|
2
3
|
import { ensureAdbAvailable } from '../utils/android/utils.js';
|
|
3
4
|
export async function checkAndroid() {
|
|
@@ -9,6 +10,8 @@ export async function checkAndroid() {
|
|
|
9
10
|
let logsAvailable = false;
|
|
10
11
|
let envValid = false;
|
|
11
12
|
let appInstalled = undefined;
|
|
13
|
+
let appInstallations = [];
|
|
14
|
+
let targets = [];
|
|
12
15
|
try {
|
|
13
16
|
const adbCheck = ensureAdbAvailable();
|
|
14
17
|
const adbCmd = adbCheck.adbCmd || 'adb';
|
|
@@ -19,6 +22,7 @@ export async function checkAndroid() {
|
|
|
19
22
|
try {
|
|
20
23
|
const out = execSync(`${adbCmd} devices -l`, { encoding: 'utf8', timeout: 1500, stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
21
24
|
const state = parseDevices(out, issues);
|
|
25
|
+
targets = out.split('\n').map(line => line.trim().split(/\s+/)).filter(parts => parts[0] && !['List', '*'].includes(parts[0])).map(parts => ({ id: parts[0], available: parts[1] === 'device' }));
|
|
22
26
|
devices = state.devices;
|
|
23
27
|
deviceStates = state.deviceStates;
|
|
24
28
|
}
|
|
@@ -43,15 +47,15 @@ export async function checkAndroid() {
|
|
|
43
47
|
if (!envValid)
|
|
44
48
|
issues.push('ANDROID_SDK_ROOT/ANDROID_HOME missing and adb not found in PATH');
|
|
45
49
|
const pkg = process.env.MCP_TARGET_PACKAGE || process.env.MCP_TARGET_APP_ID;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
appInstallations = await Promise.all(targets.map(target => probeInstallation(adbCmd, target.id, pkg, 'android-current-user', adbAvailable && target.available)));
|
|
51
|
+
reportInstallations(appInstallations, pkg, issues);
|
|
52
|
+
appInstalled = installationSummary(appInstallations);
|
|
49
53
|
}
|
|
50
54
|
catch (e) {
|
|
51
55
|
console.debug('[get_system_status] adb availability check failed: ' + String(e));
|
|
52
56
|
issues.push('ADB check failed');
|
|
53
57
|
}
|
|
54
|
-
return { adbAvailable, adbVersion, devices, deviceStates, logsAvailable, envValid, appInstalled, issues };
|
|
58
|
+
return { adbAvailable, adbVersion, devices, deviceStates, logsAvailable, envValid, appInstalled, appInstallations, issues };
|
|
55
59
|
}
|
|
56
60
|
function parseDevices(out, issues) {
|
|
57
61
|
const lines = out.split('\n').map(l => l.trim()).filter(Boolean);
|
|
@@ -73,16 +77,16 @@ function parseDevices(out, issues) {
|
|
|
73
77
|
issues.push(`${stateCounts['offline']} device(s) offline`);
|
|
74
78
|
return { devices, deviceStates };
|
|
75
79
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
function installationSummary(checks) {
|
|
81
|
+
return checks.length === 1 && checks[0].status !== 'unknown' ? checks[0].status === 'installed' : undefined;
|
|
82
|
+
}
|
|
83
|
+
function reportInstallations(checks, pkg, issues) {
|
|
84
|
+
if (pkg !== undefined && !validAppId(pkg))
|
|
85
|
+
issues.push('Invalid target app identifier');
|
|
86
|
+
for (const check of checks) {
|
|
87
|
+
if (check.status === 'absent')
|
|
88
|
+
issues.push(`App ${check.appId} not installed on device ${check.deviceId}`);
|
|
89
|
+
if (check.status === 'unknown' && check.reason !== 'APP_ID_NOT_PROVIDED')
|
|
90
|
+
issues.push(`Installation check unavailable on device ${check.deviceId}: ${check.reason}`);
|
|
87
91
|
}
|
|
88
92
|
}
|
package/dist/system/index.js
CHANGED
|
@@ -40,7 +40,8 @@ export async function getSystemStatus() {
|
|
|
40
40
|
logsAvailable: androidRaw.logsAvailable,
|
|
41
41
|
envValid: androidRaw.envValid,
|
|
42
42
|
issues,
|
|
43
|
-
appInstalled: androidRaw.appInstalled,
|
|
43
|
+
...(androidRaw.appInstalled === undefined ? {} : { appInstalled: androidRaw.appInstalled }),
|
|
44
|
+
appInstallations: androidRaw.appInstallations,
|
|
44
45
|
iosAvailable: iosRaw.iosAvailable,
|
|
45
46
|
iosDevices: iosRaw.iosDevices,
|
|
46
47
|
gradleJavaHome: gradle.gradleJavaHome,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { promises as fsPromises } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
export { findInPath, resolveAdbCmd, getAdbCmd, ensureAdbAvailable, getAdbArgs, getAdbTimeout, execAdb, spawnAdb } from './commands.js';
|
|
4
|
-
import { execAdb } from './commands.js';
|
|
4
|
+
import { execAdb, getAdbCmd } from './commands.js';
|
|
5
|
+
import { installationFields, probeInstallation } from '../app-installation.js';
|
|
5
6
|
export { mergeEnv, prepareGradle } from './gradle.js';
|
|
6
7
|
export { parseBounds, getCenter, parseBooleanAttr, parseNumberAttr, normalizeClassName, inferAndroidRole, buildAndroidSelectorConfidence, buildAndroidSelector, buildAndroidSemantic, isSliderLikeAndroid, extractAndroidState, traverseNode } from './ui-tree.js';
|
|
7
8
|
export { parseLogLine } from './logs.js';
|
|
@@ -79,22 +80,12 @@ export async function listAndroidDevices(appId) {
|
|
|
79
80
|
execAdb(['shell', 'getprop', 'ro.kernel.qemu'], serial).catch(() => '0')
|
|
80
81
|
]);
|
|
81
82
|
const simulator = simOutput === '1';
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const pm = await execAdb(['shell', 'pm', 'path', appId], serial);
|
|
86
|
-
appInstalled = !!(pm && pm.includes('package:'));
|
|
87
|
-
}
|
|
88
|
-
catch (e) {
|
|
89
|
-
console.debug(`[listAndroidDevices] pm check failed for ${serial}: ${String(e)}`);
|
|
90
|
-
appInstalled = false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return { platform: 'android', id: serial, osVersion, model, simulator, appInstalled };
|
|
83
|
+
const check = await probeInstallation(getAdbCmd(), serial, appId, 'android-current-user', deviceLines.find(line => line.split(/\s+/)[0] === serial)?.split(/\s+/)[1] === 'device');
|
|
84
|
+
return { platform: 'android', id: serial, osVersion, model, simulator, ...installationFields(check) };
|
|
94
85
|
}
|
|
95
|
-
catch
|
|
96
|
-
|
|
97
|
-
return { platform: 'android', id: serial, osVersion: '', model: '', simulator: false,
|
|
86
|
+
catch {
|
|
87
|
+
const check = await probeInstallation(getAdbCmd(), serial, appId, 'android-current-user', false);
|
|
88
|
+
return { platform: 'android', id: serial, osVersion: '', model: '', simulator: false, ...installationFields(check) };
|
|
98
89
|
}
|
|
99
90
|
}));
|
|
100
91
|
return infos;
|