mobile-debug-mcp 0.30.2 → 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.
@@ -1,74 +1,596 @@
1
+ import { spawnSync } from 'child_process'
2
+ import path from 'path'
3
+
1
4
  import { checkAndroid } from './android.js'
2
- import { checkIOS } from './ios.js'
3
5
  import { checkGradle } from './gradle.js'
6
+ import { checkIOS } from './ios.js'
7
+ import { resolveAdbCmd } from '../utils/android/utils.js'
8
+ import { getIdbCmd, getXcrunCmd } from '../utils/ios/utils.js'
9
+ import { probeIdb } from '../utils/cli/idb/idb-helper.js'
10
+ import { detectJavaHome } from '../utils/java.js'
11
+
12
+ type HostOs = 'macos' | 'linux' | 'windows' | 'freebsd' | 'openbsd' | 'netbsd' | 'sunos' | 'unknown'
13
+ type PlatformStatus = 'ready' | 'partially_ready' | 'unavailable' | 'unsupported'
14
+ type ValidationStatus = 'valid' | 'invalid' | 'missing' | 'unsupported' | 'unknown'
15
+
16
+ interface DiagnosticFailure {
17
+ code: string
18
+ platform: 'android' | 'ios'
19
+ tool: string | null
20
+ message: string
21
+ searchedLocations: string[]
22
+ selectedProvider: string | null
23
+ rawVersion: string | null
24
+ remediation: string[]
25
+ }
26
+
27
+ interface ToolEntry {
28
+ resolvedPath: string | null
29
+ discoverySource: string | null
30
+ parsedVersion: string | null
31
+ rawVersionOutput: string | null
32
+ validationStatus: ValidationStatus
33
+ }
34
+
35
+ interface CapabilityEntry {
36
+ availability: boolean
37
+ selectedProvider: string | null
38
+ blockingFailure: DiagnosticFailure | null
39
+ }
40
+
41
+ interface PlatformEntry {
42
+ status: PlatformStatus
43
+ tools: Record<string, ToolEntry>
44
+ providers: Record<string, { available: boolean; selected: boolean; tool: string | null }>
45
+ capabilities: Record<string, CapabilityEntry>
46
+ devices: Array<Record<string, unknown>>
47
+ failures: DiagnosticFailure[]
48
+ }
49
+
50
+ function normalizeHostOs(platform: NodeJS.Platform): HostOs {
51
+ switch (platform) {
52
+ case 'darwin':
53
+ return 'macos'
54
+ case 'linux':
55
+ return 'linux'
56
+ case 'win32':
57
+ return 'windows'
58
+ case 'freebsd':
59
+ case 'openbsd':
60
+ case 'netbsd':
61
+ case 'sunos':
62
+ return platform
63
+ default:
64
+ return 'unknown'
65
+ }
66
+ }
67
+
68
+ function isSupportedHost(hostOs: HostOs): boolean {
69
+ return hostOs === 'macos' || hostOs === 'linux' || hostOs === 'windows'
70
+ }
71
+
72
+ function firstVersionToken(raw: string | null | undefined): string | null {
73
+ if (!raw) return null
74
+ const match = String(raw).match(/(\d+(?:\.\d+)+|\d+)/)
75
+ return match?.[1] ?? null
76
+ }
77
+
78
+ function probeCommand(command: string, args: string[], timeoutMs = 2000): { ok: boolean, raw: string | null, parsedVersion: string | null } {
79
+ try {
80
+ const result = spawnSync(command, args, { encoding: 'utf8', timeout: timeoutMs, stdio: ['ignore', 'pipe', 'pipe'] })
81
+ const raw = `${result.stdout || ''}${result.stderr || ''}`.trim() || null
82
+ return {
83
+ ok: result.status === 0,
84
+ raw,
85
+ parsedVersion: firstVersionToken(raw)
86
+ }
87
+ } catch {
88
+ return { ok: false, raw: null, parsedVersion: null }
89
+ }
90
+ }
91
+
92
+ function makeFailure(
93
+ code: string,
94
+ platform: 'android' | 'ios',
95
+ message: string,
96
+ opts: Partial<DiagnosticFailure> = {}
97
+ ): DiagnosticFailure {
98
+ return {
99
+ code,
100
+ platform,
101
+ tool: opts.tool ?? null,
102
+ message,
103
+ searchedLocations: opts.searchedLocations ?? [],
104
+ selectedProvider: opts.selectedProvider ?? null,
105
+ rawVersion: opts.rawVersion ?? null,
106
+ remediation: opts.remediation ?? []
107
+ }
108
+ }
109
+
110
+ function makeToolEntry(
111
+ resolvedPath: string | null,
112
+ discoverySource: string | null,
113
+ rawVersionOutput: string | null,
114
+ validationStatus: ValidationStatus
115
+ ): ToolEntry {
116
+ return {
117
+ resolvedPath,
118
+ discoverySource,
119
+ parsedVersion: firstVersionToken(rawVersionOutput),
120
+ rawVersionOutput,
121
+ validationStatus
122
+ }
123
+ }
124
+
125
+ function describeAdbDiscovery(adbCmd: string): { discoverySource: string | null, searchedLocations: string[] } {
126
+ const searchedLocations: string[] = ['ADB_PATH', 'ANDROID_SDK_ROOT/platform-tools/adb', 'ANDROID_HOME/platform-tools/adb', '~/Library/Android/sdk/platform-tools/adb', 'PATH']
127
+ if (process.env.ADB_PATH && process.env.ADB_PATH.trim()) return { discoverySource: 'ADB_PATH', searchedLocations }
128
+ if (process.env.ANDROID_SDK_ROOT) return { discoverySource: 'ANDROID_SDK_ROOT', searchedLocations }
129
+ if (process.env.ANDROID_HOME) return { discoverySource: 'ANDROID_HOME', searchedLocations }
130
+ if (adbCmd.includes('Library/Android/sdk')) return { discoverySource: 'HOME_SDK', searchedLocations }
131
+ if (adbCmd === 'adb') return { discoverySource: 'PATH_OR_FALLBACK', searchedLocations }
132
+ return { discoverySource: 'PATH', searchedLocations }
133
+ }
134
+
135
+ function describeXcrunDiscovery(xcrunCmd: string): { discoverySource: string | null, searchedLocations: string[] } {
136
+ const searchedLocations: string[] = ['XCRUN_PATH', 'PATH']
137
+ if (process.env.XCRUN_PATH && process.env.XCRUN_PATH.trim()) return { discoverySource: 'XCRUN_PATH', searchedLocations }
138
+ return { discoverySource: xcrunCmd === 'xcrun' ? 'PATH' : 'PATH', searchedLocations }
139
+ }
140
+
141
+ function describeIdbDiscovery(idbCmd: string): { discoverySource: string | null, searchedLocations: string[] } {
142
+ const searchedLocations: string[] = ['MCP_IDB_PATH', 'IDB_PATH', 'mcp.config.json', '~/.mcp/config.json', 'PATH', '/opt/homebrew/bin/idb', '/usr/local/bin/idb']
143
+ if (process.env.MCP_IDB_PATH && process.env.MCP_IDB_PATH.trim()) return { discoverySource: 'MCP_IDB_PATH', searchedLocations }
144
+ if (process.env.IDB_PATH && process.env.IDB_PATH.trim()) return { discoverySource: 'IDB_PATH', searchedLocations }
145
+ return { discoverySource: idbCmd === 'idb' ? 'PATH_OR_FALLBACK' : 'PATH', searchedLocations }
146
+ }
147
+
148
+ function describeJavaDiscovery(javaHome: string | null): { discoverySource: string | null, searchedLocations: string[] } {
149
+ const searchedLocations: string[] = ['GRADLE_JAVA_HOME', 'JAVA_HOME', 'GRADLE_HOME', 'detectJavaHome']
150
+ if (process.env.GRADLE_JAVA_HOME && process.env.GRADLE_JAVA_HOME.trim()) return { discoverySource: 'GRADLE_JAVA_HOME', searchedLocations }
151
+ if (process.env.JAVA_HOME && process.env.JAVA_HOME.trim()) return { discoverySource: 'JAVA_HOME', searchedLocations }
152
+ if (process.env.GRADLE_HOME && process.env.GRADLE_HOME.trim()) return { discoverySource: 'GRADLE_HOME', searchedLocations }
153
+ return { discoverySource: javaHome ? 'detected' : null, searchedLocations }
154
+ }
155
+
156
+ function resolveJavaBinaryPath(javaHome: string, hostOs: HostOs): string {
157
+ const javaBinary = hostOs === 'windows' ? 'java.exe' : 'java'
158
+ return path.join(javaHome, 'bin', javaBinary)
159
+ }
160
+
161
+ function buildUnsupportedHostStatus(hostOs: HostOs) {
162
+ const failureMessage = `Host operating system '${hostOs}' is not supported.`
163
+ const remediation = ['Run Mobile Debug MCP on macOS, Linux, or Windows.']
164
+ const failureAndroid = makeFailure('HOST_OS_UNSUPPORTED', 'android', failureMessage, {
165
+ remediation
166
+ })
167
+ const failureIos = makeFailure('HOST_OS_UNSUPPORTED', 'ios', failureMessage, {
168
+ remediation
169
+ })
170
+
171
+ return {
172
+ success: false,
173
+ status: 'blocked',
174
+ adbAvailable: false,
175
+ adbVersion: '',
176
+ devices: 0,
177
+ deviceStates: '',
178
+ logsAvailable: false,
179
+ envValid: false,
180
+ issues: [failureMessage],
181
+ appInstalled: undefined,
182
+ iosAvailable: false,
183
+ iosDevices: 0,
184
+ gradleJavaHome: undefined,
185
+ gradleValid: false,
186
+ gradleFilesChecked: [],
187
+ gradleSuggestedFixes: [],
188
+ host: { os: hostOs, supported: false },
189
+ android: {
190
+ status: 'unsupported',
191
+ tools: {},
192
+ providers: {},
193
+ capabilities: {},
194
+ devices: [],
195
+ failures: [failureAndroid]
196
+ },
197
+ ios: {
198
+ status: 'unsupported',
199
+ tools: {},
200
+ providers: {},
201
+ capabilities: {},
202
+ devices: [],
203
+ failures: [failureIos]
204
+ },
205
+ summary: {
206
+ overall: 'blocked',
207
+ android: { ready: false, summary: `Host operating system '${hostOs}' is not supported.`, blockers: [failureMessage] },
208
+ ios: { ready: false, summary: `Host operating system '${hostOs}' is not supported.`, blockers: [failureMessage] },
209
+ gradle: { ready: false, summary: 'Gradle status unavailable on unsupported host', blockers: [], suggestedFixes: [] }
210
+ }
211
+ }
212
+ }
213
+
214
+ function buildAndroidPlatform(android: Awaited<ReturnType<typeof checkAndroid>>, hostSupported: boolean): PlatformEntry {
215
+ const resolvedAdbCmd = resolveAdbCmd()
216
+ const adbDiscovery = describeAdbDiscovery(resolvedAdbCmd)
217
+ const adbTool = makeToolEntry(
218
+ resolvedAdbCmd,
219
+ adbDiscovery.discoverySource,
220
+ android.adbVersion || null,
221
+ android.adbAvailable ? 'valid' : 'invalid'
222
+ )
223
+
224
+ const adbAvailable = !!android.adbAvailable
225
+ const deviceIssues = android.issues.filter((issue) => /unauthorized|offline|No Android devices connected/i.test(issue))
226
+ const envFailure = !android.envValid
227
+ ? makeFailure('TOOL_VALIDATION_FAILED', 'android', 'Android environment prerequisites are incomplete.', {
228
+ tool: 'adb',
229
+ searchedLocations: ['ANDROID_SDK_ROOT', 'ANDROID_HOME', 'PATH'],
230
+ selectedProvider: 'adb',
231
+ rawVersion: android.adbVersion || null,
232
+ remediation: ['Set ANDROID_SDK_ROOT or ANDROID_HOME, or ensure adb is on PATH.']
233
+ })
234
+ : null
235
+
236
+ const adbFailure = !adbAvailable
237
+ ? makeFailure(
238
+ process.env.ADB_PATH && process.env.ADB_PATH.trim() ? 'TOOL_NOT_EXECUTABLE' : 'TOOL_NOT_FOUND',
239
+ 'android',
240
+ 'Android adb could not be resolved or executed.',
241
+ {
242
+ tool: 'adb',
243
+ searchedLocations: adbDiscovery.searchedLocations,
244
+ selectedProvider: null,
245
+ rawVersion: android.adbVersion || null,
246
+ remediation: [
247
+ 'Install Android platform-tools.',
248
+ 'Set ADB_PATH or ANDROID_SDK_ROOT/ANDROID_HOME if adb is installed in a non-standard location.'
249
+ ]
250
+ }
251
+ )
252
+ : null
253
+
254
+ const deviceFailure = adbAvailable && android.devices === 0
255
+ ? makeFailure('DEVICE_NOT_FOUND', 'android', 'No Android devices or emulators are connected.', {
256
+ tool: 'adb',
257
+ searchedLocations: ['adb devices -l'],
258
+ selectedProvider: 'adb',
259
+ rawVersion: android.adbVersion || null,
260
+ remediation: ['Start an emulator or connect a device, then re-run adb devices.']
261
+ })
262
+ : null
263
+
264
+ const diagnosticFailures = [adbFailure, envFailure, deviceFailure].filter((value): value is DiagnosticFailure => value !== null)
265
+
266
+ const status: PlatformStatus = !hostSupported
267
+ ? 'unsupported'
268
+ : !adbAvailable
269
+ ? 'unavailable'
270
+ : (android.devices === 0 || deviceIssues.length > 0 || !android.logsAvailable || !android.envValid)
271
+ ? 'partially_ready'
272
+ : 'ready'
273
+
274
+ const deviceEntries = android.devices > 0
275
+ ? Array.from({ length: android.devices }, (_, index) => ({
276
+ id: `android-device-${index + 1}`,
277
+ state: deviceIssues.length > 0 ? 'degraded' : 'connected'
278
+ }))
279
+ : []
280
+
281
+ const deviceDiscoveryAvailable = adbAvailable
282
+ const installAvailable = adbAvailable && android.devices > 0 && deviceIssues.length === 0
283
+ const logAvailable = adbAvailable && android.logsAvailable
284
+
285
+ return {
286
+ status,
287
+ tools: {
288
+ adb: adbTool
289
+ },
290
+ providers: {
291
+ adb: { available: adbAvailable, selected: true, tool: 'adb' },
292
+ jdwp: { available: adbAvailable && android.devices > 0, selected: false, tool: 'adb' }
293
+ },
294
+ capabilities: {
295
+ device_discovery: {
296
+ availability: deviceDiscoveryAvailable,
297
+ selectedProvider: deviceDiscoveryAvailable ? 'adb' : null,
298
+ blockingFailure: deviceDiscoveryAvailable ? null : diagnosticFailures[0] ?? null
299
+ },
300
+ install_app: {
301
+ availability: installAvailable,
302
+ selectedProvider: installAvailable ? 'adb' : null,
303
+ blockingFailure: installAvailable ? null : deviceFailure ?? adbFailure ?? envFailure
304
+ },
305
+ launch_app: {
306
+ availability: installAvailable,
307
+ selectedProvider: installAvailable ? 'adb' : null,
308
+ blockingFailure: installAvailable ? null : deviceFailure ?? adbFailure ?? envFailure
309
+ },
310
+ ui_interaction: {
311
+ availability: installAvailable,
312
+ selectedProvider: installAvailable ? 'adb' : null,
313
+ blockingFailure: installAvailable ? null : deviceFailure ?? adbFailure ?? envFailure
314
+ },
315
+ screenshot: {
316
+ availability: installAvailable,
317
+ selectedProvider: installAvailable ? 'adb' : null,
318
+ blockingFailure: installAvailable ? null : deviceFailure ?? adbFailure ?? envFailure
319
+ },
320
+ log_capture: {
321
+ availability: logAvailable,
322
+ selectedProvider: logAvailable ? 'adb' : null,
323
+ blockingFailure: logAvailable ? null : adbFailure ?? envFailure
324
+ },
325
+ debug: {
326
+ availability: adbAvailable && android.devices > 0,
327
+ selectedProvider: adbAvailable && android.devices > 0 ? 'JDWP' : null,
328
+ blockingFailure: adbAvailable && android.devices > 0 ? null : deviceFailure ?? adbFailure ?? envFailure
329
+ }
330
+ },
331
+ devices: deviceEntries,
332
+ failures: diagnosticFailures
333
+ }
334
+ }
335
+
336
+ function buildIOSPlatform(ios: Awaited<ReturnType<typeof checkIOS>>, hostSupported: boolean, hostOs: HostOs): PlatformEntry {
337
+ const platformUnsupported = !hostSupported || hostOs === 'linux' || hostOs === 'windows'
338
+ if (platformUnsupported) {
339
+ return {
340
+ status: 'unsupported',
341
+ tools: {},
342
+ providers: {},
343
+ capabilities: {},
344
+ devices: [],
345
+ failures: [
346
+ makeFailure('PLATFORM_UNAVAILABLE', 'ios', 'iOS tooling is only supported on macOS.', {
347
+ tool: null,
348
+ searchedLocations: [],
349
+ selectedProvider: null,
350
+ rawVersion: null,
351
+ remediation: ['Run Mobile Debug MCP on macOS for iOS toolchain discovery.']
352
+ })
353
+ ]
354
+ }
355
+ }
356
+
357
+ const xcrunCmd = getXcrunCmd()
358
+ const xcrunProbe = ios.iosAvailable ? probeCommand(xcrunCmd, ['--version']) : { ok: false, raw: null, parsedVersion: null }
359
+ const idbCmd = getIdbCmd()
360
+ const idbProbe = hostSupported && hostOs === 'macos' ? probeIdb(idbCmd) : { ok: false, raw: null, parsedVersion: null }
361
+ const idbCompanionProbe = hostSupported && hostOs === 'macos' ? probeCommand('idb_companion', ['--version']) : { ok: false, raw: null, parsedVersion: null }
362
+ const xcrunTool = makeToolEntry(xcrunCmd, describeXcrunDiscovery(xcrunCmd).discoverySource, xcrunProbe.raw, xcrunProbe.ok ? 'valid' : 'invalid')
363
+ const idbTool = makeToolEntry(idbCmd, describeIdbDiscovery(idbCmd).discoverySource, idbProbe.raw, idbProbe.ok ? 'valid' : 'invalid')
364
+ const idbCompanionTool = makeToolEntry('idb_companion', 'PATH', idbCompanionProbe.raw, idbCompanionProbe.ok ? 'valid' : 'invalid')
365
+ const lldbTool = makeToolEntry('xcrun lldb', 'xcrun', xcrunProbe.raw, xcrunProbe.ok ? 'valid' : 'invalid')
366
+
367
+ const simctlAvailable = !!ios.iosAvailable
368
+ const idbAvailable = !!idbProbe.ok
369
+ const bootedDevices = ios.iosDevices > 0
370
+ ? Array.from({ length: ios.iosDevices }, (_, index) => ({
371
+ id: `ios-simulator-${index + 1}`,
372
+ state: 'booted'
373
+ }))
374
+ : []
375
+
376
+ const platformUnavailableFailure = !platformUnsupported && !simctlAvailable
377
+ ? makeFailure('PLATFORM_UNAVAILABLE', 'ios', 'iOS tooling is unavailable on this host.', {
378
+ tool: 'xcrun',
379
+ searchedLocations: describeXcrunDiscovery(xcrunCmd).searchedLocations,
380
+ selectedProvider: null,
381
+ rawVersion: xcrunProbe.raw,
382
+ remediation: ['Install Xcode Command Line Tools and ensure xcrun is on PATH.']
383
+ })
384
+ : null
385
+
386
+ const deviceFailure = simctlAvailable && !bootedDevices.length
387
+ ? makeFailure('DEVICE_NOT_FOUND', 'ios', 'No iOS simulators are booted.', {
388
+ tool: 'simctl',
389
+ searchedLocations: ['xcrun simctl list devices booted --json'],
390
+ selectedProvider: 'simctl',
391
+ rawVersion: xcrunProbe.raw,
392
+ remediation: ['Boot a simulator in Xcode or via xcrun simctl boot.']
393
+ })
394
+ : null
395
+
396
+ const selectedInstallProvider = idbAvailable ? 'idb' : simctlAvailable ? 'simctl' : null
397
+ const selectedDebugProvider = simctlAvailable ? 'LLDB' : null
398
+ const selectedDiscoveryProvider = idbAvailable ? 'idb' : simctlAvailable ? 'simctl' : null
399
+
400
+ const status: PlatformStatus = platformUnsupported
401
+ ? 'unsupported'
402
+ : !simctlAvailable
403
+ ? 'unavailable'
404
+ : !bootedDevices.length
405
+ ? 'partially_ready'
406
+ : 'ready'
407
+
408
+ const iosFailure = platformUnsupported
409
+ ? makeFailure('PLATFORM_UNAVAILABLE', 'ios', 'iOS tooling is only supported on macOS.', {
410
+ tool: null,
411
+ searchedLocations: [],
412
+ selectedProvider: null,
413
+ rawVersion: null,
414
+ remediation: ['Run Mobile Debug MCP on macOS for iOS toolchain discovery.']
415
+ })
416
+ : platformUnavailableFailure ?? deviceFailure ?? null
417
+
418
+ const failureList = [iosFailure].filter((value): value is DiagnosticFailure => value !== null)
419
+
420
+ return {
421
+ status,
422
+ tools: {
423
+ xcrun: xcrunTool,
424
+ idb: idbTool,
425
+ idb_companion: idbCompanionTool,
426
+ lldb: lldbTool
427
+ },
428
+ providers: {
429
+ idb: { available: idbAvailable, selected: selectedInstallProvider === 'idb', tool: 'idb' },
430
+ simctl: { available: simctlAvailable, selected: selectedInstallProvider === 'simctl', tool: 'xcrun' },
431
+ lldb: { available: simctlAvailable, selected: selectedDebugProvider === 'LLDB', tool: 'xcrun' }
432
+ },
433
+ capabilities: {
434
+ device_discovery: {
435
+ availability: !!selectedDiscoveryProvider,
436
+ selectedProvider: selectedDiscoveryProvider,
437
+ blockingFailure: selectedDiscoveryProvider ? null : iosFailure
438
+ },
439
+ install_app: {
440
+ availability: !!selectedInstallProvider && bootedDevices.length > 0,
441
+ selectedProvider: selectedInstallProvider,
442
+ blockingFailure: selectedInstallProvider && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
443
+ },
444
+ launch_app: {
445
+ availability: !!selectedInstallProvider && bootedDevices.length > 0,
446
+ selectedProvider: selectedInstallProvider,
447
+ blockingFailure: selectedInstallProvider && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
448
+ },
449
+ ui_interaction: {
450
+ availability: !!selectedInstallProvider && bootedDevices.length > 0,
451
+ selectedProvider: selectedInstallProvider,
452
+ blockingFailure: selectedInstallProvider && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
453
+ },
454
+ screenshot: {
455
+ availability: !!selectedInstallProvider && bootedDevices.length > 0,
456
+ selectedProvider: selectedInstallProvider,
457
+ blockingFailure: selectedInstallProvider && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
458
+ },
459
+ log_capture: {
460
+ availability: simctlAvailable && bootedDevices.length > 0,
461
+ selectedProvider: simctlAvailable ? 'simctl' : null,
462
+ blockingFailure: simctlAvailable && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
463
+ },
464
+ debug: {
465
+ availability: !!selectedDebugProvider,
466
+ selectedProvider: selectedDebugProvider,
467
+ blockingFailure: selectedDebugProvider ? null : iosFailure
468
+ }
469
+ },
470
+ devices: bootedDevices,
471
+ failures: failureList
472
+ }
473
+ }
474
+
475
+ function buildLegacySummary(androidStatus: PlatformEntry, iosStatus: PlatformEntry, gradle: Awaited<ReturnType<typeof checkGradle>>) {
476
+ const androidReady = androidStatus.status === 'ready'
477
+ const iosReady = iosStatus.status === 'ready'
478
+ const gradleReady = (gradle.issues || []).length === 0
479
+ const overallStatus = (androidReady || iosReady || gradleReady)
480
+ ? ((androidReady && iosReady && gradleReady) ? 'ready' : 'degraded')
481
+ : 'blocked'
482
+
483
+ return {
484
+ overall: overallStatus,
485
+ android: {
486
+ ready: androidReady,
487
+ summary: androidStatus.status === 'ready'
488
+ ? `${androidStatus.devices.length} Android device(s) connected; log access available`
489
+ : androidStatus.status === 'partially_ready'
490
+ ? 'Android toolchain available but device-dependent capabilities are limited'
491
+ : androidStatus.status === 'unsupported'
492
+ ? 'Android unsupported on this host'
493
+ : 'ADB unavailable',
494
+ blockers: androidStatus.failures.map((failure) => failure.message)
495
+ },
496
+ ios: {
497
+ ready: iosReady,
498
+ summary: iosStatus.status === 'ready'
499
+ ? `${iosStatus.devices.length} iOS simulator(s) booted`
500
+ : iosStatus.status === 'partially_ready'
501
+ ? 'iOS toolchain available but device-dependent capabilities are limited'
502
+ : iosStatus.status === 'unsupported'
503
+ ? 'iOS unsupported on this host'
504
+ : 'xcrun unavailable',
505
+ blockers: iosStatus.failures.map((failure) => failure.message)
506
+ },
507
+ gradle: {
508
+ ready: gradleReady,
509
+ summary: !gradle.gradleJavaHome
510
+ ? 'No explicit Gradle JDK override detected'
511
+ : gradleReady
512
+ ? `Gradle JDK configured at ${gradle.gradleJavaHome}`
513
+ : `Gradle JDK override invalid: ${gradle.gradleJavaHome}`,
514
+ blockers: gradle.issues || [],
515
+ suggestedFixes: gradle.suggestedFixes || []
516
+ }
517
+ }
518
+ }
4
519
 
