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
@@ -12,16 +12,35 @@ import { getSystemStatus } from '../system/index.js'
12
12
  import {
13
13
  buildActionExecutionResult,
14
14
  captureActionFingerprint,
15
+ getArrayArg,
16
+ getBooleanArg,
17
+ getNumberArg,
18
+ getObjectArg,
19
+ getStringArg,
15
20
  inferGenericFailure,
16
21
  inferScrollFailure,
22
+ requireBooleanArg,
23
+ requireNumberArg,
24
+ requireObjectArg,
25
+ requireStringArg,
17
26
  ToolCallArgs,
18
27
  ToolHandler,
19
28
  wrapResponse,
20
29
  wrapToolError
21
30
  } from './common.js'
22
31
 
32
+ type PlatformArg = 'android' | 'ios'
33
+ type ProjectTypeArg = 'native' | 'kmp' | 'react-native' | 'flutter'
34
+ type ExpectElementSelectorArg = { text?: string, resource_id?: string, accessibility_id?: string, contains?: boolean }
35
+ type WaitForUiMatchArg = { index?: number }
36
+ type WaitForUiRetryArg = { max_attempts?: number, backoff_ms?: number }
37
+ type ScrollSelectorArg = { text?: string, resourceId?: string, contentDesc?: string, className?: string }
38
+ type ClassifyNetworkRequestArg = { endpoint: string, status: 'success' | 'failure' | 'retryable' }
39
+
23
40
  async function handleStartApp(args: ToolCallArgs) {
24
- const { platform, appId, deviceId } = args as any
41
+ const platform = requireStringArg(args, 'platform') as PlatformArg
42
+ const appId = requireStringArg(args, 'appId')
43
+ const deviceId = getStringArg(args, 'deviceId')
25
44
  const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId)
26
45
  ToolsNetwork.notifyActionStart()
27
46
  const res = await (platform === 'android' ? new AndroidManage().startApp(appId, deviceId) : new iOSManage().startApp(appId, deviceId))
@@ -45,14 +64,18 @@ async function handleStartApp(args: ToolCallArgs) {
45
64
  }
46
65
 
47
66
  async function handleTerminateApp(args: ToolCallArgs) {
48
- const { platform, appId, deviceId } = args as any
67
+ const platform = requireStringArg(args, 'platform') as PlatformArg
68
+ const appId = requireStringArg(args, 'appId')
69
+ const deviceId = getStringArg(args, 'deviceId')
49
70
  const res = await (platform === 'android' ? new AndroidManage().terminateApp(appId, deviceId) : new iOSManage().terminateApp(appId, deviceId))
50
71
  const response: TerminateAppResponse = { device: res.device, appTerminated: res.appTerminated }
51
72
  return wrapResponse(response)
52
73
  }
53
74
 
54
75
  async function handleRestartApp(args: ToolCallArgs) {
55
- const { platform, appId, deviceId } = args as any
76
+ const platform = requireStringArg(args, 'platform') as PlatformArg
77
+ const appId = requireStringArg(args, 'appId')
78
+ const deviceId = getStringArg(args, 'deviceId')
56
79
  const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId)
57
80
  ToolsNetwork.notifyActionStart()
58
81
  const res = await (platform === 'android' ? new AndroidManage().restartApp(appId, deviceId) : new iOSManage().restartApp(appId, deviceId))
@@ -77,14 +100,19 @@ async function handleRestartApp(args: ToolCallArgs) {
77
100
  }
78
101
 
79
102
  async function handleResetAppData(args: ToolCallArgs) {
80
- const { platform, appId, deviceId } = args as any
103
+ const platform = requireStringArg(args, 'platform') as PlatformArg
104
+ const appId = requireStringArg(args, 'appId')
105
+ const deviceId = getStringArg(args, 'deviceId')
81
106
  const res = await (platform === 'android' ? new AndroidManage().resetAppData(appId, deviceId) : new iOSManage().resetAppData(appId, deviceId))
82
107
  const response: ResetAppDataResponse = { device: res.device, dataCleared: res.dataCleared }
83
108
  return wrapResponse(response)
84
109
  }
