mobile-debug-mcp 0.30.1 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +50 -5
  2. package/dist/interact/index.js +546 -133
  3. package/dist/manage/ios.js +1 -1
  4. package/dist/server/tool-definitions.js +13 -9
  5. package/dist/server/tool-handlers.js +4 -4
  6. package/dist/server-core.js +1 -1
  7. package/dist/system/index.js +485 -48
  8. package/dist/system/ios.js +15 -7
  9. package/dist/utils/android/utils.js +4 -4
  10. package/dist/utils/cli/idb/check-idb.js +10 -3
  11. package/dist/utils/cli/idb/idb-helper.js +47 -13
  12. package/dist/utils/ios/utils.js +5 -9
  13. package/docs/CHANGELOG.md +6 -0
  14. package/docs/ROADMAP.md +88 -212
  15. package/docs/specs/baseline-spec-v0.md +1 -1
  16. package/docs/specs-deltas/015-adjustable-control-support.md +541 -0
  17. package/docs/specs-deltas/016-environment-tools.md +533 -0
  18. package/docs/tools/TOOLS.md +2 -0
  19. package/docs/tools/manage.md +4 -2
  20. package/docs/tools/system.md +9 -2
  21. package/package.json +1 -1
  22. package/skills/spec-review/SKILL.md +52 -0
  23. package/skills/spec-review/references/spec-review-checklist.md +13 -0
  24. package/skills/spec-review/references/spec-review-template.md +28 -0
  25. package/src/interact/index.ts +581 -138
  26. package/src/manage/ios.ts +1 -1
  27. package/src/server/tool-definitions.ts +13 -9
  28. package/src/server/tool-handlers.ts +4 -4
  29. package/src/server-core.ts +1 -1
  30. package/src/system/index.ts +572 -50
  31. package/src/system/ios.ts +14 -7
  32. package/src/types.ts +2 -0
  33. package/src/utils/android/utils.ts +3 -3
  34. package/src/utils/cli/idb/check-idb.ts +11 -2
  35. package/src/utils/cli/idb/idb-helper.ts +49 -12
  36. package/src/utils/ios/utils.ts +4 -8
  37. package/test/unit/interact/adjust_control.test.ts +586 -33
  38. package/test/unit/interact/tap_element.test.ts +1 -1
  39. package/test/unit/server/contract.test.ts +4 -0
  40. package/test/unit/system/get_system_status.test.ts +3 -0
  41. package/test/unit/system/system_status.test.ts +40 -0
  42. package/test/unit/utils/idb_health.test.ts +74 -0
  43. package/skills/rfc-review/SKILL.md +0 -52
  44. package/skills/rfc-review/references/rfc-review-checklist.md +0 -12
  45. package/skills/rfc-review/references/rfc-review-template.md +0 -28
  46. /package/docs/{rfcs → specs-deltas}/001-state-verification.md +0 -0
  47. /package/docs/{rfcs → specs-deltas}/002-richer-element-identity +0 -0
  48. /package/docs/{rfcs → specs-deltas}/003-wait-and-synchronization-reliability.md +0 -0
  49. /package/docs/{rfcs → specs-deltas}/004-action-verification-routing.md +0 -0
  50. /package/docs/{rfcs → specs-deltas}/005-unified-action-execution-and-verification-model.md +0 -0
  51. /package/docs/{rfcs → specs-deltas}/006-runtime-action-instrumentation-and-binding-layer.md +0 -0
  52. /package/docs/{rfcs → specs-deltas}/007-actionability-resolution-and-executable-target-selection.md +0 -0
  53. /package/docs/{rfcs → specs-deltas}/008-adjustable-control-support-and-semantic-value-manipulation.md +0 -0
  54. /package/docs/{rfcs → specs-deltas}/009-semantic-control-modeling-for-custom-and-composite-controls.md +0 -0
  55. /package/docs/{rfcs → specs-deltas}/010-verification-stabilization-and-temporal-convergence.md +0 -0
  56. /package/docs/{rfcs → specs-deltas}/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md +0 -0
  57. /package/docs/{rfcs → specs-deltas}/011.1-recovery-contract-types-and-runtime-wiring-spec.md +0 -0
  58. /package/docs/{rfcs → specs-deltas}/012-action-trace-and-xecution-observability.md +0 -0
  59. /package/docs/{rfcs → specs-deltas}/013-wait-and-synchronization-reliability.md +0 -0
  60. /package/docs/{rfcs → specs-deltas}/014-actionability-resolution.md +0 -0