5
520
  export async function getSystemStatus() {
521
+ const hostOs = normalizeHostOs((process.env.MOBILE_DEBUG_MCP_HOST_OS || process.platform) as NodeJS.Platform)
522
+ if (!isSupportedHost(hostOs)) {
523
+ return buildUnsupportedHostStatus(hostOs)
524
+ }
525
+
6
526
  try {
7
- const android = await checkAndroid()
8
- const ios = await checkIOS()
9
- const gradle = await checkGradle()
10
- const issues = [...android.issues, ...ios.issues, ...(gradle.issues || [])]
527
+ const [androidRaw, gradleRaw] = await Promise.all([checkAndroid(), checkGradle()])
528
+ const iosRaw = hostOs === 'macos'
529
+ ? await checkIOS()
530
+ : {
531
+ iosAvailable: false,
532
+ iosDevices: 0,
533
+ issues: ['iOS tooling is only supported on macOS.']
534
+ }
535
+
536
+ const androidStatus = buildAndroidPlatform(androidRaw, true)
537
+ const iosStatus = buildIOSPlatform(iosRaw as Awaited<ReturnType<typeof checkIOS>>, hostOs === 'macos', hostOs)
538
+ const gradle = gradleRaw
11
539
 
540
+ const issues = [...androidRaw.issues, ...iosRaw.issues, ...(gradle.issues || [])]
12
541
  const success = issues.length === 0
13
- const androidReady = android.adbAvailable && android.devices > 0 && !android.issues.some((issue) => /unauthorized|offline/i.test(issue))
14
- const iosReady = ios.iosAvailable && ios.iosDevices > 0
15
- const gradleReady = (gradle.issues || []).length === 0
542
+ const androidReady = androidRaw.adbAvailable && androidRaw.devices > 0 && !androidRaw.issues.some((issue) => /unauthorized|offline/i.test(issue))
543
+ const iosReady = hostOs === 'macos' ? (iosRaw.iosAvailable && iosRaw.iosDevices > 0) : false
16
544
  const overallStatus = success ? 'ready' : (androidReady || iosReady ? 'degraded' : 'blocked')
17
545
 
18
- const androidSummary = !android.adbAvailable
19
- ? 'ADB unavailable'
20
- : android.devices === 0
21
- ? 'ADB available but no Android devices connected'
22
- : android.logsAvailable
23
- ? `${android.devices} Android device(s) connected; log access available`
24
- : `${android.devices} Android device(s) connected; log access unavailable`
25
-
26
- const iosSummary = !ios.iosAvailable
27
- ? 'xcrun unavailable'
28
- : ios.iosDevices === 0
29
- ? 'xcrun available but no iOS simulators booted'
30
- : `${ios.iosDevices} iOS simulator(s) booted`
31
-
32
- const gradleSummary = !gradle.gradleJavaHome
33
- ? 'No explicit Gradle JDK override detected'
34
- : gradleReady
35
- ? `Gradle JDK configured at ${gradle.gradleJavaHome}`
36
- : `Gradle JDK override invalid: ${gradle.gradleJavaHome}`
546
+ const javaHome = gradle.gradleJavaHome || (await detectJavaHome().catch(() => undefined))
547
+ const javaBin = javaHome ? resolveJavaBinaryPath(javaHome, hostOs) : null
548
+ const javaProbe = javaBin ? probeCommand(javaBin, ['-version']) : { ok: false, raw: null, parsedVersion: null }
549
+ const javaTool = makeToolEntry(
550
+ javaBin,
551
+ describeJavaDiscovery(javaHome ?? null).discoverySource,
552
+ javaProbe.raw,
553
+ javaProbe.ok ? 'valid' : (javaHome ? 'invalid' : 'missing')
554
+ )
37
555
 
38
556
  return {
39
557
  success,
40
558
  status: overallStatus,
41
- adbAvailable: android.adbAvailable,
42
- adbVersion: android.adbVersion,
43
- devices: android.devices,
44
- deviceStates: android.deviceStates,
45
- logsAvailable: android.logsAvailable,
46
- envValid: android.envValid,
559
+ adbAvailable: androidRaw.adbAvailable,
560
+ adbVersion: androidRaw.adbVersion,
561
+ devices: androidRaw.devices,
562
+ deviceStates: androidRaw.deviceStates,
563
+ logsAvailable: androidRaw.logsAvailable,
564
+ envValid: androidRaw.envValid,
47
565
  issues,
48
- appInstalled: android.appInstalled,
49
- iosAvailable: ios.iosAvailable,
50
- iosDevices: ios.iosDevices,
566
+ appInstalled: androidRaw.appInstalled,
567
+ iosAvailable: iosRaw.iosAvailable,
568
+ iosDevices: iosRaw.iosDevices,
51
569
  gradleJavaHome: gradle.gradleJavaHome,
52
570
  gradleValid: gradle.gradleValid,
53
571
  gradleFilesChecked: gradle.filesChecked,
54
572
  gradleSuggestedFixes: gradle.suggestedFixes,
55
- summary: {
56
- overall: overallStatus,
573
+ host: {
574
+ os: hostOs,
575
+ supported: true
576
+ },
577
+ android: {
578
+ ...androidStatus,
579
+ status: androidStatus.status
580
+ },
581
+ ios: {
582
+ ...iosStatus,
583
+ status: iosStatus.status
584
+ },
585
+ summary: buildLegacySummary(androidStatus, iosStatus, gradle),
586
+ tools: {
57
587
  android: {
58
- ready: androidReady,
59
- summary: androidSummary,
60
- blockers: android.issues
61
- },
62
- ios: {
63
- ready: iosReady,
64
- summary: iosSummary,
65
- blockers: ios.issues
588
+ adb: androidStatus.tools.adb,
589
+ java: javaTool
66
590
  },
591
+ ios: iosStatus.tools,
67
592
  gradle: {
68
- ready: gradleReady,
69
- summary: gradleSummary,
70
- blockers: gradle.issues || [],
71
- suggestedFixes: gradle.suggestedFixes || []
593
+ java: javaTool
72
594
  }
73
595
  }
74
596
  }
package/src/system/ios.ts CHANGED
@@ -12,14 +12,21 @@ export async function checkIOS() {
12
12
  iosAvailable = true
13
13
  try {
14
14
  const simOut = execSync(`${xcrun} simctl list devices booted --json`, { encoding: 'utf8', timeout: 1500, stdio: ['ignore','pipe','ignore'] })
15
- const data = JSON.parse(simOut)
16
- type SimDevice = { state?: string }
17
- let count = 0
18
- for (const k in data.devices) {
19
- const arr = data.devices[k]
20
- if (Array.isArray(arr)) count += arr.filter((d: SimDevice) => (d.state || '').toLowerCase() === 'booted').length
15
+ try {
16
+ const data = JSON.parse(simOut)
17
+ type SimDevice = { state?: string }
18
+ let count = 0
19
+ for (const k in data.devices) {
20
+ const arr = data.devices[k]
21
+ if (Array.isArray(arr)) count += arr.filter((d: SimDevice) => (d.state || '').toLowerCase() === 'booted').length
22
+ }
23
+ iosDevices = count
24
+ } catch {
25
+ // Some xcrun builds include extra text around the JSON response.
26
+ // Fall back to counting explicit booted state markers so readiness remains deterministic.
27
+ const bootedMatches = (simOut.match(/"state"\s*:\s*"Booted"/gi) || []).length
28
+ iosDevices = bootedMatches
21
29
  }
22
- iosDevices = count
23
30
  if (iosDevices === 0) issues.push('No iOS simulators/devices booted')
24
31
  } catch (e: unknown) { console.debug('[get_system_status] simctl list failed: ' + String(e)) }
25
32
  } catch (e: unknown) { iosAvailable = false; console.debug('[get_system_status] xcrun --version failed: ' + String(e)) }