85
110
 
86
111
  async function handleInstallApp(args: ToolCallArgs) {
87
- const { platform, projectType, appPath, deviceId } = args as any
112
+ const platform = requireStringArg(args, 'platform') as PlatformArg
113
+ const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
114
+ const appPath = requireStringArg(args, 'appPath')
115
+ const deviceId = getStringArg(args, 'deviceId')
88
116
  const res = await ToolsManage.installAppHandler({ platform, appPath, deviceId, projectType })
89
117
  const response: InstallAppResponse = {
90
118
  device: res.device,
@@ -96,13 +124,20 @@ async function handleInstallApp(args: ToolCallArgs) {
96
124
  }
97
125
 
98
126
  async function handleBuildApp(args: ToolCallArgs) {
99
- const { platform, projectType, projectPath, variant } = args as any
127
+ const platform = requireStringArg(args, 'platform') as PlatformArg
128
+ const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
129
+ const projectPath = requireStringArg(args, 'projectPath')
130
+ const variant = getStringArg(args, 'variant')
100
131
  const res = await ToolsManage.buildAppHandler({ platform, projectPath, variant, projectType })
101
132
  return wrapResponse(res)
102
133
  }
103
134
 
104
135
  async function handleBuildAndInstall(args: ToolCallArgs) {
105
- const { platform, projectType, projectPath, deviceId, timeout } = args as any
136
+ const platform = requireStringArg(args, 'platform') as PlatformArg
137
+ const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
138
+ const projectPath = requireStringArg(args, 'projectPath')
139
+ const deviceId = getStringArg(args, 'deviceId')
140
+ const timeout = getNumberArg(args, 'timeout')
106
141
  const res = await ToolsManage.buildAndInstallHandler({ platform, projectPath, deviceId, timeout, projectType })
107
142
  return {
108
143
  content: [
@@ -113,7 +148,16 @@ async function handleBuildAndInstall(args: ToolCallArgs) {
113
148
  }
114
149
 
115
150
  async function handleGetLogs(args: ToolCallArgs) {
116
- const { platform, appId, deviceId, pid, tag, level, contains, since_seconds, limit, lines } = args as any
151
+ const platform = requireStringArg(args, 'platform') as PlatformArg
152
+ const appId = getStringArg(args, 'appId')
153
+ const deviceId = getStringArg(args, 'deviceId')
154
+ const pid = getNumberArg(args, 'pid')
155
+ const tag = getStringArg(args, 'tag')
156
+ const level = getStringArg(args, 'level')
157
+ const contains = getStringArg(args, 'contains')
158
+ const since_seconds = getNumberArg(args, 'since_seconds')
159
+ const limit = getNumberArg(args, 'limit')
160
+ const lines = getNumberArg(args, 'lines')
117
161
  const res = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, pid, tag, level, contains, since_seconds, limit, lines })
118
162
  const filtered = !!(pid || tag || level || contains || since_seconds || appId)
119
163
  return {
@@ -125,7 +169,8 @@ async function handleGetLogs(args: ToolCallArgs) {
125
169
  }
126
170
 
127
171
  async function handleListDevices(args: ToolCallArgs) {
128
- const { platform, appId } = args as any
172
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
173
+ const appId = getStringArg(args, 'appId')
129
174
  const res = await ToolsManage.listDevicesHandler({ platform, appId })
130
175
  return wrapResponse(res)
131
176
  }
@@ -136,7 +181,8 @@ async function handleGetSystemStatus() {
136
181
  }
137
182
 
138
183
  async function handleCaptureScreenshot(args: ToolCallArgs) {
139
- const { platform, deviceId } = args as any
184
+ const platform = requireStringArg(args, 'platform') as PlatformArg
185
+ const deviceId = getStringArg(args, 'deviceId')
140
186
  const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId })
141
187
  const mime = (res as any).screenshot_mime || 'image/png'
142
188
  const content: Array<{ type: 'text' | 'image'; text?: string; data?: string; mimeType?: string }> = [
@@ -151,61 +197,95 @@ async function handleCaptureScreenshot(args: ToolCallArgs) {
151
197
  }
152
198
 
153
199
  async function handleCaptureDebugSnapshot(args: ToolCallArgs) {
154
- const { reason, includeLogs, logLines, platform, appId, deviceId, sessionId } = args as any
200
+ const reason = getStringArg(args, 'reason')
201
+ const includeLogs = getBooleanArg(args, 'includeLogs')
202
+ const logLines = getNumberArg(args, 'logLines')
203
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
204
+ const appId = getStringArg(args, 'appId')
205
+ const deviceId = getStringArg(args, 'deviceId')
206
+ const sessionId = getStringArg(args, 'sessionId')
155
207
  const res = await ToolsObserve.captureDebugSnapshotHandler({ reason, includeLogs, logLines, platform, appId, deviceId, sessionId })
156
208
  return wrapResponse(res)
157
209
  }
158
210
 
159
211
  async function handleGetUITree(args: ToolCallArgs) {
160
- const { platform, deviceId } = args as any
212
+ const platform = requireStringArg(args, 'platform') as PlatformArg
213
+ const deviceId = getStringArg(args, 'deviceId')
161
214
  const res = await ToolsObserve.getUITreeHandler({ platform, deviceId })
162
215
  return wrapResponse(res)
163
216
  }
164
217
 
165
218
  async function handleGetCurrentScreen(args: ToolCallArgs) {
166
- const { deviceId } = args as any
219
+ const deviceId = getStringArg(args, 'deviceId')
167
220
  const res = await ToolsObserve.getCurrentScreenHandler({ deviceId })
168
221
  return wrapResponse(res)
169
222
  }
170
223
 
171
224
  async function handleGetScreenFingerprint(args: ToolCallArgs) {
172
- const { platform, deviceId } = args as any
225
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
226
+ const deviceId = getStringArg(args, 'deviceId')
173
227
  const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId })
174
228
  return wrapResponse(res)
175
229
  }
176
230
 
177
231
  async function handleWaitForScreenChange(args: ToolCallArgs) {
178
- const { platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId } = args as any
232
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
233
+ const previousFingerprint = requireStringArg(args, 'previousFingerprint')
234
+ const timeoutMs = getNumberArg(args, 'timeoutMs')
235
+ const pollIntervalMs = getNumberArg(args, 'pollIntervalMs')
236
+ const deviceId = getStringArg(args, 'deviceId')
179
237
  const res = await ToolsInteract.waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId })
180
238
  return wrapResponse(res)
181
239
  }
182
240
 
183
241
  async function handleExpectScreen(args: ToolCallArgs) {
184
- const { platform, fingerprint, screen, deviceId } = args as any
242
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
243
+ const fingerprint = getStringArg(args, 'fingerprint')
244
+ const screen = getStringArg(args, 'screen')
245
+ const deviceId = getStringArg(args, 'deviceId')
185
246
  const res = await ToolsInteract.expectScreenHandler({ platform, fingerprint, screen, deviceId })
186
247
  return wrapResponse(res)
187
248
  }
188
249
 
189
250
  async function handleExpectElementVisible(args: ToolCallArgs) {
190
- const { selector, element_id, timeout_ms, poll_interval_ms, platform, deviceId } = args as any
251
+ const selector = requireObjectArg<ExpectElementSelectorArg>(args, 'selector')
252
+ const element_id = getStringArg(args, 'element_id')
253
+ const timeout_ms = getNumberArg(args, 'timeout_ms')
254
+ const poll_interval_ms = getNumberArg(args, 'poll_interval_ms')
255
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
256
+ const deviceId = getStringArg(args, 'deviceId')
191
257
  const res = await ToolsInteract.expectElementVisibleHandler({ selector, element_id, timeout_ms, poll_interval_ms, platform, deviceId })
192
258
  return wrapResponse(res)
193
259
  }
194
260
 
195
261
  async function handleWaitForUI(args: ToolCallArgs) {
196
- const { selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry, platform, deviceId } = args as any
262
+ const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
263
+ const condition = (getStringArg(args, 'condition') as 'exists' | 'not_exists' | 'visible' | 'clickable' | undefined) ?? 'exists'
264
+ const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000
265
+ const poll_interval_ms = getNumberArg(args, 'poll_interval_ms') ?? 300
266
+ const match = getObjectArg<WaitForUiMatchArg>(args, 'match')
267
+ const retry = getObjectArg<WaitForUiRetryArg>(args, 'retry')
268
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
269
+ const deviceId = getStringArg(args, 'deviceId')
197
270
  const res = await ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform, deviceId })