@@ -257,7 +257,7 @@ export class iOSManage {
257
257
  // Gather diagnostics for simctl failure
258
258
  const diag = execCommandWithDiagnostics(['simctl', 'install', deviceId, toInstall], deviceId);
259
259
  try {
260
- const child = spawn(getIdbCmd(), ['--version']);
260
+ const child = spawn(getIdbCmd(), ['list-targets', '--json']);
261
261
  const idbExists = await new Promise((resolve) => {
262
262
  child.on('error', () => resolve(false));
263
263
  child.on('close', (code) => resolve(code === 0));
@@ -568,11 +568,11 @@ Adjust a numeric control value with verification.
568
568
  This is the initial adjustable-control surface for slider-like controls and other controls that expose a numeric value or value_range.
569
569
 
570
570
  Inputs:
571
- - selector or element_id
571
+ - exactly one of selector or element_id
572
572
  - property (defaults to "value")
573
573
  - targetValue
574
- - tolerance (optional)
575
- - maxAttempts (optional)
574
+ - tolerance (optional; discrete default is 0, continuous default is max(step / 2, 1% of range) when omitted)
575
+ - maxAttempts (optional; defaults to 5)
576
576
  - platform/deviceId (optional)
577
577
 
578
578
  Output Structure:
@@ -584,14 +584,14 @@ Output Structure:
584
584
  - success = true when the control converges within tolerance
585
585
 
586
586
  Verification Guidance:
587
- - Prefer direct target placement when value_range is available; fall back to a drag only if the direct tap does not converge
587
+ - Prefer direct platform adjustment when the runtime exposes it; then verified increment/decrement actions; use coordinate fallback only as degraded mode
588
588
  - Use expect_state for the control value readback
589
- - Treat coordinate fallback as degraded mode
589
+ - Do not use derived semantic metadata as proof of success
590
590
 
591
591
  Failure Handling:
592
592
  - ELEMENT_NOT_FOUND → re-resolve the control
593
593
  - ELEMENT_NOT_INTERACTABLE → the control cannot be adjusted through the current runtime
594
- - TIMEOUT → the control did not converge within bounded retries
594
+ - CONTROL_CONVERGENCE_FAILED → the control did not converge within bounded retries or the requested value is out of range
595
595
  - UNKNOWN → capture a snapshot and stop`,
596
596
  inputSchema: {
597
597
  type: 'object',
@@ -608,12 +608,16 @@ Failure Handling:
608
608
  element_id: { type: 'string', description: 'Optional previously resolved element identifier.' },
609
609
  property: { type: 'string', description: 'Readable numeric state property to adjust.', default: 'value' },
610
610
  targetValue: { type: 'number', description: 'Target numeric value.' },
611
- tolerance: { type: 'number', description: 'Accepted numeric tolerance around the target value.', default: 0 },
612
- maxAttempts: { type: 'number', description: 'Maximum adjustment attempts.', default: 3 },
611
+ tolerance: { type: 'number', description: 'Accepted numeric tolerance around the target value. Defaults are derived from the readable control domain.' },
612
+ maxAttempts: { type: 'number', description: 'Maximum adjustment attempts.', default: 5 },
613
613
  platform: { type: 'string', enum: ['android', 'ios'], description: 'Optional platform override' },
614
614
  deviceId: { type: 'string', description: 'Optional device serial/udid' }
615
615
  },
616
- required: ['targetValue']
616
+ required: ['targetValue'],
617
+ oneOf: [
618
+ { required: ['selector'] },
619
+ { required: ['element_id'] }
620
+ ]
617
621
  }
618
622
  },
619
623
  {
@@ -231,12 +231,12 @@ async function handleAdjustControl(args) {
231
231
  const element_id = getStringArg(args, 'element_id');
232
232
  const property = getStringArg(args, 'property') ?? 'value';
233
233
  const targetValue = requireNumberArg(args, 'targetValue');
234
- const tolerance = getNumberArg(args, 'tolerance') ?? 0;
235
- const maxAttempts = getNumberArg(args, 'maxAttempts') ?? 3;
234
+ const tolerance = getNumberArg(args, 'tolerance');
235
+ const maxAttempts = getNumberArg(args, 'maxAttempts');
236
236
  const platform = getStringArg(args, 'platform');
237
237
  const deviceId = getStringArg(args, 'deviceId');
238
- if (!selector && !element_id) {
239
- throw new Error('Missing selector or element_id argument');
238
+ if ((!selector && !element_id) || (selector && element_id)) {
239
+ throw new Error('Exactly one of selector or element_id argument is required');
240
240
  }
241
241
  const res = await ToolsInteract.adjustControlHandler({
242
242
  selector: selector ?? undefined,
@@ -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.30.1'
9
+ version: '0.31.0'
10
10
  };
11
11
  export function createServer() {
12
12
  const server = new Server(serverInfo, {
@@ -1,68 +1,505 @@
1
+ import { spawnSync } from 'child_process';
2
+ import path from 'path';
1
3
  import { checkAndroid } from './android.js';
2
- import { checkIOS } from './ios.js';
3
4
  import { checkGradle } from './gradle.js';
5
+ import { checkIOS } from './ios.js';
6
+ import { resolveAdbCmd } from '../utils/android/utils.js';
7
+ import { getIdbCmd, getXcrunCmd } from '../utils/ios/utils.js';
8
+ import { probeIdb } from '../utils/cli/idb/idb-helper.js';
9
+ import { detectJavaHome } from '../utils/java.js';
10
+ function normalizeHostOs(platform) {
11
+ switch (platform) {
12
+ case 'darwin':
13
+ return 'macos';
14
+ case 'linux':
15
+ return 'linux';
16
+ case 'win32':
17
+ return 'windows';
18
+ case 'freebsd':
19
+ case 'openbsd':
20
+ case 'netbsd':
21
+ case 'sunos':
22
+ return platform;
23
+ default:
24
+ return 'unknown';
25
+ }
26
+ }
27
+ function isSupportedHost(hostOs) {
28
+ return hostOs === 'macos' || hostOs === 'linux' || hostOs === 'windows';
29
+ }
30
+ function firstVersionToken(raw) {
31
+ if (!raw)
32
+ return null;
33
+ const match = String(raw).match(/(\d+(?:\.\d+)+|\d+)/);
34
+ return match?.[1] ?? null;
35
+ }
36
+ function probeCommand(command, args, timeoutMs = 2000) {
37
+ try {
38
+ const result = spawnSync(command, args, { encoding: 'utf8', timeout: timeoutMs, stdio: ['ignore', 'pipe', 'pipe'] });
39
+ const raw = `${result.stdout || ''}${result.stderr || ''}`.trim() || null;
40
+ return {
41
+ ok: result.status === 0,
42
+ raw,
43
+ parsedVersion: firstVersionToken(raw)
44
+ };
45
+ }
46
+ catch {
47
+ return { ok: false, raw: null, parsedVersion: null };
48
+ }
49
+ }
50
+ function makeFailure(code, platform, message, opts = {}) {
51
+ return {
52
+ code,
53
+ platform,
54
+ tool: opts.tool ?? null,
55
+ message,
56
+ searchedLocations: opts.searchedLocations ?? [],
57
+ selectedProvider: opts.selectedProvider ?? null,
58
+ rawVersion: opts.rawVersion ?? null,
59
+ remediation: opts.remediation ?? []
60
+ };
61
+ }
62
+ function makeToolEntry(resolvedPath, discoverySource, rawVersionOutput, validationStatus) {
63
+ return {
64
+ resolvedPath,
65
+ discoverySource,
66
+ parsedVersion: firstVersionToken(rawVersionOutput),
67
+ rawVersionOutput,
68
+ validationStatus
69
+ };
70
+ }
71
+ function describeAdbDiscovery(adbCmd) {
72
+ const searchedLocations = ['ADB_PATH', 'ANDROID_SDK_ROOT/platform-tools/adb', 'ANDROID_HOME/platform-tools/adb', '~/Library/Android/sdk/platform-tools/adb', 'PATH'];
73
+ if (process.env.ADB_PATH && process.env.ADB_PATH.trim())
74
+ return { discoverySource: 'ADB_PATH', searchedLocations };
75
+ if (process.env.ANDROID_SDK_ROOT)
76
+ return { discoverySource: 'ANDROID_SDK_ROOT', searchedLocations };
77
+ if (process.env.ANDROID_HOME)
78
+ return { discoverySource: 'ANDROID_HOME', searchedLocations };
79
+ if (adbCmd.includes('Library/Android/sdk'))
80
+ return { discoverySource: 'HOME_SDK', searchedLocations };
81
+ if (adbCmd === 'adb')
82
+ return { discoverySource: 'PATH_OR_FALLBACK', searchedLocations };
83
+ return { discoverySource: 'PATH', searchedLocations };
84
+ }
85
+ function describeXcrunDiscovery(xcrunCmd) {
86
+ const searchedLocations = ['XCRUN_PATH', 'PATH'];
87
+ if (process.env.XCRUN_PATH && process.env.XCRUN_PATH.trim())
88
+ return { discoverySource: 'XCRUN_PATH', searchedLocations };
89
+ return { discoverySource: xcrunCmd === 'xcrun' ? 'PATH' : 'PATH', searchedLocations };
90
+ }
91
+ function describeIdbDiscovery(idbCmd) {
92
+ const searchedLocations = ['MCP_IDB_PATH', 'IDB_PATH', 'mcp.config.json', '~/.mcp/config.json', 'PATH', '/opt/homebrew/bin/idb', '/usr/local/bin/idb'];
93
+ if (process.env.MCP_IDB_PATH && process.env.MCP_IDB_PATH.trim())
94
+ return { discoverySource: 'MCP_IDB_PATH', searchedLocations };
95
+ if (process.env.IDB_PATH && process.env.IDB_PATH.trim())
96
+ return { discoverySource: 'IDB_PATH', searchedLocations };
97
+ return { discoverySource: idbCmd === 'idb' ? 'PATH_OR_FALLBACK' : 'PATH', searchedLocations };
98
+ }
99
+ function describeJavaDiscovery(javaHome) {
100
+ const searchedLocations = ['GRADLE_JAVA_HOME', 'JAVA_HOME', 'GRADLE_HOME', 'detectJavaHome'];
101
+ if (process.env.GRADLE_JAVA_HOME && process.env.GRADLE_JAVA_HOME.trim())
102
+ return { discoverySource: 'GRADLE_JAVA_HOME', searchedLocations };
103
+ if (process.env.JAVA_HOME && process.env.JAVA_HOME.trim())
104
+ return { discoverySource: 'JAVA_HOME', searchedLocations };
105
+ if (process.env.GRADLE_HOME && process.env.GRADLE_HOME.trim())
106
+ return { discoverySource: 'GRADLE_HOME', searchedLocations };
107
+ return { discoverySource: javaHome ? 'detected' : null, searchedLocations };
108
+ }
109
+ function resolveJavaBinaryPath(javaHome, hostOs) {
110
+ const javaBinary = hostOs === 'windows' ? 'java.exe' : 'java';
111
+ return path.join(javaHome, 'bin', javaBinary);
112
+ }
113
+ function buildUnsupportedHostStatus(hostOs) {
114
+ const failureMessage = `Host operating system '${hostOs}' is not supported.`;
115
+ const remediation = ['Run Mobile Debug MCP on macOS, Linux, or Windows.'];
116
+ const failureAndroid = makeFailure('HOST_OS_UNSUPPORTED', 'android', failureMessage, {
117
+ remediation
118
+ });
119
+ const failureIos = makeFailure('HOST_OS_UNSUPPORTED', 'ios', failureMessage, {
120
+ remediation
121
+ });
122
+ return {
123
+ success: false,
124
+ status: 'blocked',
125
+ adbAvailable: false,
126
+ adbVersion: '',
127
+ devices: 0,
128
+ deviceStates: '',
129
+ logsAvailable: false,
130
+ envValid: false,
131
+ issues: [failureMessage],
132
+ appInstalled: undefined,
133
+ iosAvailable: false,
134
+ iosDevices: 0,
135
+ gradleJavaHome: undefined,
136
+ gradleValid: false,
137
+ gradleFilesChecked: [],
138
+ gradleSuggestedFixes: [],
139
+ host: { os: hostOs, supported: false },
140
+ android: {
141
+ status: 'unsupported',
142
+ tools: {},
143
+ providers: {},
144
+ capabilities: {},
145
+ devices: [],
146
+ failures: [failureAndroid]
147
+ },
148
+ ios: {
149
+ status: 'unsupported',
150
+ tools: {},
151
+ providers: {},
152
+ capabilities: {},
153
+ devices: [],
154
+ failures: [failureIos]
155
+ },
156
+ summary: {
157
+ overall: 'blocked',
158
+ android: { ready: false, summary: `Host operating system '${hostOs}' is not supported.`, blockers: [failureMessage] },
159
+ ios: { ready: false, summary: `Host operating system '${hostOs}' is not supported.`, blockers: [failureMessage] },
160
+ gradle: { ready: false, summary: 'Gradle status unavailable on unsupported host', blockers: [], suggestedFixes: [] }
161
+ }
162
+ };
163
+ }
164
+ function buildAndroidPlatform(android, hostSupported) {
165
+ const resolvedAdbCmd = resolveAdbCmd();
166
+ const adbDiscovery = describeAdbDiscovery(resolvedAdbCmd);
167
+ const adbTool = makeToolEntry(resolvedAdbCmd, adbDiscovery.discoverySource, android.adbVersion || null, android.adbAvailable ? 'valid' : 'invalid');
168
+ const adbAvailable = !!android.adbAvailable;
169
+ const deviceIssues = android.issues.filter((issue) => /unauthorized|offline|No Android devices connected/i.test(issue));
170
+ const envFailure = !android.envValid
171
+ ? makeFailure('TOOL_VALIDATION_FAILED', 'android', 'Android environment prerequisites are incomplete.', {
172
+ tool: 'adb',
173
+ searchedLocations: ['ANDROID_SDK_ROOT', 'ANDROID_HOME', 'PATH'],
174
+ selectedProvider: 'adb',
175
+ rawVersion: android.adbVersion || null,
176
+ remediation: ['Set ANDROID_SDK_ROOT or ANDROID_HOME, or ensure adb is on PATH.']
177
+ })
178
+ : null;
179
+ const adbFailure = !adbAvailable
180
+ ? makeFailure(process.env.ADB_PATH && process.env.ADB_PATH.trim() ? 'TOOL_NOT_EXECUTABLE' : 'TOOL_NOT_FOUND', 'android', 'Android adb could not be resolved or executed.', {
181
+ tool: 'adb',
182
+ searchedLocations: adbDiscovery.searchedLocations,
183
+ selectedProvider: null,
184
+ rawVersion: android.adbVersion || null,
185
+ remediation: [
186
+ 'Install Android platform-tools.',
187
+ 'Set ADB_PATH or ANDROID_SDK_ROOT/ANDROID_HOME if adb is installed in a non-standard location.'
188
+ ]
189
+ })
190
+ : null;
191
+ const deviceFailure = adbAvailable && android.devices === 0
192
+ ? makeFailure('DEVICE_NOT_FOUND', 'android', 'No Android devices or emulators are connected.', {
193
+ tool: 'adb',
194
+ searchedLocations: ['adb devices -l'],
195
+ selectedProvider: 'adb',
196
+ rawVersion: android.adbVersion || null,
197
+ remediation: ['Start an emulator or connect a device, then re-run adb devices.']
198
+ })
199
+ : null;
200
+ const diagnosticFailures = [adbFailure, envFailure, deviceFailure].filter((value) => value !== null);
201
+ const status = !hostSupported
202
+ ? 'unsupported'
203
+ : !adbAvailable
204
+ ? 'unavailable'
205
+ : (android.devices === 0 || deviceIssues.length > 0 || !android.logsAvailable || !android.envValid)
206
+ ? 'partially_ready'
207
+ : 'ready';
208
+ const deviceEntries = android.devices > 0
209
+ ? Array.from({ length: android.devices }, (_, index) => ({
210
+ id: `android-device-${index + 1}`,
211
+ state: deviceIssues.length > 0 ? 'degraded' : 'connected'
212
+ }))
213
+ : [];
214
+ const deviceDiscoveryAvailable = adbAvailable;
215
+ const installAvailable = adbAvailable && android.devices > 0 && deviceIssues.length === 0;
216
+ const logAvailable = adbAvailable && android.logsAvailable;
217
+ return {
218
+ status,
219
+ tools: {
220
+ adb: adbTool
221
+ },
222
+ providers: {
223
+ adb: { available: adbAvailable, selected: true, tool: 'adb' },
224
+ jdwp: { available: adbAvailable && android.devices > 0, selected: false, tool: 'adb' }
225
+ },
226
+ capabilities: {
227
+ device_discovery: {
228
+ availability: deviceDiscoveryAvailable,
229
+ selectedProvider: deviceDiscoveryAvailable ? 'adb' : null,
230
+ blockingFailure: deviceDiscoveryAvailable ? null : diagnosticFailures[0] ?? null
231
+ },
232
+ install_app: {
233
+ availability: installAvailable,
234
+ selectedProvider: installAvailable ? 'adb' : null,
235
+ blockingFailure: installAvailable ? null : deviceFailure ?? adbFailure ?? envFailure
236
+ },
237
+ launch_app: {
238
+ availability: installAvailable,
239
+ selectedProvider: installAvailable ? 'adb' : null,
240
+ blockingFailure: installAvailable ? null : deviceFailure ?? adbFailure ?? envFailure
241
+ },
242
+ ui_interaction: {
243
+ availability: installAvailable,
244
+ selectedProvider: installAvailable ? 'adb' : null,
245
+ blockingFailure: installAvailable ? null : deviceFailure ?? adbFailure ?? envFailure
246
+ },
247
+ screenshot: {
248
+ availability: installAvailable,
249
+ selectedProvider: installAvailable ? 'adb' : null,
250
+ blockingFailure: installAvailable ? null : deviceFailure ?? adbFailure ?? envFailure
251
+ },
252
+ log_capture: {
253
+ availability: logAvailable,
254
+ selectedProvider: logAvailable ? 'adb' : null,
255
+ blockingFailure: logAvailable ? null : adbFailure ?? envFailure
256
+ },
257
+ debug: {
258
+ availability: adbAvailable && android.devices > 0,
259
+ selectedProvider: adbAvailable && android.devices > 0 ? 'JDWP' : null,
260
+ blockingFailure: adbAvailable && android.devices > 0 ? null : deviceFailure ?? adbFailure ?? envFailure
261
+ }
262
+ },
263
+ devices: deviceEntries,
264
+ failures: diagnosticFailures
265
+ };
266
+ }
267
+ function buildIOSPlatform(ios, hostSupported, hostOs) {
268
+ const platformUnsupported = !hostSupported || hostOs === 'linux' || hostOs === 'windows';
269
+ if (platformUnsupported) {
270
+ return {
271
+ status: 'unsupported',
272
+ tools: {},
273
+ providers: {},
274
+ capabilities: {},
275
+ devices: [],
276
+ failures: [
277
+ makeFailure('PLATFORM_UNAVAILABLE', 'ios', 'iOS tooling is only supported on macOS.', {
278
+ tool: null,
279
+ searchedLocations: [],
280
+ selectedProvider: null,
281
+ rawVersion: null,
282
+ remediation: ['Run Mobile Debug MCP on macOS for iOS toolchain discovery.']
283
+ })
284
+ ]
285
+ };
286
+ }
287
+ const xcrunCmd = getXcrunCmd();
288
+ const xcrunProbe = ios.iosAvailable ? probeCommand(xcrunCmd, ['--version']) : { ok: false, raw: null, parsedVersion: null };
289
+ const idbCmd = getIdbCmd();
290
+ const idbProbe = hostSupported && hostOs === 'macos' ? probeIdb(idbCmd) : { ok: false, raw: null, parsedVersion: null };
291
+ const idbCompanionProbe = hostSupported && hostOs === 'macos' ? probeCommand('idb_companion', ['--version']) : { ok: false, raw: null, parsedVersion: null };
292
+ const xcrunTool = makeToolEntry(xcrunCmd, describeXcrunDiscovery(xcrunCmd).discoverySource, xcrunProbe.raw, xcrunProbe.ok ? 'valid' : 'invalid');
293
+ const idbTool = makeToolEntry(idbCmd, describeIdbDiscovery(idbCmd).discoverySource, idbProbe.raw, idbProbe.ok ? 'valid' : 'invalid');
294
+ const idbCompanionTool = makeToolEntry('idb_companion', 'PATH', idbCompanionProbe.raw, idbCompanionProbe.ok ? 'valid' : 'invalid');
295
+ const lldbTool = makeToolEntry('xcrun lldb', 'xcrun', xcrunProbe.raw, xcrunProbe.ok ? 'valid' : 'invalid');
296
+ const simctlAvailable = !!ios.iosAvailable;
297
+ const idbAvailable = !!idbProbe.ok;
298
+ const bootedDevices = ios.iosDevices > 0
299
+ ? Array.from({ length: ios.iosDevices }, (_, index) => ({
300
+ id: `ios-simulator-${index + 1}`,
301
+ state: 'booted'
302
+ }))
303
+ : [];
304
+ const platformUnavailableFailure = !platformUnsupported && !simctlAvailable
305
+ ? makeFailure('PLATFORM_UNAVAILABLE', 'ios', 'iOS tooling is unavailable on this host.', {
306
+ tool: 'xcrun',
307
+ searchedLocations: describeXcrunDiscovery(xcrunCmd).searchedLocations,
308
+ selectedProvider: null,
309
+ rawVersion: xcrunProbe.raw,
310
+ remediation: ['Install Xcode Command Line Tools and ensure xcrun is on PATH.']
311
+ })
312
+ : null;
313
+ const deviceFailure = simctlAvailable && !bootedDevices.length
314
+ ? makeFailure('DEVICE_NOT_FOUND', 'ios', 'No iOS simulators are booted.', {
315
+ tool: 'simctl',
316
+ searchedLocations: ['xcrun simctl list devices booted --json'],
317
+ selectedProvider: 'simctl',
318
+ rawVersion: xcrunProbe.raw,
319
+ remediation: ['Boot a simulator in Xcode or via xcrun simctl boot.']
320
+ })
321
+ : null;
322
+ const selectedInstallProvider = idbAvailable ? 'idb' : simctlAvailable ? 'simctl' : null;
323
+ const selectedDebugProvider = simctlAvailable ? 'LLDB' : null;
324
+ const selectedDiscoveryProvider = idbAvailable ? 'idb' : simctlAvailable ? 'simctl' : null;
325
+ const status = platformUnsupported
326
+ ? 'unsupported'
327
+ : !simctlAvailable
328
+ ? 'unavailable'
329
+ : !bootedDevices.length
330
+ ? 'partially_ready'
331
+ : 'ready';
332
+ const iosFailure = platformUnsupported
333
+ ? makeFailure('PLATFORM_UNAVAILABLE', 'ios', 'iOS tooling is only supported on macOS.', {
334
+ tool: null,
335
+ searchedLocations: [],
336
+ selectedProvider: null,
337
+ rawVersion: null,
338
+ remediation: ['Run Mobile Debug MCP on macOS for iOS toolchain discovery.']
339
+ })
340
+ : platformUnavailableFailure ?? deviceFailure ?? null;
341
+ const failureList = [iosFailure].filter((value) => value !== null);
342
+ return {
343
+ status,
344
+ tools: {
345
+ xcrun: xcrunTool,
346
+ idb: idbTool,
347
+ idb_companion: idbCompanionTool,
348
+ lldb: lldbTool
349
+ },
350
+ providers: {
351
+ idb: { available: idbAvailable, selected: selectedInstallProvider === 'idb', tool: 'idb' },
352
+ simctl: { available: simctlAvailable, selected: selectedInstallProvider === 'simctl', tool: 'xcrun' },
353
+ lldb: { available: simctlAvailable, selected: selectedDebugProvider === 'LLDB', tool: 'xcrun' }
354
+ },
355
+ capabilities: {
356
+ device_discovery: {
357
+ availability: !!selectedDiscoveryProvider,
358
+ selectedProvider: selectedDiscoveryProvider,
359
+ blockingFailure: selectedDiscoveryProvider ? null : iosFailure
360
+ },
361
+ install_app: {
362
+ availability: !!selectedInstallProvider && bootedDevices.length > 0,
363
+ selectedProvider: selectedInstallProvider,
364
+ blockingFailure: selectedInstallProvider && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
365
+ },
366
+ launch_app: {
367
+ availability: !!selectedInstallProvider && bootedDevices.length > 0,
368
+ selectedProvider: selectedInstallProvider,
369
+ blockingFailure: selectedInstallProvider && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
370
+ },
371
+ ui_interaction: {
372
+ availability: !!selectedInstallProvider && bootedDevices.length > 0,
373
+ selectedProvider: selectedInstallProvider,
374
+ blockingFailure: selectedInstallProvider && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
375
+ },
376
+ screenshot: {
377
+ availability: !!selectedInstallProvider && bootedDevices.length > 0,
378
+ selectedProvider: selectedInstallProvider,
379
+ blockingFailure: selectedInstallProvider && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
380
+ },
381
+ log_capture: {
382
+ availability: simctlAvailable && bootedDevices.length > 0,
383
+ selectedProvider: simctlAvailable ? 'simctl' : null,
384
+ blockingFailure: simctlAvailable && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
385
+ },
386
+ debug: {
387
+ availability: !!selectedDebugProvider,
388
+ selectedProvider: selectedDebugProvider,
389
+ blockingFailure: selectedDebugProvider ? null : iosFailure
390
+ }
391
+ },
392
+ devices: bootedDevices,
393
+ failures: failureList
394
+ };
395
+ }
396
+ function buildLegacySummary(androidStatus, iosStatus, gradle) {
397
+ const androidReady = androidStatus.status === 'ready';
398
+ const iosReady = iosStatus.status === 'ready';
399
+ const gradleReady = (gradle.issues || []).length === 0;
400
+ const overallStatus = (androidReady || iosReady || gradleReady)
401
+ ? ((androidReady && iosReady && gradleReady) ? 'ready' : 'degraded')
402
+ : 'blocked';
403
+ return {
404
+ overall: overallStatus,
405
+ android: {
406
+ ready: androidReady,
407
+ summary: androidStatus.status === 'ready'
408
+ ? `${androidStatus.devices.length} Android device(s) connected; log access available`
409
+ : androidStatus.status === 'partially_ready'
410
+ ? 'Android toolchain available but device-dependent capabilities are limited'
411
+ : androidStatus.status === 'unsupported'
412
+ ? 'Android unsupported on this host'
413
+ : 'ADB unavailable',
414
+ blockers: androidStatus.failures.map((failure) => failure.message)
415
+ },
416
+ ios: {
417
+ ready: iosReady,
418
+ summary: iosStatus.status === 'ready'
419
+ ? `${iosStatus.devices.length} iOS simulator(s) booted`
420
+ : iosStatus.status === 'partially_ready'
421
+ ? 'iOS toolchain available but device-dependent capabilities are limited'
422
+ : iosStatus.status === 'unsupported'
423
+ ? 'iOS unsupported on this host'
424
+ : 'xcrun unavailable',
425
+ blockers: iosStatus.failures.map((failure) => failure.message)
426
+ },
427
+ gradle: {
428
+ ready: gradleReady,
429
+ summary: !gradle.gradleJavaHome
430
+ ? 'No explicit Gradle JDK override detected'
431
+ : gradleReady
432
+ ? `Gradle JDK configured at ${gradle.gradleJavaHome}`
433
+ : `Gradle JDK override invalid: ${gradle.gradleJavaHome}`,
434
+ blockers: gradle.issues || [],
435
+ suggestedFixes: gradle.suggestedFixes || []
436
+ }
437
+ };
438
+ }
4
439
  export async function getSystemStatus() {
440
+ const hostOs = normalizeHostOs((process.env.MOBILE_DEBUG_MCP_HOST_OS || process.platform));
441
+ if (!isSupportedHost(hostOs)) {
442
+ return buildUnsupportedHostStatus(hostOs);
443
+ }
5
444
  try {
6
- const android = await checkAndroid();
7
- const ios = await checkIOS();
8
- const gradle = await checkGradle();
9
- const issues = [...android.issues, ...ios.issues, ...(gradle.issues || [])];
445
+ const [androidRaw, gradleRaw] = await Promise.all([checkAndroid(), checkGradle()]);
446
+ const iosRaw = hostOs === 'macos'
447
+ ? await checkIOS()
448
+ : {
449
+ iosAvailable: false,
450
+ iosDevices: 0,
451
+ issues: ['iOS tooling is only supported on macOS.']
452
+ };
453
+ const androidStatus = buildAndroidPlatform(androidRaw, true);
454
+ const iosStatus = buildIOSPlatform(iosRaw, hostOs === 'macos', hostOs);
455
+ const gradle = gradleRaw;
456
+ const issues = [...androidRaw.issues, ...iosRaw.issues, ...(gradle.issues || [])];
10
457
  const success = issues.length === 0;
11
- const androidReady = android.adbAvailable && android.devices > 0 && !android.issues.some((issue) => /unauthorized|offline/i.test(issue));
12
- const iosReady = ios.iosAvailable && ios.iosDevices > 0;
13
- const gradleReady = (gradle.issues || []).length === 0;
458
+ const androidReady = androidRaw.adbAvailable && androidRaw.devices > 0 && !androidRaw.issues.some((issue) => /unauthorized|offline/i.test(issue));
459
+ const iosReady = hostOs === 'macos' ? (iosRaw.iosAvailable && iosRaw.iosDevices > 0) : false;
14
460
  const overallStatus = success ? 'ready' : (androidReady || iosReady ? 'degraded' : 'blocked');
15
- const androidSummary = !android.adbAvailable
16
- ? 'ADB unavailable'
17
- : android.devices === 0
18
- ? 'ADB available but no Android devices connected'
19
- : android.logsAvailable
20
- ? `${android.devices} Android device(s) connected; log access available`
21
- : `${android.devices} Android device(s) connected; log access unavailable`;
22
- const iosSummary = !ios.iosAvailable
23
- ? 'xcrun unavailable'
24
- : ios.iosDevices === 0
25
- ? 'xcrun available but no iOS simulators booted'
26
- : `${ios.iosDevices} iOS simulator(s) booted`;
27
- const gradleSummary = !gradle.gradleJavaHome
28
- ? 'No explicit Gradle JDK override detected'
29
- : gradleReady
30
- ? `Gradle JDK configured at ${gradle.gradleJavaHome}`
31
- : `Gradle JDK override invalid: ${gradle.gradleJavaHome}`;
461
+ const javaHome = gradle.gradleJavaHome || (await detectJavaHome().catch(() => undefined));
462
+ const javaBin = javaHome ? resolveJavaBinaryPath(javaHome, hostOs) : null;
463
+ const javaProbe = javaBin ? probeCommand(javaBin, ['-version']) : { ok: false, raw: null, parsedVersion: null };
464
+ const javaTool = makeToolEntry(javaBin, describeJavaDiscovery(javaHome ?? null).discoverySource, javaProbe.raw, javaProbe.ok ? 'valid' : (javaHome ? 'invalid' : 'missing'));
32
465
  return {
33
466
  success,
34
467
  status: overallStatus,
35
- adbAvailable: android.adbAvailable,
36
- adbVersion: android.adbVersion,
37
- devices: android.devices,
38
- deviceStates: android.deviceStates,
39
- logsAvailable: android.logsAvailable,
40
- envValid: android.envValid,
468
+ adbAvailable: androidRaw.adbAvailable,
469
+ adbVersion: androidRaw.adbVersion,
470
+ devices: androidRaw.devices,
471
+ deviceStates: androidRaw.deviceStates,
472
+ logsAvailable: androidRaw.logsAvailable,
473
+ envValid: androidRaw.envValid,
41
474
  issues,
42
- appInstalled: android.appInstalled,
43
- iosAvailable: ios.iosAvailable,
44
- iosDevices: ios.iosDevices,
475
+ appInstalled: androidRaw.appInstalled,
476
+ iosAvailable: iosRaw.iosAvailable,
477
+ iosDevices: iosRaw.iosDevices,
45
478
  gradleJavaHome: gradle.gradleJavaHome,
46
479
  gradleValid: gradle.gradleValid,
47
480
  gradleFilesChecked: gradle.filesChecked,
48
481
  gradleSuggestedFixes: gradle.suggestedFixes,
49
- summary: {
50
- overall: overallStatus,
482
+ host: {
483
+ os: hostOs,
484
+ supported: true
485
+ },
486
+ android: {
487
+ ...androidStatus,
488
+ status: androidStatus.status
489
+ },
490
+ ios: {
491
+ ...iosStatus,
492
+ status: iosStatus.status
493
+ },
494
+ summary: buildLegacySummary(androidStatus, iosStatus, gradle),
495
+ tools: {
51
496
  android: {
52
- ready: androidReady,
53
- summary: androidSummary,
54
- blockers: android.issues
55
- },
56
- ios: {
57
- ready: iosReady,
58
- summary: iosSummary,
59
- blockers: ios.issues
497
+ adb: androidStatus.tools.adb,
498
+ java: javaTool
60
499
  },
500
+ ios: iosStatus.tools,
61
501
  gradle: {
62
- ready: gradleReady,
63
- summary: gradleSummary,
64
- blockers: gradle.issues || [],
65
- suggestedFixes: gradle.suggestedFixes || []
502
+ java: javaTool
66
503
  }
67
504
  }
68
505
  };