mobile-debug-mcp 0.24.6 → 0.24.8

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 (48) hide show
  1. package/.github/workflows/ci.yml +1 -3
  2. package/README.md +12 -5
  3. package/dist/interact/index.js +89 -3
  4. package/dist/manage/android.js +9 -5
  5. package/dist/manage/index.js +37 -23
  6. package/dist/manage/ios.js +12 -15
  7. package/dist/server/common.js +46 -0
  8. package/dist/server/tool-handlers.js +120 -33
  9. package/dist/server-core.js +1 -1
  10. package/dist/utils/android/utils.js +17 -5
  11. package/dist/utils/cli/idb/check-idb.js +1 -1
  12. package/docs/CHANGELOG.md +18 -10
  13. package/eslint.config.js +2 -47
  14. package/package.json +7 -6
  15. package/src/interact/index.ts +112 -4
  16. package/src/manage/android.ts +22 -11
  17. package/src/manage/index.ts +37 -16
  18. package/src/manage/ios.ts +28 -15
  19. package/src/server/common.ts +50 -0
  20. package/src/server/tool-handlers.ts +136 -32
  21. package/src/server-core.ts +1 -1
  22. package/src/utils/android/utils.ts +18 -7
  23. package/src/utils/cli/idb/check-idb.ts +1 -1
  24. package/test/device/automated/observe/capture_screenshot.android.smoke.ts +1 -1
  25. package/test/device/automated/observe/capture_screenshot.ios.smoke.ts +1 -1
  26. package/test/device/automated/observe/get_logs.android.smoke.ts +1 -1
  27. package/test/device/automated/observe/get_logs.ios.smoke.ts +1 -1
  28. package/test/device/automated/observe/get_ui_tree.android.smoke.ts +1 -1
  29. package/test/device/automated/observe/get_ui_tree.ios.smoke.ts +1 -1
  30. package/test/device/manual/interact/app_lifecycle.manual.ts +3 -3
  31. package/test/device/manual/observe/capture_screenshot.manual.ts +2 -2
  32. package/test/device/manual/observe/get_logs.manual.ts +2 -2
  33. package/test/device/manual/observe/get_ui_tree.manual.ts +2 -2
  34. package/test/device/manual/observe/logstream.manual.ts +1 -1
  35. package/test/device/manual/observe/screen_fingerprint.manual.ts +2 -2
  36. package/test/unit/manage/scoped_env.test.ts +137 -0
  37. package/test/unit/observe/find_element.test.ts +64 -5
  38. package/test/unit/server/capture_screenshot.test.ts +17 -0
  39. package/test/unit/server/common.test.ts +18 -0
  40. package/test/unit/server/contract.test.ts +3 -0
  41. package/test/unit/server/get_logs.test.ts +17 -0
  42. package/test/unit/server/get_network_activity.test.ts +17 -0
  43. package/test/unit/server/get_ui_tree.test.ts +17 -0
  44. package/test/unit/server/response_shapes.test.ts +18 -0
  45. package/test/unit/server/start_log_stream.test.ts +37 -0
  46. package/.eslintignore +0 -5
  47. package/.eslintrc.cjs +0 -18
  48. package/eslint.config.cjs +0 -36
@@ -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.8'
10
10
  };