198
271
  return wrapResponse(res)
199
272
  }
200
273
 
201
274
  async function handleFindElement(args: ToolCallArgs) {
202
- const { query, exact = false, timeoutMs = 3000, platform, deviceId } = args as any
275
+ const query = requireStringArg(args, 'query')
276
+ const exact = getBooleanArg(args, 'exact') ?? false
277
+ const timeoutMs = getNumberArg(args, 'timeoutMs') ?? 3000
278
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
279
+ const deviceId = getStringArg(args, 'deviceId')
203
280
  const res = await ToolsInteract.findElementHandler({ query, exact, timeoutMs, platform, deviceId })
204
281
  return wrapResponse(res)
205
282
  }
206
283
 
207
284
  async function handleTap(args: ToolCallArgs) {
208
- const { platform, x, y, deviceId } = args as any
285
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
286
+ const x = requireNumberArg(args, 'x')
287
+ const y = requireNumberArg(args, 'y')
288
+ const deviceId = getStringArg(args, 'deviceId')
209
289
  const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId)
210
290
  ToolsNetwork.notifyActionStart()
211
291
  const res = await ToolsInteract.tapHandler({ platform, x, y, deviceId })
@@ -221,14 +301,20 @@ async function handleTap(args: ToolCallArgs) {
221
301
  }
