mobile-debug-mcp 0.24.6 → 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.
Files changed (45) hide show
  1. package/.github/workflows/ci.yml +1 -3
  2. package/README.md +7 -0
  3. package/dist/manage/android.js +9 -5
  4. package/dist/manage/index.js +37 -23
  5. package/dist/manage/ios.js +12 -15
  6. package/dist/server/common.js +46 -0
  7. package/dist/server/tool-handlers.js +120 -33
  8. package/dist/server-core.js +1 -1
  9. package/dist/utils/android/utils.js +17 -5
  10. package/dist/utils/cli/idb/check-idb.js +1 -1
  11. package/docs/CHANGELOG.md +15 -10
  12. package/eslint.config.js +2 -47
  13. package/package.json +7 -6
  14. package/src/manage/android.ts +22 -11
  15. package/src/manage/index.ts +37 -16
  16. package/src/manage/ios.ts +28 -15
  17. package/src/server/common.ts +50 -0
  18. package/src/server/tool-handlers.ts +136 -32
  19. package/src/server-core.ts +1 -1
  20. package/src/utils/android/utils.ts +18 -7
  21. package/src/utils/cli/idb/check-idb.ts +1 -1
  22. package/test/device/automated/observe/capture_screenshot.android.smoke.ts +1 -1
  23. package/test/device/automated/observe/capture_screenshot.ios.smoke.ts +1 -1
  24. package/test/device/automated/observe/get_logs.android.smoke.ts +1 -1
  25. package/test/device/automated/observe/get_logs.ios.smoke.ts +1 -1
  26. package/test/device/automated/observe/get_ui_tree.android.smoke.ts +1 -1
  27. package/test/device/automated/observe/get_ui_tree.ios.smoke.ts +1 -1
  28. package/test/device/manual/interact/app_lifecycle.manual.ts +3 -3
  29. package/test/device/manual/observe/capture_screenshot.manual.ts +2 -2
  30. package/test/device/manual/observe/get_logs.manual.ts +2 -2
  31. package/test/device/manual/observe/get_ui_tree.manual.ts +2 -2
  32. package/test/device/manual/observe/logstream.manual.ts +1 -1
  33. package/test/device/manual/observe/screen_fingerprint.manual.ts +2 -2
  34. package/test/unit/manage/scoped_env.test.ts +137 -0
  35. package/test/unit/server/capture_screenshot.test.ts +17 -0
  36. package/test/unit/server/common.test.ts +18 -0
  37. package/test/unit/server/contract.test.ts +3 -0
  38. package/test/unit/server/get_logs.test.ts +17 -0
  39. package/test/unit/server/get_network_activity.test.ts +17 -0
  40. package/test/unit/server/get_ui_tree.test.ts +17 -0
  41. package/test/unit/server/response_shapes.test.ts +18 -0
  42. package/test/unit/server/start_log_stream.test.ts +37 -0
  43. package/.eslintignore +0 -5
  44. package/.eslintrc.cjs +0 -18
  45. package/eslint.config.cjs +0 -36
@@ -58,6 +58,4 @@ jobs:
58
58
  - name: Build and run Android integration tests
59
59
  env:
60
60
  ADB_TIMEOUT: 120000
61
- run: |
62
- npm run build
63
- node test/integration/run-install-android.js || true
61
+ run: npm run test:device
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
@@ -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, _variant) {
14
- void _variant;
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 (await import('../utils/android/utils.js')).prepareGradle(projectPath);
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 = Object.assign({}, process.env);
51
+ const env = { ...process.env };
48
52
  if (detectedJavaHome) {
49
53
  if (env.JAVA_HOME !== detectedJavaHome) {
50
54
  env.JAVA_HOME = detectedJavaHome;
@@ -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
- const artifact = await android.build(projectPath, task);
85
- return artifact;
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 (forceClean)
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
- const artifact = await ios.build(projectPath, opts);
115
- return artifact;
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
  }
@@ -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
- let opts = {};
10
- if (typeof optsOrVariant === 'string')
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 || process.env.MCP_XCODE_DESTINATION_UDID || process.env.MCP_XCODE_DESTINATION || '';
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 || process.env.XCODEBUILD_PATH || 'xcodebuild';
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 || process.env.MCP_DERIVED_DATA || path.join(projectRootDir, 'build', 'DerivedData');
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 = process.env.MCP_XCODE_RESULTBUNDLE_PATH || path.join(projectRootDir, 'build', 'xcresults', `ResultBundle-${Date.now()}-${Math.random().toString(36).slice(2)}.xcresult`);
105
- const xcodeJobs = parseInt(process.env.MCP_XCODE_JOBS || '', 10) || 4;
106
- const forceClean = opts.forceClean || process.env.MCP_FORCE_CLEAN === '1';
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(process.env.MCP_XCODEBUILD_TIMEOUT || '', 10) || 180000; // default 3 minutes
148
- const MAX_RETRIES = parseInt(process.env.MCP_XCODEBUILD_RETRIES || '', 10) || 1;
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: process.env.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
@@ -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 { platform, appId, deviceId } = args;
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 { platform, appId, deviceId } = args;
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 { platform, appId, deviceId } = args;
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 { platform, appId, deviceId } = args;
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 { platform, projectType, appPath, deviceId } = args;
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 { platform, projectType, projectPath, variant } = args;
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 { platform, projectType, projectPath, deviceId, timeout } = args;
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 { platform, appId, deviceId, pid, tag, level, contains, since_seconds, limit, lines } = args;
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 { platform, appId } = args;
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 { platform, deviceId } = args;
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 { reason, includeLogs, logLines, platform, appId, deviceId, sessionId } = args;
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 { platform, deviceId } = args;
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 { deviceId } = args;
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 { platform, deviceId } = args;
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 { platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId } = args;
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 { platform, fingerprint, screen, deviceId } = args;
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 { selector, element_id, timeout_ms, poll_interval_ms, platform, deviceId } = args;
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 { selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry, platform, deviceId } = args;
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 { query, exact = false, timeoutMs = 3000, platform, deviceId } = args;
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 { platform, x, y, deviceId } = args;
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 { elementId } = args;
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 { platform = 'android', x1, y1, x2, y2, duration, deviceId } = args;
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 { platform, selector, direction, maxScrolls, scrollAmount, deviceId } = args;
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 { text, deviceId } = args;
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 { deviceId } = args;
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 { platform, packageName, level, sessionId, deviceId } = args;
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 { platform, sessionId, limit, since } = args;
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 { platform, sessionId } = args;
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 { uiChanged, expectedElementVisible, networkRequests, hasLogErrors } = args;
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: Boolean(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 { platform, deviceId } = args;
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
  }
@@ -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.0'
9
+ version: '0.24.7'
10
10
  };
11
11
  export function createServer() {
12
12
  const server = new Server(serverInfo, {