11
11
  export function createServer() {
12
12
  const server = new Server(serverInfo, {
@@ -57,23 +57,36 @@ export function ensureAdbAvailable() {
57
57
  return { adbCmd: adb, ok: false, error: String(err) };
58
58
  }
59
59
  }
60
+ function mergeEnv(overrides) {
61
+ const env = {};
62
+ for (const [key, value] of Object.entries(process.env)) {
63
+ if (typeof value === 'string')
64
+ env[key] = value;
65
+ }
66
+ for (const [key, value] of Object.entries(overrides || {})) {
67
+ if (typeof value === 'string')
68
+ env[key] = value;
69
+ }
70
+ return env;
71
+ }
60
72
  /**
61
73
  * Prepare Gradle execution options for building an Android project.
62
74
  * Returns execCmd (wrapper or gradle), base gradleArgs array, and spawn options including env.
63
75
  */
64
- export async function prepareGradle(projectPath) {
76
+ export async function prepareGradle(projectPath, envOverrides = {}) {
77
+ const env = mergeEnv(envOverrides);
65
78
  const gradlewPath = path.join(projectPath, 'gradlew');
66
79
  const gradleCmd = existsSync(gradlewPath) ? './gradlew' : 'gradle';
67
80
  const execCmd = existsSync(gradlewPath) ? gradlewPath : gradleCmd;
68
81
  // Start with a default task; callers may append/override via env flags
69
- const gradleArgs = [process.env.MCP_GRADLE_TASK || 'assembleDebug'];
82
+ const gradleArgs = [env.MCP_GRADLE_TASK || 'assembleDebug'];
70
83
  // Respect generic MCP_BUILD_JOBS and Android-specific MCP_GRADLE_WORKERS
71
- const workers = process.env.MCP_GRADLE_WORKERS || process.env.MCP_BUILD_JOBS;
84
+ const workers = env.MCP_GRADLE_WORKERS || env.MCP_BUILD_JOBS;
72
85
  if (workers) {
73
86
  gradleArgs.push(`--max-workers=${workers}`);
74
87
  }
75
88
  // Respect gradle cache env: default enabled; set MCP_GRADLE_CACHE=0 to disable
76
- if (process.env.MCP_GRADLE_CACHE === '0') {
89
+ if (env.MCP_GRADLE_CACHE === '0') {
77
90
  gradleArgs.push('-Dorg.gradle.caching=false');
78
91
  }
79
92
  const detectedJavaHome = await detectJavaHome().catch(() => undefined);
@@ -85,7 +98,6 @@ export async function prepareGradle(projectPath) {
85
98
  catch {
86
99
  gradleCheck = { gradleJavaHome: undefined, gradleValid: false, filesChecked: [], issues: [] };
87
100
  }
88
- const env = Object.assign({}, process.env);
89
101
  // Ensure child processes can find Android platform-tools (adb, etc.) by
90
102
  // prepending the platform-tools directory to PATH for spawned processes.
91
103
  const adbPath = resolveAdbCmd();
@@ -24,7 +24,7 @@ async function runInstaller() {
24
24
  // prefer invoking the TS script via npx/tsx to ensure environment
25
25
  const runner = which('npx') ? 'npx' : which('tsx') ? 'tsx' : null;
26
26
  if (runner) {
27
- const args = runner === 'npx' ? ['tsx', './src/cli/idb/install-idb.ts'] : ['./src/cli/idb/install-idb.ts'];
27
+ const args = runner === 'npx' ? ['tsx', './src/utils/cli/idb/install-idb.ts'] : ['./src/utils/cli/idb/install-idb.ts'];
28
28
  const res = spawnSync(runner, args, { stdio: 'inherit' });
29
29
  return typeof res.status === 'number' ? res.status === 0 : false;
30
30
  }
package/docs/CHANGELOG.md CHANGED
@@ -2,8 +2,16 @@
2
2
 
3
3
  All notable changes to the **Mobile Debug MCP** project will be documented in this file.
4
4
 
5
- ## [0.24.6]
6
- - minor changes
5
+ ## [0.24.8]
6
+ - Improved slider interaction
7
+
8
+ ## [0.24.7]
9
+ - Aligned runtime metadata with the published package version.
10
+ - Fixed stale CLI helper paths in npm scripts and the `idb` healthcheck helper.
11
+ - Simplified ESLint configuration by keeping the flat config and removing legacy config files.
12
+ - Updated CI to use the current automated device test runner.
13
+ - Tightened server handler argument parsing and added contract coverage for version and required-argument error responses.
14
+ - Scoped temporary build environment overrides to the duration of each build helper call and added regression coverage for env restoration.
7
15
 
8
16
  ## [0.24.5]
9
17
  - Improved snapshots
@@ -27,7 +35,7 @@ All notable changes to the **Mobile Debug MCP** project will be documented in th
27
35
 
28
36
  ## [0.23.0]
29
37
  - Added network monitoring
30
- - Added
38
+ - Added action-outcome classification tooling for backend-driven flows without visible UI changes.
31
39
 
32
40
  ## [0.22.0]
33
41
  - Added a portable `test-authoring` skill package and documented the repository's vendor-neutral skill format
@@ -38,14 +46,14 @@ All notable changes to the **Mobile Debug MCP** project will be documented in th
38
46
  - Fixed incorrect timeout
39
47
 
40
48
  ## [0.21.4]
41
- - updated `wait_for_ui` with better contract and observability
42
- - update `get_logs` to get better output
49
+ - Updated `wait_for_ui` with better contract and observability
50
+ - Updated `get_logs` to return more useful structured output
43
51
 
44
52
  ## [0.21.3]
45
53
  - Added structured logs
46
54
 
47
55
  ## [0.21.2]
48
- - Fixed screenshots not working, imnproved tool
56
+ - Fixed screenshots not working and improved the tool output
49
57
 
50
58
  ## [0.21.1]
51
59
  - Removed wait_for_element and renamed observe_until to wait_for_ui (obsolete references removed)
@@ -54,7 +62,7 @@ All notable changes to the **Mobile Debug MCP** project will be documented in th
54
62
  - Added `wait_for_ui` as a tool for agents to wait for things like API requests
55
63
 
56
64
  ## [0.20.1]
57
- - Fixes gradle home issue for android
65
+ - Fixed Gradle home handling for Android
58
66
 
59
67
  ## [0.20.0]
60
68
  - Added `get_system_status` tool and refactored system health checks into `src/system`.
@@ -63,8 +71,8 @@ All notable changes to the **Mobile Debug MCP** project will be documented in th
63
71
 
64
72
 
65
73
  ## [0.19.2]
66
- - Added healthcheck improvments
67
- - Added skills
74
+ - Added healthcheck improvements
75
+ - Added reusable agent skills
68
76
 
69
77
  ## [0.19.1]
70
78
 
@@ -113,7 +121,7 @@ All notable changes to the **Mobile Debug MCP** project will be documented in th
113
121
  ## [0.12.1]
114
122
  - Improve iOS build/install reliability: project auto-scan, explicit simulator destination, configurable watchdog timeout (MCP_XCODEBUILD_TIMEOUT) and retries (MCP_XCODEBUILD_RETRIES), and DerivedData fallback for locating .app artifacts.
115
123
  - Make install_app capable of building iOS projects before installing so agents can autonomously fix, build, install and validate apps.
116
- - Migrate CLI scripts into typed src/cli/* modules and update npm scripts; fix ESM import paths and lint issues.
124
+ - Migrate CLI scripts into typed source modules and update npm scripts; fix ESM import paths and lint issues.
117
125
  - Add preflight checks and idb resolution helpers (getIdbCmd, isIDBInstalled) and add idb_companion health checks.
118
126
  - Capture build stdout/stderr into build-results/ for easier diagnostics and surfaced suggestions when KMP frameworks are missing.
119
127
  - Add device test runner under test/device and gate device-dependent tests behind RUN_DEVICE_TESTS.
package/eslint.config.js CHANGED
@@ -3,7 +3,6 @@ import tsPlugin from '@typescript-eslint/eslint-plugin'
3
3
  import unusedImports from 'eslint-plugin-unused-imports'
4
4
 
5
5
  export default [
6
- // Files/directories to ignore
7
6
  {
8
7
  ignores: [
9
8
  'dist/',
@@ -11,55 +10,11 @@ export default [
11
10
  '.git/',
12
11
  '.vscode/',
13
12
  'coverage/',
14
- '.env',
13
+ '.env'
15
14
  ]
16
15
  },
17
- // Apply rules to JS/TS source
18
16
  {
19
- files: ['src/**/*.ts', 'src/**/*.js'],
20
- languageOptions: {
21
- parser: tsParser,
22
- parserOptions: {
23
- ecmaVersion: 2020,
24
- sourceType: 'module',
25
- project: './tsconfig.json'
26
- }
27
- },
28
- plugins: {
29
- '@typescript-eslint': tsPlugin,
30
- 'unused-imports': unusedImports
31
- },
32
- rules: {
33
- // Use plugin to error on unused imports and provide autofix where possible
34
- 'unused-imports/no-unused-imports': 'error',
35
- 'unused-imports/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
36
- // Disable the default TS rule to avoid duplicate warnings
37
- '@typescript-eslint/no-unused-vars': 'off'
38
- }
39
- },
40
- // Apply lighter rules to test files (no project reference to avoid TS project parsing)
41
- {
42
- files: ['test/**/*.ts', 'test/**/*.js'],
43
- languageOptions: {
44
- parser: tsParser,
45
- parserOptions: {
46
- ecmaVersion: 2020,
47
- sourceType: 'module'
48
- }
49
- },
50
- plugins: {
51
- '@typescript-eslint': tsPlugin,
52
- 'unused-imports': unusedImports
53
- },
54
- rules: {
55
- 'unused-imports/no-unused-imports': 'error',
56
- 'unused-imports/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
57
- '@typescript-eslint/no-unused-vars': 'off'
58
- }
59
- },
60
- // Apply rules to CLI tooling
61
- {
62
- files: ['src/cli/**/*.ts', 'src/cli/**/*.js'],
17
+ files: ['src/**/*.ts', 'src/**/*.js', 'test/**/*.ts', 'test/**/*.js'],
63
18
  languageOptions: {
64
19
  parser: tsParser,
65
20
  parserOptions: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-debug-mcp",
3
- "version": "0.24.6",
3
+ "version": "0.24.8",
4
4
  "description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,15 +10,16 @@
10
10
  "build": "tsc",
11
11
  "start": "node ./dist/server.js",
12
12
  "prepare": "npm run build",
13
- "healthcheck": "tsx ./src/cli/idb/check-idb.ts",
14
- "install-idb": "tsx ./src/cli/idb/install-idb.ts",
15
- "preflight-ios": "tsx ./src/cli/ios/preflight-ios.ts",
13
+ "healthcheck": "tsx ./src/utils/cli/idb/check-idb.ts",
14
+ "install-idb": "tsx ./src/utils/cli/idb/install-idb.ts",
15
+ "preflight-ios": "tsx ./src/utils/cli/ios/preflight-ios.ts",
16
16
  "test:unit": "tsx test/unit/index.ts",
17
17
  "test:integration": "npm run test:device",
18
18
  "test:device": "npm run build && tsx test/device/index.ts",
19
19
  "test": "npm run test:unit",
20
- "lint": "eslint --ext .ts,.js src test --quiet",
21
- "lint:fix": "eslint --ext .ts,.js src test --fix"
20
+ "lint": "eslint src test --quiet",
21
+ "lint:fix": "eslint src test --fix",
22
+ "verify": "npm run lint && npm run build && npm run test:unit"
22
23
  },
23
24
 
24
25
  "engines": {