222
302
 
223
303
  async function handleTapElement(args: ToolCallArgs) {
224
- const { elementId } = args as any
304
+ const elementId = requireStringArg(args, 'elementId')
225
305
  ToolsNetwork.notifyActionStart()
226
306
  const res = await ToolsInteract.tapElementHandler({ elementId })
227
307
  return wrapResponse(res)
228
308
  }
229
309
 
230
310
  async function handleSwipe(args: ToolCallArgs) {
231
- const { platform = 'android', x1, y1, x2, y2, duration, deviceId } = args as any
311
+ const platform = (getStringArg(args, 'platform') as PlatformArg | undefined) ?? 'android'
312
+ const x1 = requireNumberArg(args, 'x1')
313
+ const y1 = requireNumberArg(args, 'y1')
314
+ const x2 = requireNumberArg(args, 'x2')
315
+ const y2 = requireNumberArg(args, 'y2')
316
+ const duration = requireNumberArg(args, 'duration')
317
+ const deviceId = getStringArg(args, 'deviceId')
232
318
  const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId)
233
319
  ToolsNetwork.notifyActionStart()
234
320
  const res = await ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId })
@@ -244,14 +330,19 @@ async function handleSwipe(args: ToolCallArgs) {
244
330
  }
245
331
 
246
332
  async function handleScrollToElement(args: ToolCallArgs) {
247
- const { platform, selector, direction, maxScrolls, scrollAmount, deviceId } = args as any
333
+ const platform = requireStringArg(args, 'platform') as PlatformArg
334
+ const selector = requireObjectArg<ScrollSelectorArg>(args, 'selector')
335
+ const direction = getStringArg(args, 'direction') as 'down' | 'up' | undefined
336
+ const maxScrolls = getNumberArg(args, 'maxScrolls')
337
+ const scrollAmount = getNumberArg(args, 'scrollAmount')
338
+ const deviceId = getStringArg(args, 'deviceId')
248
339
  const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId)
