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.
- package/README.md +50 -5
- package/dist/interact/index.js +546 -133
- package/dist/manage/ios.js +1 -1
- package/dist/server/tool-definitions.js +13 -9
- package/dist/server/tool-handlers.js +4 -4
- package/dist/server-core.js +1 -1
- package/dist/system/index.js +485 -48
- package/dist/system/ios.js +15 -7
- package/dist/utils/android/utils.js +4 -4
- package/dist/utils/cli/idb/check-idb.js +10 -3
- package/dist/utils/cli/idb/idb-helper.js +47 -13
- package/dist/utils/ios/utils.js +5 -9
- package/docs/CHANGELOG.md +6 -0
- package/docs/ROADMAP.md +88 -212
- package/docs/specs/baseline-spec-v0.md +1 -1
- package/docs/specs-deltas/015-adjustable-control-support.md +541 -0
- package/docs/specs-deltas/016-environment-tools.md +533 -0
- package/docs/tools/TOOLS.md +2 -0
- package/docs/tools/manage.md +4 -2
- package/docs/tools/system.md +9 -2
- package/package.json +1 -1
- package/skills/spec-review/SKILL.md +52 -0
- package/skills/spec-review/references/spec-review-checklist.md +13 -0
- package/skills/spec-review/references/spec-review-template.md +28 -0
- package/src/interact/index.ts +581 -138
- package/src/manage/ios.ts +1 -1
- package/src/server/tool-definitions.ts +13 -9
- package/src/server/tool-handlers.ts +4 -4
- package/src/server-core.ts +1 -1
- package/src/system/index.ts +572 -50
- package/src/system/ios.ts +14 -7
- package/src/types.ts +2 -0
- package/src/utils/android/utils.ts +3 -3
- package/src/utils/cli/idb/check-idb.ts +11 -2
- package/src/utils/cli/idb/idb-helper.ts +49 -12
- package/src/utils/ios/utils.ts +4 -8
- package/test/unit/interact/adjust_control.test.ts +586 -33
- package/test/unit/interact/tap_element.test.ts +1 -1
- package/test/unit/server/contract.test.ts +4 -0
- package/test/unit/system/get_system_status.test.ts +3 -0
- package/test/unit/system/system_status.test.ts +40 -0
- package/test/unit/utils/idb_health.test.ts +74 -0
- package/skills/rfc-review/SKILL.md +0 -52
- package/skills/rfc-review/references/rfc-review-checklist.md +0 -12
- package/skills/rfc-review/references/rfc-review-template.md +0 -28
- /package/docs/{rfcs → specs-deltas}/001-state-verification.md +0 -0
- /package/docs/{rfcs → specs-deltas}/002-richer-element-identity +0 -0
- /package/docs/{rfcs → specs-deltas}/003-wait-and-synchronization-reliability.md +0 -0
- /package/docs/{rfcs → specs-deltas}/004-action-verification-routing.md +0 -0
- /package/docs/{rfcs → specs-deltas}/005-unified-action-execution-and-verification-model.md +0 -0
- /package/docs/{rfcs → specs-deltas}/006-runtime-action-instrumentation-and-binding-layer.md +0 -0
- /package/docs/{rfcs → specs-deltas}/007-actionability-resolution-and-executable-target-selection.md +0 -0
- /package/docs/{rfcs → specs-deltas}/008-adjustable-control-support-and-semantic-value-manipulation.md +0 -0
- /package/docs/{rfcs → specs-deltas}/009-semantic-control-modeling-for-custom-and-composite-controls.md +0 -0
- /package/docs/{rfcs → specs-deltas}/010-verification-stabilization-and-temporal-convergence.md +0 -0
- /package/docs/{rfcs → specs-deltas}/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md +0 -0
- /package/docs/{rfcs → specs-deltas}/011.1-recovery-contract-types-and-runtime-wiring-spec.md +0 -0
- /package/docs/{rfcs → specs-deltas}/012-action-trace-and-xecution-observability.md +0 -0
- /package/docs/{rfcs → specs-deltas}/013-wait-and-synchronization-reliability.md +0 -0
- /package/docs/{rfcs → specs-deltas}/014-actionability-resolution.md +0 -0
package/dist/manage/ios.js
CHANGED
|
@@ -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(), ['--
|
|
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
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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.
|
|
612
|
-
maxAttempts: { type: 'number', description: 'Maximum adjustment attempts.', default:
|
|
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')
|
|
235
|
-
const maxAttempts = getNumberArg(args, 'maxAttempts')
|
|
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('
|
|
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,
|
package/dist/server-core.js
CHANGED
|
@@ -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.
|
|
9
|
+
version: '0.31.0'
|
|
10
10
|
};
|
|
11
11
|
export function createServer() {
|
|
12
12
|
const server = new Server(serverInfo, {
|
package/dist/system/index.js
CHANGED
|
@@ -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
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
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 =
|
|
12
|
-
const iosReady =
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
36
|
-
adbVersion:
|
|
37
|
-
devices:
|
|
38
|
-
deviceStates:
|
|
39
|
-
logsAvailable:
|
|
40
|
-
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:
|
|
43
|
-
iosAvailable:
|
|
44
|
-
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
63
|
-
summary: gradleSummary,
|
|
64
|
-
blockers: gradle.issues || [],
|
|
65
|
-
suggestedFixes: gradle.suggestedFixes || []
|
|
502
|
+
java: javaTool
|
|
66
503
|
}
|
|
67
504
|
}
|
|
68
505
|
};
|