mobile-debug-mcp 0.24.5 → 0.24.7
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/.github/workflows/ci.yml +1 -3
- package/README.md +7 -0
- package/dist/manage/android.js +9 -5
- package/dist/manage/index.js +37 -23
- package/dist/manage/ios.js +12 -15
- package/dist/observe/index.js +2 -2
- package/dist/server/common.js +46 -0
- package/dist/server/tool-handlers.js +120 -33
- package/dist/server-core.js +1 -1
- package/dist/utils/android/utils.js +17 -5
- package/dist/utils/cli/idb/check-idb.js +1 -1
- package/docs/CHANGELOG.md +16 -8
- package/docs/tools/observe.md +2 -2
- package/eslint.config.js +2 -47
- package/package.json +7 -6
- package/src/manage/android.ts +22 -11
- package/src/manage/index.ts +37 -16
- package/src/manage/ios.ts +28 -15
- package/src/observe/index.ts +2 -2
- package/src/server/common.ts +50 -0
- package/src/server/tool-handlers.ts +136 -32
- package/src/server-core.ts +1 -1
- package/src/utils/android/utils.ts +18 -7
- package/src/utils/cli/idb/check-idb.ts +1 -1
- package/test/device/automated/observe/capture_screenshot.android.smoke.ts +1 -1
- package/test/device/automated/observe/capture_screenshot.ios.smoke.ts +1 -1
- package/test/device/automated/observe/get_logs.android.smoke.ts +1 -1
- package/test/device/automated/observe/get_logs.ios.smoke.ts +1 -1
- package/test/device/automated/observe/get_ui_tree.android.smoke.ts +1 -1
- package/test/device/automated/observe/get_ui_tree.ios.smoke.ts +1 -1
- package/test/device/manual/interact/app_lifecycle.manual.ts +3 -3
- package/test/device/manual/observe/capture_screenshot.manual.ts +2 -2
- package/test/device/manual/observe/get_logs.manual.ts +2 -2
- package/test/device/manual/observe/get_ui_tree.manual.ts +2 -2
- package/test/device/manual/observe/logstream.manual.ts +1 -1
- package/test/device/manual/observe/screen_fingerprint.manual.ts +2 -2
- package/test/unit/manage/scoped_env.test.ts +137 -0
- package/test/unit/server/capture_screenshot.test.ts +17 -0
- package/test/unit/server/common.test.ts +18 -0
- package/test/unit/server/contract.test.ts +3 -0
- package/test/unit/server/get_logs.test.ts +17 -0
- package/test/unit/server/get_network_activity.test.ts +17 -0
- package/test/unit/server/get_ui_tree.test.ts +17 -0
- package/test/unit/server/response_shapes.test.ts +18 -0
- package/test/unit/server/start_log_stream.test.ts +37 -0
- package/.eslintignore +0 -5
- package/.eslintrc.cjs +0 -18
- package/eslint.config.cjs +0 -36
package/.github/workflows/ci.yml
CHANGED
package/README.md
CHANGED
|
@@ -52,8 +52,15 @@ Feature building:
|
|
|
52
52
|
|
|
53
53
|
- `npm run test:unit` runs every automated unit test under `test/unit/...`
|
|
54
54
|
- `npm run test:device` runs the automated device smoke checks under `test/device/automated/...`
|
|
55
|
+
- `npm run verify` runs the default maintainer verification sequence: lint, build, and unit tests
|
|
55
56
|
- Manual and debug-oriented device scripts live under `test/device/manual/...` and are not part of the default test commands
|
|
56
57
|
|
|
58
|
+
## Utility Scripts
|
|
59
|
+
|
|
60
|
+
- `npm run healthcheck` runs the `idb`/tooling healthcheck helper from `src/utils/cli/idb/check-idb.ts`
|
|
61
|
+
- `npm run install-idb` runs the guided `idb` installer helper from `src/utils/cli/idb/install-idb.ts`
|
|
62
|
+
- `npm run preflight-ios` runs the iOS preflight helper from `src/utils/cli/ios/preflight-ios.ts`
|
|
63
|
+
|
|
57
64
|
## Agent skills
|
|
58
65
|
|
|
59
66
|
- `skills/mcp-builder/` contains reusable build/install guidance for agents
|
package/dist/manage/android.js
CHANGED
|
@@ -2,7 +2,7 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
|
-
import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } from '../utils/android/utils.js';
|
|
5
|
+
import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk, prepareGradle } from '../utils/android/utils.js';
|
|
6
6
|
import { execAdbWithDiagnostics } from '../utils/diagnostics.js';
|
|
7
7
|
import { detectJavaHome } from '../utils/java.js';
|
|
8
8
|
import { AndroidObserve } from '../observe/android.js';
|
|
@@ -10,11 +10,15 @@ export class AndroidManage {
|
|
|
10
10
|
isTestOnlyInstallFailure(output) {
|
|
11
11
|
return typeof output === 'string' && output.includes('INSTALL_FAILED_TEST_ONLY');
|
|
12
12
|
}
|
|
13
|
-
async build(projectPath,
|
|
14
|
-
|
|
13
|
+
async build(projectPath, optionsOrVariant) {
|
|
14
|
+
const options = typeof optionsOrVariant === 'string' ? { variant: optionsOrVariant } : (optionsOrVariant || {});
|
|
15
15
|
try {
|
|
16
|
+
const env = {
|
|
17
|
+
...(options.env || {}),
|
|
18
|
+
...(options.variant ? { MCP_GRADLE_TASK: options.variant } : {})
|
|
19
|
+
};
|
|
16
20
|
// Always use the shared prepareGradle utility for consistent env/setup
|
|
17
|
-
const { execCmd, gradleArgs, spawnOpts } = await
|
|
21
|
+
const { execCmd, gradleArgs, spawnOpts } = await prepareGradle(projectPath, env);
|
|
18
22
|
await new Promise((resolve, reject) => {
|
|
19
23
|
const proc = spawn(execCmd, gradleArgs, spawnOpts);
|
|
20
24
|
let stderr = '';
|
|
@@ -44,7 +48,7 @@ export class AndroidManage {
|
|
|
44
48
|
const stat = await fs.stat(apkPath).catch(() => null);
|
|
45
49
|
if (stat && stat.isDirectory()) {
|
|
46
50
|
const detectedJavaHome = await detectJavaHome().catch(() => undefined);
|
|
47
|
-
const env =
|
|
51
|
+
const env = { ...process.env };
|
|
48
52
|
if (detectedJavaHome) {
|
|
49
53
|
if (env.JAVA_HOME !== detectedJavaHome) {
|
|
50
54
|
env.JAVA_HOME = detectedJavaHome;
|
package/dist/manage/index.js
CHANGED
|
@@ -70,31 +70,35 @@ export async function detectProjectPlatform(projectPath) {
|
|
|
70
70
|
return 'unknown';
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
function mergeDefinedEnv(...parts) {
|
|
74
|
+
const merged = {};
|
|
75
|
+
for (const part of parts) {
|
|
76
|
+
if (!part)
|
|
77
|
+
continue;
|
|
78
|
+
for (const [key, value] of Object.entries(part)) {
|
|
79
|
+
if (typeof value === 'undefined')
|
|
80
|
+
continue;
|
|
81
|
+
merged[key] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return merged;
|
|
85
|
+
}
|
|
73
86
|
export class ToolsManage {
|
|
74
87
|
static async build_android({ projectPath, gradleTask, maxWorkers, gradleCache, forceClean }) {
|
|
75
88
|
const android = new AndroidManage();
|
|
76
|
-
// prepare gradle options via environment hints
|
|
77
|
-
if (typeof maxWorkers === 'number')
|
|
78
|
-
process.env.MCP_GRADLE_WORKERS = String(maxWorkers);
|
|
79
|
-
if (typeof gradleCache === 'boolean')
|
|
80
|
-
process.env.MCP_GRADLE_CACHE = gradleCache ? '1' : '0';
|
|
81
|
-
if (forceClean)
|
|
82
|
-
process.env.MCP_FORCE_CLEAN_ANDROID = '1';
|
|
83
89
|
const task = gradleTask || 'assembleDebug';
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
return await android.build(projectPath, {
|
|
91
|
+
variant: task,
|
|
92
|
+
env: mergeDefinedEnv({
|
|
93
|
+
MCP_GRADLE_TASK: task,
|
|
94
|
+
MCP_GRADLE_WORKERS: typeof maxWorkers === 'number' ? String(maxWorkers) : undefined,
|
|
95
|
+
MCP_GRADLE_CACHE: typeof gradleCache === 'boolean' ? (gradleCache ? '1' : '0') : undefined,
|
|
96
|
+
MCP_FORCE_CLEAN_ANDROID: forceClean ? '1' : undefined
|
|
97
|
+
})
|
|
98
|
+
});
|
|
86
99
|
}
|
|
87
100
|
static async build_ios({ projectPath, workspace: _workspace, project: _project, scheme: _scheme, destinationUDID, derivedDataPath, buildJobs, forceClean }) {
|
|
88
101
|
const ios = new iOSManage();
|
|
89
|
-
// Use provided options rather than env-only; still set env fallbacks for downstream tools
|
|
90
|
-
if (derivedDataPath)
|
|
91
|
-
process.env.MCP_DERIVED_DATA = derivedDataPath;
|
|
92
|
-
if (typeof buildJobs === 'number')
|
|
93
|
-
process.env.MCP_BUILD_JOBS = String(buildJobs);
|
|
94
|
-
if (forceClean)
|
|
95
|
-
process.env.MCP_FORCE_CLEAN_IOS = '1';
|
|
96
|
-
if (destinationUDID)
|
|
97
|
-
process.env.MCP_XCODE_DESTINATION_UDID = destinationUDID;
|
|
98
102
|
const opts = {};
|
|
99
103
|
if (_workspace)
|
|
100
104
|
opts.workspace = _workspace;
|
|
@@ -106,13 +110,23 @@ export class ToolsManage {
|
|
|
106
110
|
opts.destinationUDID = destinationUDID;
|
|
107
111
|
if (derivedDataPath)
|
|
108
112
|
opts.derivedDataPath = derivedDataPath;
|
|
109
|
-
if (
|
|
113
|
+
if (typeof buildJobs === 'number')
|
|
114
|
+
opts.buildJobs = buildJobs;
|
|
115
|
+
if (typeof forceClean === 'boolean')
|
|
110
116
|
opts.forceClean = forceClean;
|
|
111
117
|
// prefer explicit xcodebuild path from env
|
|
112
118
|
if (process.env.XCODEBUILD_PATH)
|
|
113
119
|
opts.xcodeCmd = process.env.XCODEBUILD_PATH;
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
return await ios.build(projectPath, {
|
|
121
|
+
...opts,
|
|
122
|
+
env: mergeDefinedEnv({
|
|
123
|
+
MCP_DERIVED_DATA: derivedDataPath,
|
|
124
|
+
MCP_XCODE_JOBS: typeof buildJobs === 'number' ? String(buildJobs) : undefined,
|
|
125
|
+
MCP_FORCE_CLEAN: typeof forceClean === 'boolean' ? (forceClean ? '1' : '0') : undefined,
|
|
126
|
+
MCP_XCODE_DESTINATION_UDID: destinationUDID,
|
|
127
|
+
XCODEBUILD_PATH: process.env.XCODEBUILD_PATH
|
|
128
|
+
})
|
|
129
|
+
});
|
|
116
130
|
}
|
|
117
131
|
static async build_flutter({ projectPath, platform, buildMode, maxWorkers: _maxWorkers, forceClean: _forceClean }) {
|
|
118
132
|
// Prefer using flutter CLI when available; otherwise delegate to native subproject builders
|
|
@@ -199,12 +213,12 @@ export class ToolsManage {
|
|
|
199
213
|
const chosen = platform || 'android';
|
|
200
214
|
if (chosen === 'android') {
|
|
201
215
|
const android = new AndroidManage();
|
|
202
|
-
const artifact = await android.build(projectPath, variant);
|
|
216
|
+
const artifact = await android.build(projectPath, { variant });
|
|
203
217
|
return artifact;
|
|
204
218
|
}
|
|
205
219
|
else {
|
|
206
220
|
const ios = new iOSManage();
|
|
207
|
-
const artifact = await ios.build(projectPath, variant);
|
|
221
|
+
const artifact = await ios.build(projectPath, { variant });
|
|
208
222
|
return artifact;
|
|
209
223
|
}
|
|
210
224
|
}
|
package/dist/manage/ios.js
CHANGED
|
@@ -6,11 +6,8 @@ import path from "path";
|
|
|
6
6
|
export class iOSManage {
|
|
7
7
|
async build(projectPath, optsOrVariant) {
|
|
8
8
|
// Support legacy variant string as second arg
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
opts.variant = optsOrVariant;
|
|
12
|
-
else
|
|
13
|
-
opts = optsOrVariant || {};
|
|
9
|
+
const opts = typeof optsOrVariant === 'string' ? {} : (optsOrVariant || {});
|
|
10
|
+
const env = { ...process.env, ...(opts.env || {}) };
|
|
14
11
|
try {
|
|
15
12
|
// Look for an Xcode workspace or project at the provided path. If not present, scan subdirectories (limited depth)
|
|
16
13
|
async function findProject(root, maxDepth = 4) {
|
|
@@ -66,7 +63,7 @@ export class iOSManage {
|
|
|
66
63
|
proj = projectInfo.proj;
|
|
67
64
|
}
|
|
68
65
|
// Determine destination: prefer explicit option, then env var, otherwise use booted simulator UDID
|
|
69
|
-
let destinationUDID = opts.destinationUDID ||
|
|
66
|
+
let destinationUDID = opts.destinationUDID || env.MCP_XCODE_DESTINATION_UDID || env.MCP_XCODE_DESTINATION || '';
|
|
70
67
|
if (!destinationUDID) {
|
|
71
68
|
try {
|
|
72
69
|
const meta = await getIOSDeviceMetadata('booted');
|
|
@@ -76,7 +73,7 @@ export class iOSManage {
|
|
|
76
73
|
catch { }
|
|
77
74
|
}
|
|
78
75
|
// Determine xcode command early so it can be used when detecting schemes
|
|
79
|
-
const xcodeCmd = opts.xcodeCmd ||
|
|
76
|
+
const xcodeCmd = opts.xcodeCmd || env.XCODEBUILD_PATH || 'xcodebuild';
|
|
80
77
|
// Determine available schemes by querying xcodebuild -list rather than guessing
|
|
81
78
|
async function detectScheme(xcodeCmdInner, workspacePath, projectPathFull, cwd) {
|
|
82
79
|
try {
|
|
@@ -99,11 +96,11 @@ export class iOSManage {
|
|
|
99
96
|
let buildArgs;
|
|
100
97
|
let chosenScheme = opts.scheme || null;
|
|
101
98
|
// Derived data and result bundle (agent-configurable)
|
|
102
|
-
const derivedDataPath = opts.derivedDataPath ||
|
|
99
|
+
const derivedDataPath = opts.derivedDataPath || env.MCP_DERIVED_DATA || path.join(projectRootDir, 'build', 'DerivedData');
|
|
103
100
|
// Use unique result bundle path by default to avoid collisions
|
|
104
|
-
const resultBundlePath =
|
|
105
|
-
const xcodeJobs = parseInt(
|
|
106
|
-
const forceClean = opts.forceClean
|
|
101
|
+
const resultBundlePath = env.MCP_XCODE_RESULTBUNDLE_PATH || path.join(projectRootDir, 'build', 'xcresults', `ResultBundle-${Date.now()}-${Math.random().toString(36).slice(2)}.xcresult`);
|
|
102
|
+
const xcodeJobs = typeof opts.buildJobs === 'number' ? opts.buildJobs : (parseInt(env.MCP_XCODE_JOBS || '', 10) || 4);
|
|
103
|
+
const forceClean = typeof opts.forceClean === 'boolean' ? opts.forceClean : env.MCP_FORCE_CLEAN === '1';
|
|
107
104
|
// ensure result dirs exist
|
|
108
105
|
await fs.mkdir(path.dirname(resultBundlePath), { recursive: true }).catch(() => { });
|
|
109
106
|
await fs.mkdir(derivedDataPath, { recursive: true }).catch(() => { });
|
|
@@ -144,8 +141,8 @@ export class iOSManage {
|
|
|
144
141
|
// Remove any stale results to avoid xcodebuild complaining about existing result bundles
|
|
145
142
|
await fs.rm(resultsDir, { recursive: true, force: true }).catch(() => { });
|
|
146
143
|
await fs.mkdir(resultsDir, { recursive: true }).catch(() => { });
|
|
147
|
-
const XCODEBUILD_TIMEOUT = parseInt(
|
|
148
|
-
const MAX_RETRIES = parseInt(
|
|
144
|
+
const XCODEBUILD_TIMEOUT = parseInt(env.MCP_XCODEBUILD_TIMEOUT || '', 10) || 180000; // default 3 minutes
|
|
145
|
+
const MAX_RETRIES = parseInt(env.MCP_XCODEBUILD_RETRIES || '', 10) || 1;
|
|
149
146
|
const tries = MAX_RETRIES + 1;
|
|
150
147
|
let lastStdout = '';
|
|
151
148
|
let lastStderr = '';
|
|
@@ -153,7 +150,7 @@ export class iOSManage {
|
|
|
153
150
|
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
154
151
|
// Run xcodebuild with a watchdog
|
|
155
152
|
const res = await new Promise((resolve) => {
|
|
156
|
-
const proc = spawn(xcodeCmd, buildArgs, { cwd: projectRootDir });
|
|
153
|
+
const proc = spawn(xcodeCmd, buildArgs, { cwd: projectRootDir, env });
|
|
157
154
|
let stdout = '';
|
|
158
155
|
let stderr = '';
|
|
159
156
|
proc.stdout?.on('data', d => stdout += d.toString());
|
|
@@ -204,7 +201,7 @@ export class iOSManage {
|
|
|
204
201
|
if (lastErr) {
|
|
205
202
|
// Include diagnostics and result bundle path when available; provide structured info useful for agents
|
|
206
203
|
const invokedCommand = `${xcodeCmd} ${buildArgs.map(a => a.includes(' ') ? `"${a}"` : a).join(' ')}`;
|
|
207
|
-
const envSnapshot = { PATH:
|
|
204
|
+
const envSnapshot = { PATH: env.PATH };
|
|
208
205
|
return { error: `xcodebuild failed: ${lastErr.message}. See build-results for logs.`, output: `stdout:\n${lastStdout}\nstderr:\n${lastStderr}`, diagnostics: { exitCode: lastErr.code || null, invokedCommand, cwd: projectRootDir, envSnapshot } };
|
|
209
206
|
}
|
|
210
207
|
// Try to locate built .app. First search project tree, then DerivedData if necessary
|
package/dist/observe/index.js
CHANGED
|
@@ -66,7 +66,7 @@ function deriveSnapshotSemantic(raw) {
|
|
|
66
66
|
return null;
|
|
67
67
|
const hasErrorLogs = raw.logs.some((entry) => /error|fatal exception|exception|failed/i.test(entry.message));
|
|
68
68
|
const hasLoadingSignals = texts.some((text) => /loading|please wait|spinner|progress/i.test(text));
|
|
69
|
-
const hasPrimaryText = texts.some((text) => /sign in|log in|
|
|
69
|
+
const hasPrimaryText = texts.some((text) => /sign in|log in|login|home|checkout|settings|menu|profile|search/i.test(text));
|
|
70
70
|
const hasScreenshot = typeof raw.screenshot === 'string' && raw.screenshot.length > 0;
|
|
71
71
|
const hasUiTree = !!tree && Array.isArray(tree.elements);
|
|
72
72
|
const signals = {
|
|
@@ -105,7 +105,7 @@ function deriveSnapshotSemantic(raw) {
|
|
|
105
105
|
signals,
|
|
106
106
|
actions_available: actionables.length > 0 ? actionables.slice(0, 10) : null,
|
|
107
107
|
confidence,
|
|
108
|
-
warnings
|
|
108
|
+
warnings
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
export class ToolsObserve {
|
package/dist/server/common.js
CHANGED
|
@@ -7,6 +7,52 @@ export function wrapResponse(data) {
|
|
|
7
7
|
}]
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
|
+
export function getStringArg(args, key) {
|
|
11
|
+
const value = args[key];
|
|
12
|
+
return typeof value === 'string' ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
export function requireStringArg(args, key) {
|
|
15
|
+
const value = getStringArg(args, key);
|
|
16
|
+
if (value === undefined)
|
|
17
|
+
throw new Error(`Missing or invalid string argument: ${key}`);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
export function getNumberArg(args, key) {
|
|
21
|
+
const value = args[key];
|
|
22
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
export function requireNumberArg(args, key) {
|
|
25
|
+
const value = getNumberArg(args, key);
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
throw new Error(`Missing or invalid number argument: ${key}`);
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
export function getBooleanArg(args, key) {
|
|
31
|
+
const value = args[key];
|
|
32
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
export function requireBooleanArg(args, key) {
|
|
35
|
+
const value = getBooleanArg(args, key);
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
throw new Error(`Missing or invalid boolean argument: ${key}`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
export function getObjectArg(args, key) {
|
|
41
|
+
const value = args[key];
|
|
42
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
43
|
+
return undefined;
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
export function requireObjectArg(args, key) {
|
|
47
|
+
const value = getObjectArg(args, key);
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
throw new Error(`Missing or invalid object argument: ${key}`);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
export function getArrayArg(args, key) {
|
|
53
|
+
const value = args[key];
|
|
54
|
+
return Array.isArray(value) ? value : undefined;
|
|
55
|
+
}
|
|
10
56
|
let actionSequence = 0;
|
|
11
57
|
export function nextActionId(actionType, timestamp) {
|
|
12
58
|
actionSequence += 1;
|
|
@@ -4,9 +4,11 @@ import { ToolsObserve } from '../observe/index.js';
|
|
|
4
4
|
import { classifyActionOutcome } from '../interact/classify.js';
|
|
5
5
|
import { ToolsNetwork } from '../network/index.js';
|
|
6
6
|
import { getSystemStatus } from '../system/index.js';
|
|
7
|
-
import { buildActionExecutionResult, captureActionFingerprint, inferGenericFailure, inferScrollFailure, wrapResponse, wrapToolError } from './common.js';
|
|
7
|
+
import { buildActionExecutionResult, captureActionFingerprint, getArrayArg, getBooleanArg, getNumberArg, getObjectArg, getStringArg, inferGenericFailure, inferScrollFailure, requireBooleanArg, requireNumberArg, requireObjectArg, requireStringArg, wrapResponse, wrapToolError } from './common.js';
|
|
8
8
|
async function handleStartApp(args) {
|
|
9
|
-
const
|
|
9
|
+
const platform = requireStringArg(args, 'platform');
|
|
10
|
+
const appId = requireStringArg(args, 'appId');
|
|
11
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
10
12
|
const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId);
|
|
11
13
|
ToolsNetwork.notifyActionStart();
|
|
12
14
|
const res = await (platform === 'android' ? new AndroidManage().startApp(appId, deviceId) : new iOSManage().startApp(appId, deviceId));
|
|
@@ -29,13 +31,17 @@ async function handleStartApp(args) {
|
|
|
29
31
|
}));
|
|
30
32
|
}
|
|
31
33
|
async function handleTerminateApp(args) {
|
|
32
|
-
const
|
|
34
|
+
const platform = requireStringArg(args, 'platform');
|
|
35
|
+
const appId = requireStringArg(args, 'appId');
|
|
36
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
33
37
|
const res = await (platform === 'android' ? new AndroidManage().terminateApp(appId, deviceId) : new iOSManage().terminateApp(appId, deviceId));
|
|
34
38
|
const response = { device: res.device, appTerminated: res.appTerminated };
|
|
35
39
|
return wrapResponse(response);
|
|
36
40
|
}
|
|
37
41
|
async function handleRestartApp(args) {
|
|
38
|
-
const
|
|
42
|
+
const platform = requireStringArg(args, 'platform');
|
|
43
|
+
const appId = requireStringArg(args, 'appId');
|
|
44
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
39
45
|
const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId);
|
|
40
46
|
ToolsNetwork.notifyActionStart();
|
|
41
47
|
const res = await (platform === 'android' ? new AndroidManage().restartApp(appId, deviceId) : new iOSManage().restartApp(appId, deviceId));
|
|
@@ -59,13 +65,18 @@ async function handleRestartApp(args) {
|
|
|
59
65
|
}));
|
|
60
66
|
}
|
|
61
67
|
async function handleResetAppData(args) {
|
|
62
|
-
const
|
|
68
|
+
const platform = requireStringArg(args, 'platform');
|
|
69
|
+
const appId = requireStringArg(args, 'appId');
|
|
70
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
63
71
|
const res = await (platform === 'android' ? new AndroidManage().resetAppData(appId, deviceId) : new iOSManage().resetAppData(appId, deviceId));
|
|
64
72
|
const response = { device: res.device, dataCleared: res.dataCleared };
|
|
65
73
|
return wrapResponse(response);
|
|
66
74
|
}
|
|
67
75
|
async function handleInstallApp(args) {
|
|
68
|
-
const
|
|
76
|
+
const platform = requireStringArg(args, 'platform');
|
|
77
|
+
const projectType = requireStringArg(args, 'projectType');
|
|
78
|
+
const appPath = requireStringArg(args, 'appPath');
|
|
79
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
69
80
|
const res = await ToolsManage.installAppHandler({ platform, appPath, deviceId, projectType });
|
|
70
81
|
const response = {
|
|
71
82
|
device: res.device,
|
|
@@ -76,12 +87,19 @@ async function handleInstallApp(args) {
|
|
|
76
87
|
return wrapResponse(response);
|
|
77
88
|
}
|
|
78
89
|
async function handleBuildApp(args) {
|
|
79
|
-
const
|
|
90
|
+
const platform = requireStringArg(args, 'platform');
|
|
91
|
+
const projectType = requireStringArg(args, 'projectType');
|
|
92
|
+
const projectPath = requireStringArg(args, 'projectPath');
|
|
93
|
+
const variant = getStringArg(args, 'variant');
|
|
80
94
|
const res = await ToolsManage.buildAppHandler({ platform, projectPath, variant, projectType });
|
|
81
95
|
return wrapResponse(res);
|
|
82
96
|
}
|
|
83
97
|
async function handleBuildAndInstall(args) {
|
|
84
|
-
const
|
|
98
|
+
const platform = requireStringArg(args, 'platform');
|
|
99
|
+
const projectType = requireStringArg(args, 'projectType');
|
|
100
|
+
const projectPath = requireStringArg(args, 'projectPath');
|
|
101
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
102
|
+
const timeout = getNumberArg(args, 'timeout');
|
|
85
103
|
const res = await ToolsManage.buildAndInstallHandler({ platform, projectPath, deviceId, timeout, projectType });
|
|
86
104
|
return {
|
|
87
105
|
content: [
|
|
@@ -91,7 +109,16 @@ async function handleBuildAndInstall(args) {
|
|
|
91
109
|
};
|
|
92
110
|
}
|
|
93
111
|
async function handleGetLogs(args) {
|
|
94
|
-
const
|
|
112
|
+
const platform = requireStringArg(args, 'platform');
|
|
113
|
+
const appId = getStringArg(args, 'appId');
|
|
114
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
115
|
+
const pid = getNumberArg(args, 'pid');
|
|
116
|
+
const tag = getStringArg(args, 'tag');
|
|
117
|
+
const level = getStringArg(args, 'level');
|
|
118
|
+
const contains = getStringArg(args, 'contains');
|
|
119
|
+
const since_seconds = getNumberArg(args, 'since_seconds');
|
|
120
|
+
const limit = getNumberArg(args, 'limit');
|
|
121
|
+
const lines = getNumberArg(args, 'lines');
|
|
95
122
|
const res = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, pid, tag, level, contains, since_seconds, limit, lines });
|
|
96
123
|
const filtered = !!(pid || tag || level || contains || since_seconds || appId);
|
|
97
124
|
return {
|
|
@@ -102,7 +129,8 @@ async function handleGetLogs(args) {
|
|
|
102
129
|
};
|
|
103
130
|
}
|
|
104
131
|
async function handleListDevices(args) {
|
|
105
|
-
const
|
|
132
|
+
const platform = getStringArg(args, 'platform');
|
|
133
|
+
const appId = getStringArg(args, 'appId');
|
|
106
134
|
const res = await ToolsManage.listDevicesHandler({ platform, appId });
|
|
107
135
|
return wrapResponse(res);
|
|
108
136
|
}
|
|
@@ -111,7 +139,8 @@ async function handleGetSystemStatus() {
|
|
|
111
139
|
return wrapResponse(result);
|
|
112
140
|
}
|
|
113
141
|
async function handleCaptureScreenshot(args) {
|
|
114
|
-
const
|
|
142
|
+
const platform = requireStringArg(args, 'platform');
|
|
143
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
115
144
|
const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId });
|
|
116
145
|
const mime = res.screenshot_mime || 'image/png';
|
|
117
146
|
const content = [
|
|
@@ -125,52 +154,86 @@ async function handleCaptureScreenshot(args) {
|
|
|
125
154
|
return { content };
|
|
126
155
|
}
|
|
127
156
|
async function handleCaptureDebugSnapshot(args) {
|
|
128
|
-
const
|
|
157
|
+
const reason = getStringArg(args, 'reason');
|
|
158
|
+
const includeLogs = getBooleanArg(args, 'includeLogs');
|
|
159
|
+
const logLines = getNumberArg(args, 'logLines');
|
|
160
|
+
const platform = getStringArg(args, 'platform');
|
|
161
|
+
const appId = getStringArg(args, 'appId');
|
|
162
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
163
|
+
const sessionId = getStringArg(args, 'sessionId');
|
|
129
164
|
const res = await ToolsObserve.captureDebugSnapshotHandler({ reason, includeLogs, logLines, platform, appId, deviceId, sessionId });
|
|
130
165
|
return wrapResponse(res);
|
|
131
166
|
}
|
|
132
167
|
async function handleGetUITree(args) {
|
|
133
|
-
const
|
|
168
|
+
const platform = requireStringArg(args, 'platform');
|
|
169
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
134
170
|
const res = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
|
135
171
|
return wrapResponse(res);
|
|
136
172
|
}
|
|
137
173
|
async function handleGetCurrentScreen(args) {
|
|
138
|
-
const
|
|
174
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
139
175
|
const res = await ToolsObserve.getCurrentScreenHandler({ deviceId });
|
|
140
176
|
return wrapResponse(res);
|
|
141
177
|
}
|
|
142
178
|
async function handleGetScreenFingerprint(args) {
|
|
143
|
-
const
|
|
179
|
+
const platform = getStringArg(args, 'platform');
|
|
180
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
144
181
|
const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
145
182
|
return wrapResponse(res);
|
|
146
183
|
}
|
|
147
184
|
async function handleWaitForScreenChange(args) {
|
|
148
|
-
const
|
|
185
|
+
const platform = getStringArg(args, 'platform');
|
|
186
|
+
const previousFingerprint = requireStringArg(args, 'previousFingerprint');
|
|
187
|
+
const timeoutMs = getNumberArg(args, 'timeoutMs');
|
|
188
|
+
const pollIntervalMs = getNumberArg(args, 'pollIntervalMs');
|
|
189
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
149
190
|
const res = await ToolsInteract.waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId });
|
|
150
191
|
return wrapResponse(res);
|
|
151
192
|
}
|
|
152
193
|
async function handleExpectScreen(args) {
|
|
153
|
-
const
|
|
194
|
+
const platform = getStringArg(args, 'platform');
|
|
195
|
+
const fingerprint = getStringArg(args, 'fingerprint');
|
|
196
|
+
const screen = getStringArg(args, 'screen');
|
|
197
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
154
198
|
const res = await ToolsInteract.expectScreenHandler({ platform, fingerprint, screen, deviceId });
|
|
155
199
|
return wrapResponse(res);
|
|
156
200
|
}
|
|
157
201
|
async function handleExpectElementVisible(args) {
|
|
158
|
-
const
|
|
202
|
+
const selector = requireObjectArg(args, 'selector');
|
|
203
|
+
const element_id = getStringArg(args, 'element_id');
|
|
204
|
+
const timeout_ms = getNumberArg(args, 'timeout_ms');
|
|
205
|
+
const poll_interval_ms = getNumberArg(args, 'poll_interval_ms');
|
|
206
|
+
const platform = getStringArg(args, 'platform');
|
|
207
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
159
208
|
const res = await ToolsInteract.expectElementVisibleHandler({ selector, element_id, timeout_ms, poll_interval_ms, platform, deviceId });
|
|
160
209
|
return wrapResponse(res);
|
|
161
210
|
}
|
|
162
211
|
async function handleWaitForUI(args) {
|
|
163
|
-
const
|
|
212
|
+
const selector = getObjectArg(args, 'selector');
|
|
213
|
+
const condition = getStringArg(args, 'condition') ?? 'exists';
|
|
214
|
+
const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000;
|
|
215
|
+
const poll_interval_ms = getNumberArg(args, 'poll_interval_ms') ?? 300;
|
|
216
|
+
const match = getObjectArg(args, 'match');
|
|
217
|
+
const retry = getObjectArg(args, 'retry');
|
|
218
|
+
const platform = getStringArg(args, 'platform');
|
|
219
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
164
220
|
const res = await ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform, deviceId });
|
|
165
221
|
return wrapResponse(res);
|
|
166
222
|
}
|
|
167
223
|
async function handleFindElement(args) {
|
|
168
|
-
const
|
|
224
|
+
const query = requireStringArg(args, 'query');
|
|
225
|
+
const exact = getBooleanArg(args, 'exact') ?? false;
|
|
226
|
+
const timeoutMs = getNumberArg(args, 'timeoutMs') ?? 3000;
|
|
227
|
+
const platform = getStringArg(args, 'platform');
|
|
228
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
169
229
|
const res = await ToolsInteract.findElementHandler({ query, exact, timeoutMs, platform, deviceId });
|
|
170
230
|
return wrapResponse(res);
|
|
171
231
|
}
|
|
172
232
|
async function handleTap(args) {
|
|
173
|
-
const
|
|
233
|
+
const platform = getStringArg(args, 'platform');
|
|
234
|
+
const x = requireNumberArg(args, 'x');
|
|
235
|
+
const y = requireNumberArg(args, 'y');
|
|
236
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
174
237
|
const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId);
|
|
175
238
|
ToolsNetwork.notifyActionStart();
|
|
176
239
|
const res = await ToolsInteract.tapHandler({ platform, x, y, deviceId });
|
|
@@ -185,13 +248,19 @@ async function handleTap(args) {
|
|
|
185
248
|
}));
|
|
186
249
|
}
|
|
187
250
|
async function handleTapElement(args) {
|
|
188
|
-
const
|
|
251
|
+
const elementId = requireStringArg(args, 'elementId');
|
|
189
252
|
ToolsNetwork.notifyActionStart();
|
|
190
253
|
const res = await ToolsInteract.tapElementHandler({ elementId });
|
|
191
254
|
return wrapResponse(res);
|
|
192
255
|
}
|
|
193
256
|
async function handleSwipe(args) {
|
|
194
|
-
const
|
|
257
|
+
const platform = getStringArg(args, 'platform') ?? 'android';
|
|
258
|
+
const x1 = requireNumberArg(args, 'x1');
|
|
259
|
+
const y1 = requireNumberArg(args, 'y1');
|
|
260
|
+
const x2 = requireNumberArg(args, 'x2');
|
|
261
|
+
const y2 = requireNumberArg(args, 'y2');
|
|
262
|
+
const duration = requireNumberArg(args, 'duration');
|
|
263
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
195
264
|
const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId);
|
|
196
265
|
ToolsNetwork.notifyActionStart();
|
|
197
266
|
const res = await ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId });
|
|
@@ -206,14 +275,19 @@ async function handleSwipe(args) {
|
|
|
206
275
|
}));
|
|
207
276
|
}
|
|
208
277
|
async function handleScrollToElement(args) {
|
|
209
|
-
const
|
|
278
|
+
const platform = requireStringArg(args, 'platform');
|
|
279
|
+
const selector = requireObjectArg(args, 'selector');
|
|
280
|
+
const direction = getStringArg(args, 'direction');
|
|
281
|
+
const maxScrolls = getNumberArg(args, 'maxScrolls');
|
|
282
|
+
const scrollAmount = getNumberArg(args, 'scrollAmount');
|
|
283
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
210
284
|
const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId);
|
|
211
285
|
ToolsNetwork.notifyActionStart();
|
|
212
286
|
const res = await ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId });
|
|
213
287
|
const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId);
|
|
214
288
|
return wrapResponse(buildActionExecutionResult({
|
|
215
289
|
actionType: 'scroll_to_element',
|
|
216
|
-
selector,
|
|
290
|
+
selector: selector ?? null,
|
|
217
291
|
resolved: res?.success && res?.element ? {
|
|
218
292
|
elementId: null,
|
|
219
293
|
text: res.element.text ?? null,
|
|
@@ -230,7 +304,8 @@ async function handleScrollToElement(args) {
|
|
|
230
304
|
}));
|
|
231
305
|
}
|
|
232
306
|
async function handleTypeText(args) {
|
|
233
|
-
const
|
|
307
|
+
const text = requireStringArg(args, 'text');
|
|
308
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
234
309
|
const uiFingerprintBefore = await captureActionFingerprint('android', deviceId);
|
|
235
310
|
ToolsNetwork.notifyActionStart();
|
|
236
311
|
const res = await ToolsInteract.typeTextHandler({ text, deviceId });
|
|
@@ -245,7 +320,7 @@ async function handleTypeText(args) {
|
|
|
245
320
|
}));
|
|
246
321
|
}
|
|
247
322
|
async function handlePressBack(args) {
|
|
248
|
-
const
|
|
323
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
249
324
|
const uiFingerprintBefore = await captureActionFingerprint('android', deviceId);
|
|
250
325
|
ToolsNetwork.notifyActionStart();
|
|
251
326
|
const res = await ToolsInteract.pressBackHandler({ deviceId });
|
|
@@ -260,24 +335,35 @@ async function handlePressBack(args) {
|
|
|
260
335
|
}));
|
|
261
336
|
}
|
|
262
337
|
async function handleStartLogStream(args) {
|
|
263
|
-
const
|
|
338
|
+
const platform = getStringArg(args, 'platform') ?? 'android';
|
|
339
|
+
const packageName = requireStringArg(args, 'packageName');
|
|
340
|
+
const level = getStringArg(args, 'level') ?? 'error';
|
|
341
|
+
const sessionId = getStringArg(args, 'sessionId');
|
|
342
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
264
343
|
const res = await ToolsObserve.startLogStreamHandler({ platform, packageName, level, sessionId, deviceId });
|
|
265
344
|
return wrapResponse(res);
|
|
266
345
|
}
|
|
267
346
|
async function handleReadLogStream(args) {
|
|
268
|
-
const
|
|
347
|
+
const platform = getStringArg(args, 'platform');
|
|
348
|
+
const sessionId = getStringArg(args, 'sessionId');
|
|
349
|
+
const limit = getNumberArg(args, 'limit');
|
|
350
|
+
const since = getStringArg(args, 'since');
|
|
269
351
|
const res = await ToolsObserve.readLogStreamHandler({ platform, sessionId, limit, since });
|
|
270
352
|
return wrapResponse(res);
|
|
271
353
|
}
|
|
272
354
|
async function handleStopLogStream(args) {
|
|
273
|
-
const
|
|
355
|
+
const platform = getStringArg(args, 'platform');
|
|
356
|
+
const sessionId = getStringArg(args, 'sessionId');
|
|
274
357
|
const res = await ToolsObserve.stopLogStreamHandler({ platform, sessionId });
|
|
275
358
|
return wrapResponse(res);
|
|
276
359
|
}
|
|
277
360
|
function handleClassifyActionOutcome(args) {
|
|
278
|
-
const
|
|
361
|
+
const uiChanged = requireBooleanArg(args, 'uiChanged');
|
|
362
|
+
const expectedElementVisible = getBooleanArg(args, 'expectedElementVisible');
|
|
363
|
+
const networkRequests = getArrayArg(args, 'networkRequests');
|
|
364
|
+
const hasLogErrors = getBooleanArg(args, 'hasLogErrors');
|
|
279
365
|
const result = classifyActionOutcome({
|
|
280
|
-
uiChanged
|
|
366
|
+
uiChanged,
|
|
281
367
|
expectedElementVisible: expectedElementVisible ?? null,
|
|
282
368
|
networkRequests: networkRequests ?? null,
|
|
283
369
|
hasLogErrors: hasLogErrors ?? null
|
|
@@ -285,7 +371,8 @@ function handleClassifyActionOutcome(args) {
|
|
|
285
371
|
return Promise.resolve(wrapResponse(result));
|
|
286
372
|
}
|
|
287
373
|
async function handleGetNetworkActivity(args) {
|
|
288
|
-
const
|
|
374
|
+
const platform = requireStringArg(args, 'platform');
|
|
375
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
289
376
|
const result = await ToolsNetwork.getNetworkActivity({ platform, deviceId });
|
|
290
377
|
return wrapResponse(result);
|
|
291
378
|
}
|
package/dist/server-core.js
CHANGED
|
@@ -6,7 +6,7 @@ import { handleToolCall } from './server/tool-handlers.js';
|
|
|
6
6
|
export { wrapResponse, toolDefinitions, handleToolCall };
|
|
7
7
|
export const serverInfo = {
|
|
8
8
|
name: 'mobile-debug-mcp',
|
|
9
|
-
version: '0.7
|
|
9
|
+
version: '0.24.7'
|
|
10
10
|
};
|
|
11
11
|
export function createServer() {
|
|
12
12
|
const server = new Server(serverInfo, {
|