249
340
  ToolsNetwork.notifyActionStart()
250
341
  const res = await ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId })
251
342
  const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId)
252
343
  return wrapResponse(buildActionExecutionResult({
253
344
  actionType: 'scroll_to_element',
254
- selector,
345
+ selector: selector ?? null,
255
346
  resolved: res?.success && res?.element ? {
256
347
  elementId: null,
257
348
  text: (res.element as any).text ?? null,
@@ -269,7 +360,8 @@ async function handleScrollToElement(args: ToolCallArgs) {
269
360
  }
270
361
 
271
362
  async function handleTypeText(args: ToolCallArgs) {
272
- const { text, deviceId } = args as any
363
+ const text = requireStringArg(args, 'text')
364
+ const deviceId = getStringArg(args, 'deviceId')
273
365
  const uiFingerprintBefore = await captureActionFingerprint('android', deviceId)
274
366
  ToolsNetwork.notifyActionStart()
275
367
  const res = await ToolsInteract.typeTextHandler({ text, deviceId })
@@ -285,7 +377,7 @@ async function handleTypeText(args: ToolCallArgs) {
285
377
  }
286
378
 
287
379
  async function handlePressBack(args: ToolCallArgs) {
288
- const { deviceId } = args as any
380
+ const deviceId = getStringArg(args, 'deviceId')
289
381
  const uiFingerprintBefore = await captureActionFingerprint('android', deviceId)
290
382
  ToolsNetwork.notifyActionStart()
291
383
  const res = await ToolsInteract.pressBackHandler({ deviceId })
@@ -301,27 +393,38 @@ async function handlePressBack(args: ToolCallArgs) {
301
393
  }
302
394
 
303
395
  async function handleStartLogStream(args: ToolCallArgs) {
304
- const { platform, packageName, level, sessionId, deviceId } = args as any
396
+ const platform = (getStringArg(args, 'platform') as PlatformArg | undefined) ?? 'android'
397
+ const packageName = requireStringArg(args, 'packageName')
398
+ const level = (getStringArg(args, 'level') as 'error' | 'warn' | 'info' | 'debug' | undefined) ?? 'error'
399
+ const sessionId = getStringArg(args, 'sessionId')
400
+ const deviceId = getStringArg(args, 'deviceId')
305
401
  const res = await ToolsObserve.startLogStreamHandler({ platform, packageName, level, sessionId, deviceId })
306
402
  return wrapResponse(res)
307
403
  }
308
404
 
309
405
  async function handleReadLogStream(args: ToolCallArgs) {
310
- const { platform, sessionId, limit, since } = args as any
406
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
407
+ const sessionId = getStringArg(args, 'sessionId')
408
+ const limit = getNumberArg(args, 'limit')
409
+ const since = getStringArg(args, 'since')
311
410
  const res = await ToolsObserve.readLogStreamHandler({ platform, sessionId, limit, since })
312
411
  return wrapResponse(res)
313
412
  }
314
413
 
315
414
  async function handleStopLogStream(args: ToolCallArgs) {
316
- const { platform, sessionId } = args as any
415
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
416
+ const sessionId = getStringArg(args, 'sessionId')
317
417
  const res = await ToolsObserve.stopLogStreamHandler({ platform, sessionId })
318
418
  return wrapResponse(res)
319
419
  }
320
420
 
321
421
  function handleClassifyActionOutcome(args: ToolCallArgs) {
322
- const { uiChanged, expectedElementVisible, networkRequests, hasLogErrors } = args as any
422
+ const uiChanged = requireBooleanArg(args, 'uiChanged')
423
+ const expectedElementVisible = getBooleanArg(args, 'expectedElementVisible')
424
+ const networkRequests = getArrayArg<ClassifyNetworkRequestArg>(args, 'networkRequests')
425
+ const hasLogErrors = getBooleanArg(args, 'hasLogErrors')
323
426
  const result = classifyActionOutcome({
324
- uiChanged: Boolean(uiChanged),
427
+ uiChanged,
325
428
  expectedElementVisible: expectedElementVisible ?? null,
326
429
  networkRequests: networkRequests ?? null,
327
430
  hasLogErrors: hasLogErrors ?? null
@@ -330,7 +433,8 @@ function handleClassifyActionOutcome(args: ToolCallArgs) {
330
433
  }
331
434
 
332
435
  async function handleGetNetworkActivity(args: ToolCallArgs) {
333
- const { platform, deviceId } = args as any
436
+ const platform = requireStringArg(args, 'platform') as PlatformArg
437
+ const deviceId = getStringArg(args, 'deviceId')
334
438
  const result = await ToolsNetwork.getNetworkActivity({ platform, deviceId })
335
439
  return wrapResponse(result)
336
440
  }
@@ -13,7 +13,7 @@ export { wrapResponse, toolDefinitions, handleToolCall }
13
13
 
14
14
  export const serverInfo = {
15
15
  name: 'mobile-debug-mcp',
16
- version: '0.7.0'
16
+ version: '0.24.7'
17
17
  }
18
18
 
19
19
  export function createServer() {
@@ -50,26 +50,40 @@ export function ensureAdbAvailable() {
50
50
  }
51
51
  }
52
52
 
53
+ type GradleEnvOverrides = Record<string, string | undefined>
54
+
55
+ function mergeEnv(overrides?: GradleEnvOverrides) {
56
+ const env: Record<string, string> = {}
57
+ for (const [key, value] of Object.entries(process.env)) {
58
+ if (typeof value === 'string') env[key] = value
59
+ }
60
+ for (const [key, value] of Object.entries(overrides || {})) {
61
+ if (typeof value === 'string') env[key] = value
62
+ }
63
+ return env
64
+ }
65
+
53
66
  /**
54
67
  * Prepare Gradle execution options for building an Android project.
55
68
  * Returns execCmd (wrapper or gradle), base gradleArgs array, and spawn options including env.
56
69
  */
57
- export async function prepareGradle(projectPath: string): Promise<{ execCmd: string, gradleArgs: string[], spawnOpts: any }> {
70
+ export async function prepareGradle(projectPath: string, envOverrides: GradleEnvOverrides = {}): Promise<{ execCmd: string, gradleArgs: string[], spawnOpts: any }> {
71
+ const env = mergeEnv(envOverrides)
58
72
  const gradlewPath = path.join(projectPath, 'gradlew')
59
73
  const gradleCmd = existsSync(gradlewPath) ? './gradlew' : 'gradle'
60
74
  const execCmd = existsSync(gradlewPath) ? gradlewPath : gradleCmd
61
75
 
62
76
  // Start with a default task; callers may append/override via env flags
63
- const gradleArgs: string[] = [ process.env.MCP_GRADLE_TASK || 'assembleDebug' ]
77
+ const gradleArgs: string[] = [ env.MCP_GRADLE_TASK || 'assembleDebug' ]
64
78
 
65
79
  // Respect generic MCP_BUILD_JOBS and Android-specific MCP_GRADLE_WORKERS
66
- const workers = process.env.MCP_GRADLE_WORKERS || process.env.MCP_BUILD_JOBS
80
+ const workers = env.MCP_GRADLE_WORKERS || env.MCP_BUILD_JOBS
67
81
  if (workers) {
68
82
  gradleArgs.push(`--max-workers=${workers}`)
69
83
  }
70
84
 
71
85
  // Respect gradle cache env: default enabled; set MCP_GRADLE_CACHE=0 to disable
72
- if (process.env.MCP_GRADLE_CACHE === '0') {
86
+ if (env.MCP_GRADLE_CACHE === '0') {
73
87
  gradleArgs.push('-Dorg.gradle.caching=false')
74
88
  }
75
89
 
@@ -82,8 +96,6 @@ export async function prepareGradle(projectPath: string): Promise<{ execCmd: str
82
96
  gradleCheck = { gradleJavaHome: undefined, gradleValid: false, filesChecked: [], issues: [] }
83
97
  }
84
98
 
85
- const env = Object.assign({}, process.env)
86
-
87
99
  // Ensure child processes can find Android platform-tools (adb, etc.) by
88
100
  // prepending the platform-tools directory to PATH for spawned processes.
89
101
  const adbPath = resolveAdbCmd()
@@ -488,4 +500,3 @@ export function parseLogLine(line: string) { // Collapse internal newlines so m
488
500
  }
489
501
 
490
502
  // Legacy readLogStreamLines shim removed. Use AndroidObserve.readLogStream(sessionId, limit, since) instead.
491
-
@@ -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' as any })
29
29
  return typeof res.status === 'number' ? res.status === 0 : false
30
30
  }
@@ -15,7 +15,7 @@ if (!fs.existsSync(helperScript)) {
15
15
  }
16
16
 
17
17
  try {
18
- const out = execSync(`tsx ${helperScript} --platform android --id default`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
18
+ const out = execSync(`tsx ${helperScript} --platform android`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
19
19
  const parsed = JSON.parse(out)
20
20
 
21
21
  if (!parsed?.resolution || parsed.resolution.width <= 0 || parsed.resolution.height <= 0) throw new Error('Invalid screenshot resolution')
@@ -15,7 +15,7 @@ if (!fs.existsSync(helperScript)) {
15
15
  }
16
16
 
17
17
  try {
18
- const out = execSync(`tsx ${helperScript} --platform ios --id booted`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
18
+ const out = execSync(`tsx ${helperScript} --platform ios`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
19
19
  const parsed = JSON.parse(out)
20
20
 
21
21
  if (!parsed?.resolution || parsed.resolution.width <= 0 || parsed.resolution.height <= 0) throw new Error('Invalid screenshot resolution')
@@ -17,7 +17,7 @@ if (!fs.existsSync(helperScript)) {
17
17
 
18
18
  try {
19
19
  // Run the helper smoke script for android
20
- const cmd = `tsx ${helperScript} --platform android --id default --limit 20`
20
+ const cmd = `tsx ${helperScript} --platform android --limit 20`
21
21
  const out = execSync(cmd, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
22
22
  const parsed = JSON.parse(out)
23
23
 
@@ -15,7 +15,7 @@ if (!fs.existsSync(helperScript)) {
15
15
  }
16
16
 
17
17
  try {
18
- const cmd = `tsx ${helperScript} --platform ios --id booted --limit 20`
18
+ const cmd = `tsx ${helperScript} --platform ios --limit 20`
19
19
  const out = execSync(cmd, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
20
20
  const parsed = JSON.parse(out)
21
21
 
@@ -15,7 +15,7 @@ if (!fs.existsSync(helperScript)) {
15
15
  }
16
16
 
17
17
  try {
18
- const out = execSync(`tsx ${helperScript} --platform android --id default`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
18
+ const out = execSync(`tsx ${helperScript} --platform android`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
19
19
  const parsed = JSON.parse(out)
20
20
 
21
21
  if (!parsed?.resolution || parsed.resolution.width <= 0 || parsed.resolution.height <= 0) throw new Error('Invalid UI tree resolution')
@@ -15,7 +15,7 @@ if (!fs.existsSync(helperScript)) {
15
15
  }
16
16
 
17
17
  try {
18
- const out = execSync(`tsx ${helperScript} --platform ios --id booted`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
18
+ const out = execSync(`tsx ${helperScript} --platform ios`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
19
19
  const parsed = JSON.parse(out)
20
20
 
21
21
  if (!parsed?.resolution || parsed.resolution.width <= 0 || parsed.resolution.height <= 0) throw new Error('Invalid UI tree resolution')
@@ -1,6 +1,6 @@
1
- import { AndroidObserve, iOSObserve } from "../../../src/observe/index.js";
2
- import { AndroidInteract } from "../../../src/interact/index.js";
3
- import { iOSInteract } from "../../../src/interact/index.js";
1
+ import { AndroidObserve, iOSObserve } from "../../../../src/observe/index.js";
2
+ import { AndroidInteract } from "../../../../src/interact/index.js";
3
+ import { iOSInteract } from "../../../../src/interact/index.js";
4
4
  import fs from "fs/promises";
5
5
 
6
6
  const androidObserve = new AndroidObserve();
@@ -1,4 +1,4 @@
1
- import { ToolsObserve } from '../../../src/observe/index.js'
1
+ import { ToolsObserve } from '../../../../src/observe/index.js'
2
2
 
3
3
  function readArg(flag: string): string | undefined {
4
4
  const index = process.argv.indexOf(flag)
@@ -8,7 +8,7 @@ function readArg(flag: string): string | undefined {
8
8
 
9
9
  async function main() {
10
10
  const platform = (readArg('--platform') || process.argv[2] || 'android') as 'android' | 'ios'
11
- const deviceId = readArg('--id') || readArg('--deviceId') || process.argv[3]
11
+ const deviceId = readArg('--id') || readArg('--deviceId') || (process.argv[2]?.startsWith('-') ? undefined : process.argv[3])
12
12
 
13
13
  const result = await ToolsObserve.captureScreenshotHandler({ platform, deviceId })
14
14
  const screenshot = (result as any).screenshot || ''
@@ -1,10 +1,10 @@
1
- import { ToolsObserve } from '../../../src/observe/index.js'
1
+ import { ToolsObserve } from '../../../../src/observe/index.js'
2
2
  import minimist from 'minimist'
3
3
 
4
4
  async function main() {
5
5
  const args = minimist(process.argv.slice(2))
6
6
  const platform = args.platform || args.p || 'android'
7
- const id = args.id || args.device || args.deviceId || 'default'
7
+ const id = args.id || args.device || args.deviceId
8
8
  const limit = typeof args.limit === 'number' ? args.limit : (typeof args.lines === 'number' ? args.lines : 50)
9
9
 
10
10
  try {
@@ -1,4 +1,4 @@
1
- import { ToolsObserve } from '../../../src/observe/index.js'
1
+ import { ToolsObserve } from '../../../../src/observe/index.js'
2
2
 
3
3
  function readArg(flag: string): string | undefined {
4
4
  const index = process.argv.indexOf(flag)
@@ -8,7 +8,7 @@ function readArg(flag: string): string | undefined {
8
8
 
9
9
  async function main() {
10
10
  const platform = (readArg('--platform') || process.argv[2] || 'android') as 'android' | 'ios'
11
- const deviceId = readArg('--id') || readArg('--deviceId') || process.argv[3]
11
+ const deviceId = readArg('--id') || readArg('--deviceId') || (process.argv[2]?.startsWith('-') ? undefined : process.argv[3])
12
12
 
13
13
  const result = await ToolsObserve.getUITreeHandler({ platform, deviceId })
14
14
  if ((result as any).error) throw new Error((result as any).error)
@@ -1,4 +1,4 @@
1
- import { AndroidObserve } from '../../../src/observe/index.js'
1
+ import { AndroidObserve } from '../../../../src/observe/index.js'
2
2
 
3
3
  async function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)) }
4
4
 
@@ -4,8 +4,8 @@
4
4
  * Usage: RUN_DEVICE_TESTS=true npx tsx test/device/manual/observe/screen_fingerprint.manual.ts [android|ios] [deviceId]
5
5
  */
6
6
 
7
- import { AndroidObserve } from '../../../src/observe/index.js'
8
- import { iOSObserve } from '../../../src/observe/index.js'
7
+ import { AndroidObserve } from '../../../../src/observe/index.js'
8
+ import { iOSObserve } from '../../../../src/observe/index.js'
9
9
 
10
10
  async function main() {
11
11
  const args = process.argv.slice(2)