mobile-debug-mcp 0.30.2 → 0.32.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/README.md +50 -5
- package/dist/interact/android.js +13 -8
- package/dist/interact/index.js +33 -22
- package/dist/interact/ios.js +5 -2
- package/dist/manage/android.js +20 -10
- package/dist/manage/ios.js +20 -11
- package/dist/observe/android.js +6 -1
- package/dist/observe/index.js +22 -5
- package/dist/observe/ios.js +7 -2
- package/dist/observe/snapshot-metadata.js +94 -15
- package/dist/server/common.js +14 -7
- package/dist/server/journey-schema.js +60 -0
- package/dist/server/tool-definitions.js +59 -6
- package/dist/server/tool-handlers.js +358 -55
- package/dist/server-core.js +1 -1
- package/dist/system/index.js +485 -48
- package/dist/system/ios.js +15 -7
- package/dist/utils/android/utils.js +4 -4
- package/dist/utils/cli/idb/check-idb.js +10 -3
- package/dist/utils/cli/idb/idb-helper.js +47 -13
- package/dist/utils/exec.js +4 -2
- package/dist/utils/ios/utils.js +10 -11
- package/dist/utils/operation-budget.js +25 -0
- package/dist/utils/resolve-device.js +12 -1
- package/dist/utils/timing.js +19 -0
- package/dist/utils/ui/index.js +2 -0
- package/docs/CHANGELOG.md +6 -0
- package/docs/ROADMAP.md +4 -4
- package/docs/benchmarks/interaction-latency/README.md +26 -0
- package/docs/benchmarks/interaction-latency/final.json +1810 -0
- package/docs/benchmarks/interaction-latency/final.md +17 -0
- package/docs/benchmarks/interaction-latency/ios-validation.md +11 -0
- package/docs/specs/baseline-spec-v0.md +1 -1
- package/docs/specs-deltas/016-environment-tools.md +533 -0
- package/docs/specs-deltas/018-interaction-latency-and-batched-journeys.md +301 -0
- package/docs/tools/TOOLS.md +2 -0
- package/docs/tools/interact.md +30 -0
- package/docs/tools/manage.md +4 -2
- package/docs/tools/system.md +9 -2
- package/package.json +1 -1
- package/src/interact/android.ts +13 -8
- package/src/interact/index.ts +32 -23
- package/src/interact/ios.ts +5 -2
- package/src/manage/android.ts +19 -10
- package/src/manage/ios.ts +19 -11
- package/src/observe/android.ts +5 -1
- package/src/observe/index.ts +21 -5
- package/src/observe/ios.ts +6 -2
- package/src/observe/snapshot-metadata.ts +94 -15
- package/src/server/common.ts +13 -6
- package/src/server/journey-schema.ts +63 -0
- package/src/server/tool-definitions.ts +60 -7
- package/src/server/tool-handlers.ts +324 -53
- package/src/server-core.ts +1 -1
- package/src/system/index.ts +572 -50
- package/src/system/ios.ts +14 -7
- package/src/types.ts +31 -2
- package/src/utils/android/utils.ts +3 -3
- package/src/utils/cli/idb/check-idb.ts +11 -2
- package/src/utils/cli/idb/idb-helper.ts +49 -12
- package/src/utils/exec.ts +4 -2
- package/src/utils/ios/utils.ts +9 -10
- package/src/utils/operation-budget.ts +22 -0
- package/src/utils/resolve-device.ts +13 -1
- package/src/utils/timing.ts +15 -0
- package/src/utils/ui/index.ts +2 -0
- package/test/device/automated/latency/benchmark.ts +136 -0
- package/test/device/automated/latency/report.ts +6 -0
- package/test/fixtures/latency-journey-app/AndroidManifest.xml +6 -0
- package/test/fixtures/latency-journey-app/README.md +10 -0
- package/test/fixtures/latency-journey-app/build.ts +26 -0
- package/test/fixtures/latency-journey-app/src/MainActivity.java +48 -0
- package/test/unit/interact/tap_element.test.ts +1 -1
- package/test/unit/manage/restart_budget.test.ts +37 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +6 -0
- package/test/unit/observe/snapshot_metadata.test.ts +20 -1
- package/test/unit/observe/state_delta.test.ts +15 -0
- package/test/unit/server/benchmark_report.test.ts +4 -0
- package/test/unit/server/contract.test.ts +1 -1
- package/test/unit/server/interaction_modes.test.ts +60 -0
- package/test/unit/server/operation_budget.test.ts +32 -0
- package/test/unit/server/response_shapes.test.ts +3 -3
- package/test/unit/server/run_journey.test.ts +90 -0
- package/test/unit/server/tap_selector.test.ts +68 -0
- package/test/unit/system/get_system_status.test.ts +3 -0
- package/test/unit/system/system_status.test.ts +40 -0
- package/test/unit/utils/idb_health.test.ts +74 -0
|
@@ -71,21 +71,55 @@ export function getIdbCmd() {
|
|
|
71
71
|
}
|
|
72
72
|
return null;
|
|
73
73
|
}
|
|
74
|
-
export function
|
|
75
|
-
const
|
|
74
|
+
export function getIdbPackageVersion() {
|
|
75
|
+
const runners = ['python3', 'python'];
|
|
76
|
+
for (const runner of runners) {
|
|
77
|
+
try {
|
|
78
|
+
const result = spawnSync(runner, ['-m', 'pip', 'show', 'fb-idb'], {
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
81
|
+
});
|
|
82
|
+
if (result.status !== 0)
|
|
83
|
+
continue;
|
|
84
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
85
|
+
const match = output.match(/^Version:\s*(.+)$/mi);
|
|
86
|
+
if (match?.[1])
|
|
87
|
+
return match[1].trim();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
export function probeIdb(cmd = getIdbCmd()) {
|
|
76
96
|
if (!cmd)
|
|
77
|
-
return false;
|
|
97
|
+
return { ok: false, raw: null, parsedVersion: null };
|
|
78
98
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
const result = spawnSync(cmd, ['list-targets', '--json'], {
|
|
100
|
+
encoding: 'utf8',
|
|
101
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
102
|
+
timeout: 5000
|
|
103
|
+
});
|
|
104
|
+
if (result.status !== 0) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
raw: `${result.stdout || ''}${result.stderr || ''}`.trim() || null,
|
|
108
|
+
parsedVersion: null
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const version = getIdbPackageVersion();
|
|
112
|
+
const raw = version ? `fb-idb ${version}` : `${result.stdout || ''}${result.stderr || ''}`.trim() || null;
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
raw,
|
|
116
|
+
parsedVersion: version
|
|
117
|
+
};
|
|
83
118
|
}
|
|
84
|
-
catch {
|
|
85
|
-
|
|
86
|
-
execSync(`${cmd} list-targets --json`, { stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 });
|
|
87
|
-
return true;
|
|
119
|
+
catch {
|
|
120
|
+
return { ok: false, raw: null, parsedVersion: null };
|
|
88
121
|
}
|
|
89
|
-
|
|
90
|
-
|
|
122
|
+
}
|
|
123
|
+
export function isIDBInstalled() {
|
|
124
|
+
return probeIdb().ok;
|
|
91
125
|
}
|
package/dist/utils/exec.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { remainingBudget } from './operation-budget.js';
|
|
1
2
|
import { spawn } from 'child_process';
|
|
2
3
|
export async function execCmd(cmd, args, opts = {}) {
|
|
3
|
-
const {
|
|
4
|
+
const { env, cwd, shell } = opts;
|
|
5
|
+
const timeout = remainingBudget(opts.timeout ?? 0);
|
|
4
6
|
return new Promise((resolve, reject) => {
|
|
5
7
|
const child = spawn(cmd, args, { env: { ...process.env, ...(env || {}) }, cwd, shell });
|
|
6
8
|
let stdout = '';
|
|
@@ -16,7 +18,7 @@ export async function execCmd(cmd, args, opts = {}) {
|
|
|
16
18
|
child.kill();
|
|
17
19
|
}
|
|
18
20
|
catch { }
|
|
19
|
-
resolve({ exitCode: null, stdout: stdout.trim(), stderr:
|
|
21
|
+
resolve({ exitCode: null, stdout: stdout.trim(), stderr: 'ACTION_TIMEOUT' });
|
|
20
22
|
}, timeout) : null;
|
|
21
23
|
child.on('close', (code) => {
|
|
22
24
|
if (timer)
|
package/dist/utils/ios/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { remainingBudget } from '../operation-budget.js';
|
|
1
2
|
import { execFile, spawn, execSync, spawnSync } from "child_process";
|
|
2
3
|
import { promises as fsPromises } from 'fs';
|
|
3
4
|
import path from 'path';
|
|
@@ -71,19 +72,15 @@ export function getIdbCmd() {
|
|
|
71
72
|
}
|
|
72
73
|
export async function isIDBInstalled() {
|
|
73
74
|
const cmd = getIdbCmd();
|
|
75
|
+
if (!cmd)
|
|
76
|
+
return false;
|
|
74
77
|
try {
|
|
75
|
-
execSync(
|
|
78
|
+
execSync(`${cmd} list-targets --json`, { stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 });
|
|
76
79
|
return true;
|
|
77
80
|
}
|
|
78
81
|
catch (e) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
catch (e2) {
|
|
84
|
-
console.debug(`[isIDBInstalled] idb presence check failed for '${cmd}': ${e instanceof Error ? e.message : String(e2)}`);
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
82
|
+
console.debug(`[isIDBInstalled] idb probe failed for '${cmd}': ${e instanceof Error ? e.message : String(e)}`);
|
|
83
|
+
return false;
|
|
87
84
|
}
|
|
88
85
|
}
|
|
89
86
|
// Validate bundle ID to prevent any potential injection or invalid characters
|
|
@@ -98,6 +95,7 @@ export function validateBundleId(bundleId) {
|
|
|
98
95
|
export function execCommand(args, deviceId = "booted") {
|
|
99
96
|
return new Promise((resolve, reject) => {
|
|
100
97
|
// Use spawn for better stream control and consistency with Android implementation
|
|
98
|
+
const budgetMs = remainingBudget();
|
|
101
99
|
const child = spawn(getXcrunCmd(), args);
|
|
102
100
|
let stdout = '';
|
|
103
101
|
let stderr = '';
|
|
@@ -113,7 +111,7 @@ export function execCommand(args, deviceId = "booted") {
|
|
|
113
111
|
}
|
|
114
112
|
const DEFAULT_XCRUN_LOG_TIMEOUT = parseInt(process.env.MCP_XCRUN_LOG_TIMEOUT || '', 10) || 30000; // env (ms) or default 30s
|
|
115
113
|
const DEFAULT_XCRUN_CMD_TIMEOUT = parseInt(process.env.MCP_XCRUN_TIMEOUT || '', 10) || 60000; // env (ms) or default 60s
|
|
116
|
-
const timeoutMs = args.includes('log') ? DEFAULT_XCRUN_LOG_TIMEOUT : DEFAULT_XCRUN_CMD_TIMEOUT; // choose appropriate timeout
|
|
114
|
+
const timeoutMs = budgetMs || (args.includes('log') ? DEFAULT_XCRUN_LOG_TIMEOUT : DEFAULT_XCRUN_CMD_TIMEOUT); // choose appropriate timeout
|
|
117
115
|
const timeout = setTimeout(() => {
|
|
118
116
|
child.kill();
|
|
119
117
|
reject(new Error(`Command timed out after ${timeoutMs}ms: ${getXcrunCmd()} ${args.join(' ')}`));
|
|
@@ -134,10 +132,11 @@ export function execCommand(args, deviceId = "booted") {
|
|
|
134
132
|
});
|
|
135
133
|
}
|
|
136
134
|
export function execCommandWithDiagnostics(args, deviceId = "booted") {
|
|
135
|
+
const budgetMs = remainingBudget();
|
|
137
136
|
// Run synchronously to capture stdout/stderr and exitCode reliably for diagnostics
|
|
138
137
|
const DEFAULT_XCRUN_LOG_TIMEOUT = parseInt(process.env.MCP_XCRUN_LOG_TIMEOUT || '', 10) || 30000;
|
|
139
138
|
const DEFAULT_XCRUN_CMD_TIMEOUT = parseInt(process.env.MCP_XCRUN_TIMEOUT || '', 10) || 60000;
|
|
140
|
-
const timeoutMs = args.includes('log') ? DEFAULT_XCRUN_LOG_TIMEOUT : DEFAULT_XCRUN_CMD_TIMEOUT;
|
|
139
|
+
const timeoutMs = budgetMs || (args.includes('log') ? DEFAULT_XCRUN_LOG_TIMEOUT : DEFAULT_XCRUN_CMD_TIMEOUT);
|
|
141
140
|
const res = spawnSync(getXcrunCmd(), args, { encoding: 'utf8', timeout: timeoutMs });
|
|
142
141
|
const runResult = {
|
|
143
142
|
exitCode: typeof res.status === 'number' ? res.status : null,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
const budgets = new AsyncLocalStorage();
|
|
3
|
+
export function hasFiniteBudget() { return Number.isFinite(budgets.getStore()?.deadline); }
|
|
4
|
+
export function runWithBudget(timeoutMs, operation) {
|
|
5
|
+
const parent = budgets.getStore();
|
|
6
|
+
const budget = { deadline: Math.min(parent?.deadline ?? Infinity, performance.now() + timeoutMs), dispatchStarted: false, parent };
|
|
7
|
+
return budgets.run(budget, () => operation(budget));
|
|
8
|
+
}
|
|
9
|
+
export function remainingBudget(fallback = 0) {
|
|
10
|
+
const budget = budgets.getStore();
|
|
11
|
+
if (!budget || !Number.isFinite(budget.deadline))
|
|
12
|
+
return fallback;
|
|
13
|
+
const remaining = Math.floor(budget.deadline - performance.now());
|
|
14
|
+
if (remaining <= 0)
|
|
15
|
+
throw new Error('ACTION_TIMEOUT');
|
|
16
|
+
return fallback > 0 ? Math.min(fallback, remaining) : remaining;
|
|
17
|
+
}
|
|
18
|
+
export function markDispatchStarted() {
|
|
19
|
+
remainingBudget();
|
|
20
|
+
let budget = budgets.getStore();
|
|
21
|
+
while (budget) {
|
|
22
|
+
budget.dispatchStarted = true;
|
|
23
|
+
budget = budget.parent;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { retainConnectedDeviceSnapshots } from '../observe/snapshot-metadata.js';
|
|
2
|
+
import { measure } from './timing.js';
|
|
3
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
1
4
|
import { listAndroidDevices } from "./android/utils.js";
|
|
2
5
|
import { listIOSDevices } from "./ios/utils.js";
|
|
3
6
|
function parseNumericVersion(v) {
|
|
@@ -32,9 +35,17 @@ export async function listDevices(platform, appId) {
|
|
|
32
35
|
}
|
|
33
36
|
return iosDeviceLister(appId);
|
|
34
37
|
}
|
|
38
|
+
const deviceContext = new AsyncLocalStorage();
|
|
39
|
+
export function withResolvedDevice(device, operation) {
|
|
40
|
+
return deviceContext.run(device, operation);
|
|
41
|
+
}
|
|
35
42
|
export async function resolveTargetDevice(opts) {
|
|
36
43
|
const { platform, appId, prefer, deviceId } = opts;
|
|
37
|
-
const
|
|
44
|
+
const current = deviceContext.getStore();
|
|
45
|
+
if (current && current.platform === platform && (!deviceId || current.id === deviceId))
|
|
46
|
+
return current;
|
|
47
|
+
const devices = await measure('device_resolution_ms', () => listDevices(platform, appId));
|
|
48
|
+
retainConnectedDeviceSnapshots(platform, devices.map(device => device.id));
|
|
38
49
|
// During unit tests (no adb/xcrun available), provide a lightweight mock device so
|
|
39
50
|
// the observe/interact unit tests can run without real devices.
|
|
40
51
|
if ((!devices || devices.length === 0) && (process.env.NODE_ENV === 'test' || process.env.MCP_TEST_MOCK_DEVICES === '1')) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
const timings = new AsyncLocalStorage();
|
|
3
|
+
export function withTiming(values, operation) { return timings.run(values, operation); }
|
|
4
|
+
export function recordTiming(component, milliseconds) {
|
|
5
|
+
const values = timings.getStore();
|
|
6
|
+
if (values)
|
|
7
|
+
values[component] = (values[component] ?? 0) + milliseconds;
|
|
8
|
+
}
|
|
9
|
+
export async function measure(component, operation) {
|
|
10
|
+
const start = performance.now();
|
|
11
|
+
try {
|
|
12
|
+
return await operation();
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
const values = timings.getStore();
|
|
16
|
+
if (values)
|
|
17
|
+
values[component] = (values[component] ?? 0) + performance.now() - start;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/utils/ui/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { remainingBudget } from '../operation-budget.js';
|
|
1
2
|
import crypto from 'crypto';
|
|
2
3
|
const ANDROID_STRUCTURAL_TYPES = ['Window', 'Application', 'View', 'ViewGroup', 'LinearLayout', 'FrameLayout', 'RelativeLayout', 'ScrollView', 'RecyclerView', 'TextView', 'ImageView'];
|
|
3
4
|
const IOS_STRUCTURAL_TYPES = ['Window', 'Application', 'View', 'ViewController', 'UITableView', 'UICollectionView', 'UILabel', 'UIImageView', 'UIView', 'UIWindow', 'UIStackView', 'UITextView', 'UITableViewCell'];
|
|
@@ -140,6 +141,7 @@ export async function scrollToElementShared(opts) {
|
|
|
140
141
|
const duration = 300;
|
|
141
142
|
let scrollsPerformed = 0;
|
|
142
143
|
for (let i = 0; i < maxScrolls; i++) {
|
|
144
|
+
remainingBudget();
|
|
143
145
|
const { x1, y1, x2, y2 } = computeCoords();
|
|
144
146
|
try {
|
|
145
147
|
await swipe(x1, y1, x2, y2, duration, deviceId);
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.32.0]
|
|
6
|
+
- Added bounded interaction verification modes, compact responses, selector-based taps, and batched journeys to reduce MCP interaction latency.
|
|
7
|
+
|
|
8
|
+
## [0.31.0]
|
|
9
|
+
- Added tool discovery/automation
|
|
10
|
+
|
|
5
11
|
## [0.30.2]
|
|
6
12
|
- improved slider interaction
|
|
7
13
|
|
package/docs/ROADMAP.md
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
- Action Trace and Execution Observability
|
|
10
10
|
- Wait and Synchronization Reliability
|
|
11
11
|
- Actionability Resolution
|
|
12
|
+
- Environment Auto-Configuration and Toolchain Discovery
|
|
13
|
+
- Adjustable Control Support
|
|
12
14
|
|
|
13
15
|
## Todo
|
|
14
16
|
|
|
15
|
-
- Adjustable Control Support
|
|
16
17
|
- Adjustable Control Precision Hardening
|
|
17
|
-
- Environment Auto-Configuration and Toolchain Discovery
|
|
18
18
|
- Signal-Oriented Diagnostic Filtering
|
|
19
19
|
- Long Press Gesture
|
|
20
20
|
- Runtime Debugger Introspection
|
|
@@ -205,7 +205,7 @@ Strengthens:
|
|
|
205
205
|
## Rationale
|
|
206
206
|
Reduces failures caused by interacting with discoverable but non-actionable UI nodes.
|
|
207
207
|
|
|
208
|
-
**Status:**
|
|
208
|
+
**Status:** Completed
|
|
209
209
|
|
|
210
210
|
Addresses cases where:
|
|
211
211
|
- visible text is not the true click target
|
|
@@ -251,7 +251,7 @@ Blocks or strengthens:
|
|
|
251
251
|
## Rationale
|
|
252
252
|
High leverage improvement for sliders and parameterized controls.
|
|
253
253
|
|
|
254
|
-
**Status:**
|
|
254
|
+
**Status:** Completed
|
|
255
255
|
|
|
256
256
|
Addresses friction around:
|
|
257
257
|
- coordinate-calibrated slider interaction
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Interaction latency benchmark
|
|
2
|
+
|
|
3
|
+
Run after `npm run verify`:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
MCP_ADB_TIMEOUT=30000 LATENCY_DEVICE_ID=emulator-5554 npx tsx test/device/automated/latency/benchmark.ts
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Use the named Pixel 9 Pro Android 16 emulator with SDK platform 36, build-tools
|
|
10
|
+
36.0.0, and a Java installation capable of `javac --release 8`. Set its display
|
|
11
|
+
to 720x1600 at density 240 to keep repeated UI hierarchy reads responsive; the
|
|
12
|
+
report records the image, API, display size, density, and Android command timeout.
|
|
13
|
+
The harness builds and signs its own offline fixture. It resets that fixture
|
|
14
|
+
only; no other app data is cleared.
|
|
15
|
+
|
|
16
|
+
The sequential protocol compares the legacy handle-based public tool flow with
|
|
17
|
+
full/debug and light/compact journeys through a spawned stdio MCP server. Each
|
|
18
|
+
mode has three warm-ups and ten measured runs. Screenshot and screenshot-free
|
|
19
|
+
debug snapshot calls are measured separately. JSON and Markdown reports record
|
|
20
|
+
fixture hash, environment, request sequence, failures, and percentiles. Failed
|
|
21
|
+
runs do not enter percentiles. Raw observations and device serials are not saved.
|
|
22
|
+
See [iOS simulator validation](ios-validation.md) for the interaction checks.
|
|
23
|
+
|
|
24
|
+
The process exits unsuccessfully on >10% full p95 regression or <30% light p50
|
|
25
|
+
improvement. A failed run is reported and excluded from percentiles as the spec
|
|
26
|
+
requires; inspect its selector and tool or step before accepting the result.
|