mobile-debug-mcp 0.19.1 → 0.20.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/dist/server.js +14 -19
- package/dist/system/android.js +78 -0
- package/dist/system/index.js +26 -0
- package/dist/system/ios.js +38 -0
- package/dist/utils/ios/utils.js +3 -2
- package/docs/CHANGELOG.md +10 -0
- package/docs/tools/TOOLS.md +1 -0
- package/docs/tools/system.md +48 -0
- package/package.json +1 -1
- package/skills/mcp-builder/SKILL.md +68 -0
- package/skills/mcp-builder/references/build-flags.md +43 -0
- package/skills/mcp-builder/references/diagnostics-schema.md +48 -0
- package/skills/mcp-builder/references/toolchain-details.md +62 -0
- package/src/server.ts +16 -15
- package/src/system/android.ts +62 -0
- package/src/system/index.ts +27 -0
- package/src/system/ios.ts +28 -0
- package/src/utils/ios/utils.ts +3 -2
- package/test/system/adb_version.test.ts +25 -0
- package/test/system/get_system_status.test.ts +52 -0
- package/test/system/system_status.test.ts +109 -0
package/dist/server.js
CHANGED
|
@@ -7,7 +7,6 @@ import { ToolsInteract } from './interact/index.js';
|
|
|
7
7
|
import { ToolsObserve } from './observe/index.js';
|
|
8
8
|
import { AndroidManage } from './manage/index.js';
|
|
9
9
|
import { iOSManage } from './manage/index.js';
|
|
10
|
-
import { ensureAdbAvailable } from './utils/android/utils.js';
|
|
11
10
|
const server = new Server({
|
|
12
11
|
name: "mobile-debug-mcp",
|
|
13
12
|
version: "0.7.0"
|
|
@@ -16,24 +15,11 @@ const server = new Server({
|
|
|
16
15
|
tools: {}
|
|
17
16
|
}
|
|
18
17
|
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
console.debug('[startup] adb available:', adbCheck.adbCmd, adbCheck.version);
|
|
25
|
-
else
|
|
26
|
-
console.warn('[startup] adb not available or failed to run:', adbCheck.adbCmd, adbCheck.error);
|
|
27
|
-
}
|
|
28
|
-
catch (e) {
|
|
29
|
-
if (e instanceof Error) {
|
|
30
|
-
console.warn('[startup] error during adb healthcheck:', e.message);
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
console.warn('[startup] error during adb healthcheck:', String(e));
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
})();
|
|
18
|
+
import { getSystemStatus } from './system/index.js';
|
|
19
|
+
// Run a quick startup healthcheck (non-fatal) by calling getSystemStatus directly and log a short summary
|
|
20
|
+
getSystemStatus().then(res => {
|
|
21
|
+
console.debug('[startup] system status summary:', { adb: res.adbAvailable, ios: res.iosAvailable, devices: res.devices, iosDevices: res.iosDevices });
|
|
22
|
+
}).catch(e => console.warn('[startup] healthcheck failed:', e instanceof Error ? e.message : String(e)));
|
|
37
23
|
function wrapResponse(data) {
|
|
38
24
|
return {
|
|
39
25
|
content: [{
|
|
@@ -196,6 +182,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
196
182
|
}
|
|
197
183
|
}
|
|
198
184
|
},
|
|
185
|
+
{
|
|
186
|
+
name: "get_system_status",
|
|
187
|
+
description: "Quick healthcheck of local mobile debugging environment (adb, devices, logs, env, iOS).",
|
|
188
|
+
inputSchema: { type: "object", properties: {} }
|
|
189
|
+
},
|
|
199
190
|
{
|
|
200
191
|
name: "capture_screenshot",
|
|
201
192
|
description: "Capture a screenshot from an Android device or iOS simulator. Returns device metadata and the screenshot image.",
|
|
@@ -596,6 +587,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
596
587
|
const res = await ToolsManage.listDevicesHandler({ platform, appId });
|
|
597
588
|
return wrapResponse(res);
|
|
598
589
|
}
|
|
590
|
+
if (name === "get_system_status") {
|
|
591
|
+
const result = await getSystemStatus();
|
|
592
|
+
return wrapResponse(result);
|
|
593
|
+
}
|
|
599
594
|
if (name === "capture_screenshot") {
|
|
600
595
|
const { platform, deviceId } = args;
|
|
601
596
|
const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId });
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { ensureAdbAvailable } from '../utils/android/utils.js';
|
|
3
|
+
export async function checkAndroid() {
|
|
4
|
+
const issues = [];
|
|
5
|
+
let adbAvailable = false;
|
|
6
|
+
let adbVersion = '';
|
|
7
|
+
let devices = 0;
|
|
8
|
+
let deviceStates = '';
|
|
9
|
+
let logsAvailable = false;
|
|
10
|
+
let envValid = false;
|
|
11
|
+
let appInstalled = undefined;
|
|
12
|
+
try {
|
|
13
|
+
const adbCheck = ensureAdbAvailable();
|
|
14
|
+
const adbCmd = adbCheck.adbCmd || 'adb';
|
|
15
|
+
adbAvailable = !!adbCheck.ok;
|
|
16
|
+
adbVersion = (adbCheck.version || '').toString().split('\n')[0];
|
|
17
|
+
if (!adbAvailable)
|
|
18
|
+
issues.push('ADB not available');
|
|
19
|
+
try {
|
|
20
|
+
const out = execSync(`${adbCmd} devices -l`, { encoding: 'utf8', timeout: 1500, stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
21
|
+
const lines = out.split('\n').map(l => l.trim()).filter(Boolean);
|
|
22
|
+
const deviceLines = lines.filter(l => !l.startsWith('List of devices'));
|
|
23
|
+
const stateCounts = {};
|
|
24
|
+
for (const l of deviceLines) {
|
|
25
|
+
const parts = l.split(/\s+/);
|
|
26
|
+
const state = parts[1] || '';
|
|
27
|
+
stateCounts[state] = (stateCounts[state] || 0) + 1;
|
|
28
|
+
}
|
|
29
|
+
devices = deviceLines.length;
|
|
30
|
+
const parts = Object.entries(stateCounts).map(([k, v]) => `${v} ${k}`);
|
|
31
|
+
deviceStates = parts.join(', ');
|
|
32
|
+
if (devices === 0)
|
|
33
|
+
issues.push('No Android devices connected');
|
|
34
|
+
if (stateCounts['unauthorized'])
|
|
35
|
+
issues.push(`${stateCounts['unauthorized']} device(s) unauthorized`);
|
|
36
|
+
if (stateCounts['offline'])
|
|
37
|
+
issues.push(`${stateCounts['offline']} device(s) offline`);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
console.debug('[get_system_status] adb devices failed: ' + String(e));
|
|
41
|
+
issues.push('Failed to list Android devices');
|
|
42
|
+
}
|
|
43
|
+
if (adbAvailable && devices > 0) {
|
|
44
|
+
try {
|
|
45
|
+
const lo = execSync(`${adbCmd} logcat -d -t 1`, { encoding: 'utf8', timeout: 1500, stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
46
|
+
logsAvailable = !!lo;
|
|
47
|
+
if (!logsAvailable)
|
|
48
|
+
issues.push('Log access failed');
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
logsAvailable = false;
|
|
52
|
+
console.debug('[get_system_status] logcat check failed: ' + String(e));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const sdkRoot = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME;
|
|
56
|
+
envValid = !!sdkRoot || (adbAvailable === true && !!(adbCheck.adbCmd && adbCheck.adbCmd !== 'adb'));
|
|
57
|
+
if (!envValid)
|
|
58
|
+
issues.push('ANDROID_SDK_ROOT/ANDROID_HOME missing and adb not found in PATH');
|
|
59
|
+
const pkg = process.env.MCP_TARGET_PACKAGE || process.env.MCP_TARGET_APP_ID;
|
|
60
|
+
if (pkg && adbAvailable && devices > 0) {
|
|
61
|
+
try {
|
|
62
|
+
const pm = execSync(`${adbCmd} shell pm path ${pkg}`, { encoding: 'utf8', timeout: 1500, stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
63
|
+
appInstalled = (pm || '').includes('package:');
|
|
64
|
+
if (!appInstalled)
|
|
65
|
+
issues.push(`App ${pkg} not installed on devices`);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
appInstalled = false;
|
|
69
|
+
console.debug('[get_system_status] pm check failed: ' + String(e));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
console.debug('[get_system_status] adb availability check failed: ' + String(e));
|
|
75
|
+
issues.push('ADB check failed');
|
|
76
|
+
}
|
|
77
|
+
return { adbAvailable, adbVersion, devices, deviceStates, logsAvailable, envValid, appInstalled, issues };
|
|
78
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { checkAndroid } from './android.js';
|
|
2
|
+
import { checkIOS } from './ios.js';
|
|
3
|
+
export async function getSystemStatus() {
|
|
4
|
+
try {
|
|
5
|
+
const android = await checkAndroid();
|
|
6
|
+
const ios = await checkIOS();
|
|
7
|
+
const issues = [...android.issues, ...ios.issues];
|
|
8
|
+
const success = issues.length === 0;
|
|
9
|
+
return {
|
|
10
|
+
success,
|
|
11
|
+
adbAvailable: android.adbAvailable,
|
|
12
|
+
adbVersion: android.adbVersion,
|
|
13
|
+
devices: android.devices,
|
|
14
|
+
deviceStates: android.deviceStates,
|
|
15
|
+
logsAvailable: android.logsAvailable,
|
|
16
|
+
envValid: android.envValid,
|
|
17
|
+
issues,
|
|
18
|
+
appInstalled: android.appInstalled,
|
|
19
|
+
iosAvailable: ios.iosAvailable,
|
|
20
|
+
iosDevices: ios.iosDevices
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
return { success: false, issues: ['Internal error: ' + (e instanceof Error ? e.message : String(e))] };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { getXcrunCmd } from '../utils/ios/utils.js';
|
|
3
|
+
export async function checkIOS() {
|
|
4
|
+
const issues = [];
|
|
5
|
+
let iosAvailable = false;
|
|
6
|
+
let iosDevices = 0;
|
|
7
|
+
try {
|
|
8
|
+
const xcrun = getXcrunCmd();
|
|
9
|
+
try {
|
|
10
|
+
execSync(`${xcrun} --version`, { stdio: ['ignore', 'pipe', 'ignore'], timeout: 1500 });
|
|
11
|
+
iosAvailable = true;
|
|
12
|
+
try {
|
|
13
|
+
const simOut = execSync(`${xcrun} simctl list devices booted --json`, { encoding: 'utf8', timeout: 1500, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
14
|
+
const data = JSON.parse(simOut);
|
|
15
|
+
let count = 0;
|
|
16
|
+
for (const k in data.devices) {
|
|
17
|
+
const arr = data.devices[k];
|
|
18
|
+
if (Array.isArray(arr))
|
|
19
|
+
count += arr.filter((d) => (d.state || '').toLowerCase() === 'booted').length;
|
|
20
|
+
}
|
|
21
|
+
iosDevices = count;
|
|
22
|
+
if (iosDevices === 0)
|
|
23
|
+
issues.push('No iOS simulators/devices booted');
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
console.debug('[get_system_status] simctl list failed: ' + String(e));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
iosAvailable = false;
|
|
31
|
+
console.debug('[get_system_status] xcrun --version failed: ' + String(e));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
console.debug('[get_system_status] xcrun check failed: ' + String(e));
|
|
36
|
+
}
|
|
37
|
+
return { iosAvailable, iosDevices, issues };
|
|
38
|
+
}
|
package/dist/utils/ios/utils.js
CHANGED
|
@@ -75,12 +75,13 @@ export async function isIDBInstalled() {
|
|
|
75
75
|
execSync(`command -v ${cmd}`, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
76
76
|
return true;
|
|
77
77
|
}
|
|
78
|
-
catch {
|
|
78
|
+
catch (e) {
|
|
79
79
|
try {
|
|
80
80
|
execSync(`${cmd} list-targets --json`, { stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 });
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
|
-
catch {
|
|
83
|
+
catch (e2) {
|
|
84
|
+
console.debug(`[isIDBInstalled] idb presence check failed for '${cmd}': ${e instanceof Error ? e.message : String(e2)}`);
|
|
84
85
|
return false;
|
|
85
86
|
}
|
|
86
87
|
}
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.20.0]
|
|
6
|
+
- Added `get_system_status` tool and refactored system health checks into `src/system`.
|
|
7
|
+
- Provides a fast environment healthcheck (ADB availability/version, connected devices, log access, Android env vars, and basic iOS xcrun/simulator checks).
|
|
8
|
+
- Designed to be fast, non-throwing, and to gate agent actions early. Unit tests added.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [0.19.2]
|
|
12
|
+
- Added healthcheck improvments
|
|
13
|
+
- Added skills
|
|
14
|
+
|
|
5
15
|
## [0.19.1]
|
|
6
16
|
|
|
7
17
|
- Fixed Android install issues
|
package/docs/tools/TOOLS.md
CHANGED
|
@@ -7,5 +7,6 @@ See:
|
|
|
7
7
|
- [mange](manage.md) — build, install and device management tools
|
|
8
8
|
- [observe](observe.md) — logs, screenshots and UI inspection tools
|
|
9
9
|
- [interact](interact.md) — UI interaction tools (tap, swipe, type, wait)
|
|
10
|
+
- [system](system.md) — environment and health checks (get_system_status)
|
|
10
11
|
|
|
11
12
|
For per-tool deep dives, open the linked files above.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# System (environment & health checks)
|
|
2
|
+
|
|
3
|
+
Tools that provide a lightweight view of the local mobile debugging environment and surface issues early so agents can decide whether to proceed.
|
|
4
|
+
|
|
5
|
+
## get_system_status
|
|
6
|
+
A fast, non-throwing healthcheck that inspects key dependencies and connections required for mobile debugging.
|
|
7
|
+
|
|
8
|
+
Input:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
{}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Response (example):
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"success": true,
|
|
19
|
+
"adbAvailable": true,
|
|
20
|
+
"adbVersion": "8.1.0",
|
|
21
|
+
"devices": 1,
|
|
22
|
+
"deviceStates": "1 device",
|
|
23
|
+
"logsAvailable": true,
|
|
24
|
+
"envValid": true,
|
|
25
|
+
"issues": [],
|
|
26
|
+
"appInstalled": true,
|
|
27
|
+
"iosAvailable": true,
|
|
28
|
+
"iosDevices": 1
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Checks performed (fast, best-effort):
|
|
33
|
+
- ADB availability and version (adb --version)
|
|
34
|
+
- Connected Android devices (adb devices -l), counts and state summary (device/unauthorized/offline)
|
|
35
|
+
- Log access probe (adb logcat -d -t 1)
|
|
36
|
+
- Android environment variables (ANDROID_SDK_ROOT / ANDROID_HOME / PATH contains adb)
|
|
37
|
+
- Optional: app installation check if MCP_TARGET_PACKAGE/MCP_TARGET_APP_ID is set (pm path)
|
|
38
|
+
- Basic iOS checks (xcrun --version and simctl list devices booted)
|
|
39
|
+
|
|
40
|
+
Behavior notes:
|
|
41
|
+
- Always returns structured JSON and never throws; any failures are surfaced in the `issues` array.
|
|
42
|
+
- Designed to be fast (<~1s probes where possible); startup callers may prefer a `fastMode` variant that only checks existence.
|
|
43
|
+
- Useful to call at the start of an agent session to gate subsequent actions.
|
|
44
|
+
|
|
45
|
+
Usage guidance:
|
|
46
|
+
- Call before build/install flows to avoid wasted build attempts on misconfigured systems.
|
|
47
|
+
- If `success: false`, attempt recovery steps or report issues to the user.
|
|
48
|
+
|
package/package.json
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# MCP Builder skill
|
|
2
|
+
|
|
3
|
+
name: mcp-builder
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
summary: Reusable procedures for building, validating and installing Android/iOS apps in this repo. Designed for agents to act autonomously with project-specific guidance.
|
|
6
|
+
|
|
7
|
+
# Purpose
|
|
8
|
+
Provide concise, actionable procedures and schemas that encode the successful sequences used in mobile-debug-mcp: toolchain detection, build orchestration, install fallbacks, diagnostics collection, and verification (lint/tests). Keep core guidance short; link to references for details.
|
|
9
|
+
|
|
10
|
+
# Activation conditions
|
|
11
|
+
Activate when an agent needs to:
|
|
12
|
+
- build or install an app from this repository
|
|
13
|
+
- diagnose failing CI/dev machine builds
|
|
14
|
+
- run lint/tests and collect reproducible diagnostics
|
|
15
|
+
|
|
16
|
+
# Surface area (actions)
|
|
17
|
+
- detect-toolchain
|
|
18
|
+
- build-android
|
|
19
|
+
- install-android
|
|
20
|
+
- build-ios
|
|
21
|
+
- install-ios
|
|
22
|
+
- run-lint
|
|
23
|
+
- run-tests
|
|
24
|
+
- collect-diagnostics
|
|
25
|
+
|
|
26
|
+
# Core guidance (what agent must do)
|
|
27
|
+
1. Prefer project conventions and existing helpers in src/utils and src/manage.
|
|
28
|
+
2. Use detect-toolchain to decide JAVA_HOME/JBR preference and whether adb/idb/xcrun are available.
|
|
29
|
+
3. For Android builds, call prepareGradle() to prepare child PATH and env, then run ./gradlew (wrapper) if present.
|
|
30
|
+
4. For installs, parse adb output defensively (ignore streamed-install noise) and on failure fall back to push+pm path.
|
|
31
|
+
5. For iOS installs, prefer simctl for simulators; if simctl fails check idb and attempt idb install with diagnostics.
|
|
32
|
+
6. On any error, call collect-diagnostics and attach env snapshot, invoked commands, stdout/stderr, and suggested fixes.
|
|
33
|
+
|
|
34
|
+
# Inputs & outputs (short schemas)
|
|
35
|
+
- detect-toolchain(input: { platform: 'android'|'ios'|'both', preferJBR?: boolean }) -> { tools: [{name, cmd, ok, version, suggestion}] }
|
|
36
|
+
- build-android(input: { projectPath, variant?, clean?, envOverrides? }) -> { success, artifactPath?, logs?, diagnostics? }
|
|
37
|
+
- install-android(input: { appPath, projectPath?, deviceId?, allowBuild? }) -> { installed:boolean, device, output?, diagnostics? }
|
|
38
|
+
- build-ios/install-ios: mirror Android schema but use scheme/workspace/project and resultBundlePath
|
|
39
|
+
- run-lint/run-tests -> { exitCode, stdout, stderr, artifacts[] }
|
|
40
|
+
- collect-diagnostics(input: { reason, platform? }) -> { artifacts: [{ name, contentBase64?, path? }], envSnapshot }
|
|
41
|
+
|
|
42
|
+
# Failure handling & suggestions
|
|
43
|
+
- Always return structured diagnostics instead of throwing when possible.
|
|
44
|
+
- Provide a short human-friendly suggestion in diagnostics (e.g., "Install Android Platform Tools or set ADB_PATH").
|
|
45
|
+
- Redact sensitive env values (tokens, credentials) when including envSnapshot.
|
|
46
|
+
|
|
47
|
+
# Progressive disclosure
|
|
48
|
+
- Keep SKILL.md compact (this file). Place heavy references in skills/mcp-builder/references/*.md and instruct agents to load them only when needed.
|
|
49
|
+
|
|
50
|
+
# References (implement as separate files)
|
|
51
|
+
- references/toolchain-details.md — exact paths/heuristics used for JBR, JAVA_HOME, ANDROID_SDK locations.
|
|
52
|
+
- references/build-flags.md — gradle/xcodebuild flags used, timeouts, and retry rationale.
|
|
53
|
+
- references/diagnostics-schema.md — JSON schema for runResult and collected artifacts.
|
|
54
|
+
|
|
55
|
+
# Implementation notes for maintainers
|
|
56
|
+
- Implement a thin adapter in src/skills/mcp-builder/index.ts that maps skill actions to existing functions in src/manage and src/utils.
|
|
57
|
+
- Provide unit tests under test/skills/mcp-builder that mock child_process to validate happy/failure paths.
|
|
58
|
+
- Add env toggles: MCP_PREFERS_JBR, MCP_GRADLE_RETRIES, MCP_XCODEBUILD_RETRIES.
|
|
59
|
+
|
|
60
|
+
# Example agent flow
|
|
61
|
+
1. call detect-toolchain({platform:'android', preferJBR:true})
|
|
62
|
+
2. if ok -> call build-android({projectPath:'/path', variant:'Debug'})
|
|
63
|
+
3. call install-android({appPath:'/path/to.apk', deviceId:'emulator-5554'})
|
|
64
|
+
4. if install fails -> call collect-diagnostics({reason:'install failure', platform:'android'})
|
|
65
|
+
|
|
66
|
+
# License
|
|
67
|
+
Same as repository (MIT).
|
|
68
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Build flags and runtime configuration
|
|
2
|
+
|
|
3
|
+
Purpose: describe the flags, environment variables and spawn-environment handling that agents should use when orchestrating Android and iOS builds for this repo.
|
|
4
|
+
|
|
5
|
+
Common env variables
|
|
6
|
+
- MCP_GRADLE_RETRIES (default: 1) — number of retry attempts on watchdog kills for Gradle
|
|
7
|
+
- MCP_GRADLE_TIMEOUT_MS (default: 300000) — gradle watch/duration timeout
|
|
8
|
+
- MCP_XCODEBUILD_RETRIES, MCP_XCODEBUILD_TIMEOUT_MS — similar for xcodebuild
|
|
9
|
+
- MCP_PREFERS_JBR — prefer Android Studio JBR for JAVA_HOME
|
|
10
|
+
- MCP_DERIVED_DATA, MCP_XCODE_RESULTBUNDLE_PATH — override DerivedData/result bundle locations
|
|
11
|
+
|
|
12
|
+
Android (Gradle)
|
|
13
|
+
- Use the Gradle wrapper if present: `./gradlew assemble<Variant>` (e.g., assembleDebug). Prefer wrapper to ensure consistent Gradle version.
|
|
14
|
+
- Prepare spawn env by prepending javaBin and platform-tools dir to PATH and set GRADLE_JAVA_HOME and `-Dorg.gradle.java.home` when necessary.
|
|
15
|
+
- If wrapper exists, ensure `chmod +x ./gradlew` before spawn.
|
|
16
|
+
- Recommended flags:
|
|
17
|
+
- `--no-daemon` sometimes helpful in CI, but wrap with existing project conventions.
|
|
18
|
+
- Set `org.gradle.jvmargs` via env or gradle.properties only if needed.
|
|
19
|
+
- Timeouts & watchdog:
|
|
20
|
+
- Use a watchdog timeout (MCP_GRADLE_TIMEOUT_MS) and retry (MCP_GRADLE_RETRIES) if killed by watchdog. Record stdout/stderr for each attempt.
|
|
21
|
+
|
|
22
|
+
iOS (xcodebuild)
|
|
23
|
+
- Use `xcodebuild -workspace <ws> -scheme <scheme> -configuration Debug -sdk iphonesimulator build` for simulator builds.
|
|
24
|
+
- Provide `-derivedDataPath` and `-resultBundlePath` to isolate builds and produce diagnostics. Default result bundle path should be unique per run to avoid collisions.
|
|
25
|
+
- Recommended flags: `-parallelizeTargets -jobs <N>` where N is from MCP_XCODE_JOBS or sensible default (4).
|
|
26
|
+
- When a destination UDID is available, always pass `-destination "platform=iOS Simulator,id=<UDID>"` to avoid ambiguous device selection.
|
|
27
|
+
- Timeouts & retries: respect MCP_XCODEBUILD_TIMEOUT_MS and MCP_XCODEBUILD_RETRIES; capture stdout/stderr and save logs under build-results.
|
|
28
|
+
|
|
29
|
+
Install-time behavior
|
|
30
|
+
- Android: prefer `adb install -r <apk>` for fast installs. If output contains spurious lines (e.g., "Performing Streamed Install" followed by "Success"), parse to extract final status. On failure try `adb push` to `/data/local/tmp` + `pm install -r <remote>` as a fallback collect push/install diagnostics.
|
|
31
|
+
- iOS: prefer `xcrun simctl install <device> <app>` for simulator. On failure attempt idb install if idb exists: `idb install <ipa|app> --udid <udid>` and record its stdout/stderr.
|
|
32
|
+
|
|
33
|
+
Diagnostics capture
|
|
34
|
+
- Save stdout/stderr, exitCode and environment snapshot for each command. Persist logs under workspace/build-results or a temp dir provided by the caller.
|
|
35
|
+
- For builds, also capture produced artifact paths to return to caller.
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
- Gradle invocation (pseudo):
|
|
39
|
+
spawnOpts.env.PATH = `${javaBin}:${platformTools}:${process.env.PATH}`
|
|
40
|
+
spawn('./gradlew', ['assembleDebug'], { cwd: projectRoot, env: spawnOpts.env })
|
|
41
|
+
|
|
42
|
+
- xcodebuild invocation (pseudo):
|
|
43
|
+
xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath /tmp/derived -resultBundlePath /tmp/Result-123.xcresult -parallelizeTargets -jobs 4 build
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Diagnostics schema
|
|
2
|
+
|
|
3
|
+
This document defines the JSON shapes that mcp-builder actions return when collecting diagnostics. Agents and tooling should rely on these keys.
|
|
4
|
+
|
|
5
|
+
Top-level diagnostic object
|
|
6
|
+
{
|
|
7
|
+
"error": "short human-friendly message",
|
|
8
|
+
"runResult": {
|
|
9
|
+
"command": "/usr/bin/adb",
|
|
10
|
+
"args": ["install", "app.apk"],
|
|
11
|
+
"exitCode": 1,
|
|
12
|
+
"stdout": "...",
|
|
13
|
+
"stderr": "...",
|
|
14
|
+
"startTimeMs": 1650000000000,
|
|
15
|
+
"endTimeMs": 1650000001000,
|
|
16
|
+
"envSnapshot": { "PATH": "...", "JAVA_HOME": "REDACTED" }
|
|
17
|
+
},
|
|
18
|
+
"artifacts": [ { name, path?, contentBase64?, type? } ],
|
|
19
|
+
"suggestedFixes": ["Install Android Platform Tools", "Set JAVA_HOME to JDK 17"]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Fields
|
|
23
|
+
- error: short message summarising the failure.
|
|
24
|
+
- runResult: information captured from a single command invocation. Use this for programmatic retries or human debugging.
|
|
25
|
+
- command, args: the executed command
|
|
26
|
+
- exitCode: numeric exit code, null for killed/unknown
|
|
27
|
+
- stdout, stderr: captured text
|
|
28
|
+
- startTimeMs, endTimeMs: epoch ms for duration
|
|
29
|
+
- envSnapshot: a restricted set of env variables (PATH, JAVA_HOME, ADB_PATH, IDB_PATH, XCRUN_PATH). Any value matching sensitive patterns (e.g., /token|secret|key|passwd/i) must be redacted to "REDACTED".
|
|
30
|
+
- artifacts: array of captured files. Each artifact: { name: string, path?: string, contentBase64?: string, type?: "log"|"archive"|"image" }
|
|
31
|
+
- suggestedFixes: short actionable suggestions for human/operator.
|
|
32
|
+
|
|
33
|
+
Notes on size and transmission
|
|
34
|
+
- Prefer storing large artifacts on disk and returning paths rather than inlining large base64 blobs. If transmitting, limit base64 inlined artifacts to a configurable max (e.g., 5MB) and prefer compression/archiving.
|
|
35
|
+
|
|
36
|
+
Example: adb install failure
|
|
37
|
+
{
|
|
38
|
+
"error": "adb: device not found",
|
|
39
|
+
"runResult": {
|
|
40
|
+
"command": "adb",
|
|
41
|
+
"args": ["devices"],
|
|
42
|
+
"exitCode": 0,
|
|
43
|
+
"stdout": "List of devices attached\n\n",
|
|
44
|
+
"stderr": "",
|
|
45
|
+
"envSnapshot": { "PATH": "/usr/local/bin:/usr/bin", "JAVA_HOME": null }
|
|
46
|
+
},
|
|
47
|
+
"suggestedFixes": ["Connect an Android device or start an emulator", "Ensure adb is on PATH or set ADB_PATH"]
|
|
48
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Toolchain details
|
|
2
|
+
|
|
3
|
+
This reference documents the exact heuristics and commands used by the mcp-builder skill to detect and validate the native toolchain (Android & iOS) on contributor machines.
|
|
4
|
+
|
|
5
|
+
Key environment variables
|
|
6
|
+
- ADB_PATH: explicit path to adb binary
|
|
7
|
+
- ANDROID_SDK_ROOT / ANDROID_HOME: SDK root; platform-tools/adb under these
|
|
8
|
+
- ANDROID_STUDIO_JBR / ANDROID_STUDIO_JDK: explicit Android Studio JBR/JDK path
|
|
9
|
+
- JAVA_HOME: system JDK
|
|
10
|
+
- IDB_PATH / MCP_IDB_PATH: idb path for iOS
|
|
11
|
+
- XCRUN_PATH: explicit xcrun path
|
|
12
|
+
- MCP_PREFERS_JBR: boolean to prefer Android Studio JBR when present
|
|
13
|
+
|
|
14
|
+
Android Java detection precedence
|
|
15
|
+
1. ANDROID_STUDIO_JBR / ANDROID_STUDIO_JDK env vars (preferred when MCP_PREFERS_JBR set)
|
|
16
|
+
2. Known Android Studio JBR locations (macOS):
|
|
17
|
+
- /Applications/Android Studio.app/Contents/jbr/Contents/Home
|
|
18
|
+
- /Applications/Android Studio Preview.app/Contents/jbr/Contents/Home
|
|
19
|
+
3. Explicit JAVA_HOME (validate via `java -version`)
|
|
20
|
+
4. macOS `/usr/libexec/java_home -v 17` or `-v 21`
|
|
21
|
+
5. Common Linux JDK locations: `/usr/lib/jvm/*temurin*`, `/usr/lib/jvm/*zulu*`
|
|
22
|
+
|
|
23
|
+
Validation rules
|
|
24
|
+
- Accept only Java 17 or Java 21 for Gradle builds in this project (both tested/known working).
|
|
25
|
+
- Reject GraalVM / Java 23 that causes Gradle jlink errors. If detected, return suggestion: "Prefer an Apple/Temurin/JBR Java 17 or 21. Set ANDROID_STUDIO_JBR or JAVA_HOME to a supported JDK."
|
|
26
|
+
|
|
27
|
+
ADB resolution precedence
|
|
28
|
+
1. process.env.ADB_PATH (explicit)
|
|
29
|
+
2. ANDROID_SDK_ROOT or ANDROID_HOME -> platform-tools/adb
|
|
30
|
+
3. Common SDK locations (macOS/Linux): $HOME/Library/Android/sdk/platform-tools, /opt/android-sdk/platform-tools
|
|
31
|
+
4. `command -v adb` / `which adb` on PATH
|
|
32
|
+
5. fallback to `adb` (best-effort)
|
|
33
|
+
|
|
34
|
+
IDB / XCRUN detection (iOS)
|
|
35
|
+
- IDB: check MCP_IDB_PATH -> IDB_PATH -> common locations (/opt/homebrew/bin/idb, /usr/local/bin/idb, $HOME/Library/Python/*/bin/idb). Validate by running `idb --version` or `idb list-targets --json`.
|
|
36
|
+
- XCRUN: check XCRUN_PATH -> run `xcrun --version`.
|
|
37
|
+
|
|
38
|
+
How to probe (commands)
|
|
39
|
+
- adb: `adb --version` and `adb devices --print` (or `adb devices -l`) to list.
|
|
40
|
+
- java: `java -XshowSettings:properties -version` or `java -version` parse first line.
|
|
41
|
+
- xcrun: `xcrun --version`
|
|
42
|
+
- idb: `idb --version` or `idb list-targets --json`
|
|
43
|
+
|
|
44
|
+
Output format
|
|
45
|
+
- Each probe returns { name, cmd, ok, version?, suggestion? } so agents can reason programmatically.
|
|
46
|
+
- Include env snapshot (PATH, JAVA_HOME, ADB_PATH, ANDROID_SDK_ROOT, IDB_PATH, XCRUN_PATH) to aid diagnostics.
|
|
47
|
+
|
|
48
|
+
Notes & rationale
|
|
49
|
+
- Prefer Android Studio JBR (JBR is bundled & known-good) because developer machines often have mismatched system JDKs (e.g., Graal). Allow override via env for CI.
|
|
50
|
+
- For long-running server processes, prepend java/bin and platform-tools to spawn env PATH when running child builds so external PATH/JAVA_HOME changes don't require a server restart.
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
- Detected result (JSON):
|
|
54
|
+
{
|
|
55
|
+
"name": "adb",
|
|
56
|
+
"cmd": "/Users/xxx/Library/Android/sdk/platform-tools/adb",
|
|
57
|
+
"ok": true,
|
|
58
|
+
"version": "Android Debug Bridge version 1.0.41"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
- Java suggestion when incompatible:
|
|
62
|
+
{ "name": "java", "ok": false, "suggestion": "Found GraalVM (Java 23). Use Java 17 or 21: set ANDROID_STUDIO_JBR or JAVA_HOME." }
|
package/src/server.ts
CHANGED
|
@@ -20,7 +20,6 @@ import { ToolsInteract } from './interact/index.js'
|
|
|
20
20
|
import { ToolsObserve } from './observe/index.js'
|
|
21
21
|
import { AndroidManage } from './manage/index.js'
|
|
22
22
|
import { iOSManage } from './manage/index.js'
|
|
23
|
-
import { ensureAdbAvailable } from './utils/android/utils.js'
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
const server = new Server(
|
|
@@ -35,20 +34,12 @@ const server = new Server(
|
|
|
35
34
|
}
|
|
36
35
|
);
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
} catch (e: unknown) {
|
|
45
|
-
if (e instanceof Error) {
|
|
46
|
-
console.warn('[startup] error during adb healthcheck:', e.message)
|
|
47
|
-
} else {
|
|
48
|
-
console.warn('[startup] error during adb healthcheck:', String(e))
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
})()
|
|
37
|
+
import { getSystemStatus } from './system/index.js'
|
|
38
|
+
|
|
39
|
+
// Run a quick startup healthcheck (non-fatal) by calling getSystemStatus directly and log a short summary
|
|
40
|
+
getSystemStatus().then(res => {
|
|
41
|
+
console.debug('[startup] system status summary:', { adb: res.adbAvailable, ios: res.iosAvailable, devices: res.devices, iosDevices: res.iosDevices })
|
|
42
|
+
}).catch(e => console.warn('[startup] healthcheck failed:', e instanceof Error ? e.message : String(e)))
|
|
52
43
|
|
|
53
44
|
function wrapResponse<T>(data: T) {
|
|
54
45
|
return {
|
|
@@ -214,6 +205,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
214
205
|
}
|
|
215
206
|
}
|
|
216
207
|
},
|
|
208
|
+
{
|
|
209
|
+
name: "get_system_status",
|
|
210
|
+
description: "Quick healthcheck of local mobile debugging environment (adb, devices, logs, env, iOS).",
|
|
211
|
+
inputSchema: { type: "object", properties: {} }
|
|
212
|
+
},
|
|
217
213
|
{
|
|
218
214
|
name: "capture_screenshot",
|
|
219
215
|
description: "Capture a screenshot from an Android device or iOS simulator. Returns device metadata and the screenshot image.",
|
|
@@ -631,6 +627,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request: SchemaOutput<typ
|
|
|
631
627
|
return wrapResponse(res)
|
|
632
628
|
}
|
|
633
629
|
|
|
630
|
+
if (name === "get_system_status") {
|
|
631
|
+
const result = await getSystemStatus()
|
|
632
|
+
return wrapResponse(result)
|
|
633
|
+
}
|
|
634
|
+
|
|
634
635
|
|
|
635
636
|
if (name === "capture_screenshot") {
|
|
636
637
|
const { platform, deviceId } = args as any
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
import { ensureAdbAvailable } from '../utils/android/utils.js'
|
|
3
|
+
|
|
4
|
+
export async function checkAndroid() {
|
|
5
|
+
const issues: string[] = []
|
|
6
|
+
let adbAvailable = false
|
|
7
|
+
let adbVersion = ''
|
|
8
|
+
let devices = 0
|
|
9
|
+
let deviceStates = ''
|
|
10
|
+
let logsAvailable = false
|
|
11
|
+
let envValid = false
|
|
12
|
+
let appInstalled: boolean | undefined = undefined
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const adbCheck = ensureAdbAvailable()
|
|
16
|
+
const adbCmd = adbCheck.adbCmd || 'adb'
|
|
17
|
+
adbAvailable = !!adbCheck.ok
|
|
18
|
+
adbVersion = (adbCheck.version || '').toString().split('\n')[0]
|
|
19
|
+
if (!adbAvailable) issues.push('ADB not available')
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const out = execSync(`${adbCmd} devices -l`, { encoding: 'utf8', timeout: 1500, stdio: ['ignore','pipe','ignore'] }).toString()
|
|
23
|
+
const lines = out.split('\n').map(l => l.trim()).filter(Boolean)
|
|
24
|
+
const deviceLines = lines.filter(l => !l.startsWith('List of devices'))
|
|
25
|
+
const stateCounts: Record<string, number> = {}
|
|
26
|
+
for (const l of deviceLines) {
|
|
27
|
+
const parts = l.split(/\s+/)
|
|
28
|
+
const state = parts[1] || ''
|
|
29
|
+
stateCounts[state] = (stateCounts[state] || 0) + 1
|
|
30
|
+
}
|
|
31
|
+
devices = deviceLines.length
|
|
32
|
+
const parts = Object.entries(stateCounts).map(([k,v]) => `${v} ${k}`)
|
|
33
|
+
deviceStates = parts.join(', ')
|
|
34
|
+
if (devices === 0) issues.push('No Android devices connected')
|
|
35
|
+
if (stateCounts['unauthorized']) issues.push(`${stateCounts['unauthorized']} device(s) unauthorized`)
|
|
36
|
+
if (stateCounts['offline']) issues.push(`${stateCounts['offline']} device(s) offline`)
|
|
37
|
+
} catch (e: unknown) { console.debug('[get_system_status] adb devices failed: ' + String(e)); issues.push('Failed to list Android devices') }
|
|
38
|
+
|
|
39
|
+
if (adbAvailable && devices > 0) {
|
|
40
|
+
try {
|
|
41
|
+
const lo = execSync(`${adbCmd} logcat -d -t 1`, { encoding: 'utf8', timeout: 1500, stdio: ['ignore','pipe','ignore'] }).toString()
|
|
42
|
+
logsAvailable = !!lo
|
|
43
|
+
if (!logsAvailable) issues.push('Log access failed')
|
|
44
|
+
} catch (e: unknown) { logsAvailable = false; console.debug('[get_system_status] logcat check failed: ' + String(e)) }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sdkRoot = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME
|
|
48
|
+
envValid = !!sdkRoot || (adbAvailable === true && !!(adbCheck.adbCmd && adbCheck.adbCmd !== 'adb'))
|
|
49
|
+
if (!envValid) issues.push('ANDROID_SDK_ROOT/ANDROID_HOME missing and adb not found in PATH')
|
|
50
|
+
|
|
51
|
+
const pkg = process.env.MCP_TARGET_PACKAGE || process.env.MCP_TARGET_APP_ID
|
|
52
|
+
if (pkg && adbAvailable && devices > 0) {
|
|
53
|
+
try {
|
|
54
|
+
const pm = execSync(`${adbCmd} shell pm path ${pkg}`, { encoding: 'utf8', timeout: 1500, stdio: ['ignore','pipe','ignore'] }).toString()
|
|
55
|
+
appInstalled = (pm || '').includes('package:')
|
|
56
|
+
if (!appInstalled) issues.push(`App ${pkg} not installed on devices`)
|
|
57
|
+
} catch (e: unknown) { appInstalled = false; console.debug('[get_system_status] pm check failed: ' + String(e)) }
|
|
58
|
+
}
|
|
59
|
+
} catch (e: unknown) { console.debug('[get_system_status] adb availability check failed: ' + String(e)); issues.push('ADB check failed') }
|
|
60
|
+
|
|
61
|
+
return { adbAvailable, adbVersion, devices, deviceStates, logsAvailable, envValid, appInstalled, issues }
|
|
62
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { checkAndroid } from './android.js'
|
|
2
|
+
import { checkIOS } from './ios.js'
|
|
3
|
+
|
|
4
|
+
export async function getSystemStatus() {
|
|
5
|
+
try {
|
|
6
|
+
const android = await checkAndroid()
|
|
7
|
+
const ios = await checkIOS()
|
|
8
|
+
const issues = [...android.issues, ...ios.issues]
|
|
9
|
+
|
|
10
|
+
const success = issues.length === 0
|
|
11
|
+
return {
|
|
12
|
+
success,
|
|
13
|
+
adbAvailable: android.adbAvailable,
|
|
14
|
+
adbVersion: android.adbVersion,
|
|
15
|
+
devices: android.devices,
|
|
16
|
+
deviceStates: android.deviceStates,
|
|
17
|
+
logsAvailable: android.logsAvailable,
|
|
18
|
+
envValid: android.envValid,
|
|
19
|
+
issues,
|
|
20
|
+
appInstalled: android.appInstalled,
|
|
21
|
+
iosAvailable: ios.iosAvailable,
|
|
22
|
+
iosDevices: ios.iosDevices
|
|
23
|
+
}
|
|
24
|
+
} catch (e: unknown) {
|
|
25
|
+
return { success: false, issues: ['Internal error: ' + (e instanceof Error ? e.message : String(e))] }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
import { getXcrunCmd } from '../utils/ios/utils.js'
|
|
3
|
+
|
|
4
|
+
export async function checkIOS() {
|
|
5
|
+
const issues: string[] = []
|
|
6
|
+
let iosAvailable = false
|
|
7
|
+
let iosDevices = 0
|
|
8
|
+
try {
|
|
9
|
+
const xcrun = getXcrunCmd()
|
|
10
|
+
try {
|
|
11
|
+
execSync(`${xcrun} --version`, { stdio: ['ignore','pipe','ignore'], timeout: 1500 })
|
|
12
|
+
iosAvailable = true
|
|
13
|
+
try {
|
|
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
|
|
21
|
+
}
|
|
22
|
+
iosDevices = count
|
|
23
|
+
if (iosDevices === 0) issues.push('No iOS simulators/devices booted')
|
|
24
|
+
} catch (e: unknown) { console.debug('[get_system_status] simctl list failed: ' + String(e)) }
|
|
25
|
+
} catch (e: unknown) { iosAvailable = false; console.debug('[get_system_status] xcrun --version failed: ' + String(e)) }
|
|
26
|
+
} catch (e: unknown) { console.debug('[get_system_status] xcrun check failed: ' + String(e)) }
|
|
27
|
+
return { iosAvailable, iosDevices, issues }
|
|
28
|
+
}
|
package/src/utils/ios/utils.ts
CHANGED
|
@@ -62,11 +62,12 @@ export async function isIDBInstalled(): Promise<boolean> {
|
|
|
62
62
|
try {
|
|
63
63
|
execSync(`command -v ${cmd}`, { stdio: ['ignore','pipe','ignore'] })
|
|
64
64
|
return true
|
|
65
|
-
} catch {
|
|
65
|
+
} catch (e: unknown) {
|
|
66
66
|
try {
|
|
67
67
|
execSync(`${cmd} list-targets --json`, { stdio: ['ignore','pipe','ignore'], timeout: 2000 })
|
|
68
68
|
return true
|
|
69
|
-
} catch {
|
|
69
|
+
} catch (e2: unknown) {
|
|
70
|
+
console.debug(`[isIDBInstalled] idb presence check failed for '${cmd}': ${e instanceof Error ? e.message : String(e2)}`)
|
|
70
71
|
return false
|
|
71
72
|
}
|
|
72
73
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import * as androidUtils from '../../src/utils/android/utils.js'
|
|
3
|
+
import * as systemStatus from '../../src/system/index.js'
|
|
4
|
+
|
|
5
|
+
const origEnsure = (androidUtils as any).ensureAdbAvailable
|
|
6
|
+
|
|
7
|
+
function mockEnsure(returnVal: any) {
|
|
8
|
+
(androidUtils as any).ensureAdbAvailable = () => returnVal
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function restoreEnsure() {
|
|
12
|
+
(androidUtils as any).ensureAdbAvailable = origEnsure
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('adb version parsing', () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
restoreEnsure()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('uses only the first line of multi-line adb --version output', async () => {
|
|
21
|
+
mockEnsure({ adbCmd: 'adb', ok: true, version: 'Android Debug Bridge version 1.0.41\nRevision 8f3b7' })
|
|
22
|
+
const res = await systemStatus.getSystemStatus()
|
|
23
|
+
assert.strictEqual(res.adbVersion, 'Android Debug Bridge version 1.0.41')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { ensureAdbAvailable } from '../../src/utils/android/utils.js'
|
|
3
|
+
import { getXcrunCmd } from '../../src/utils/ios/utils.js'
|
|
4
|
+
|
|
5
|
+
// We import the server handler module to access the internal get_system_status implementation.
|
|
6
|
+
import * as server from '../../src/server.js'
|
|
7
|
+
|
|
8
|
+
// Small helper to call the tool handler similarly to how the MCP transport would.
|
|
9
|
+
async function callGetSystemStatus() {
|
|
10
|
+
const req = { params: { name: 'get_system_status', arguments: {} } }
|
|
11
|
+
// @ts-ignore - use the handler exported from server
|
|
12
|
+
const handler = (server as any).defaultRequestHandler || (server as any).callToolHandler || (server as any).__callTool
|
|
13
|
+
if (!handler) {
|
|
14
|
+
// fallback: require the module and call the exported server instance's request handler
|
|
15
|
+
// The server code registers the handler directly; we will emulate by requiring compiled code in dist if available.
|
|
16
|
+
try {
|
|
17
|
+
const dist = await import('../../dist/server.js')
|
|
18
|
+
// Try to execute by sending a call via the server instance if exported
|
|
19
|
+
if (dist && dist.server && typeof dist.server._handleCall === 'function') {
|
|
20
|
+
return dist.server._handleCall(req)
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// best effort only
|
|
24
|
+
}
|
|
25
|
+
throw new Error('Cannot locate server call handler for tests')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return handler(req)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('get_system_status tool (unit)', () => {
|
|
32
|
+
it('returns structured result without throwing', async () => {
|
|
33
|
+
const res = await callGetSystemStatus()
|
|
34
|
+
// Handler returns { content: [{ type: 'text', text: JSON.stringify(...) }] }
|
|
35
|
+
assert(res && res.content && Array.isArray(res.content))
|
|
36
|
+
const textBlock = res.content.find((c: any) => c.type === 'text')
|
|
37
|
+
assert(textBlock && textBlock.text)
|
|
38
|
+
const payload = JSON.parse(textBlock.text)
|
|
39
|
+
assert(typeof payload.success === 'boolean')
|
|
40
|
+
assert(Array.isArray(payload.issues))
|
|
41
|
+
}).timeout(5000)
|
|
42
|
+
|
|
43
|
+
it('detects adb availability helper works', () => {
|
|
44
|
+
const adb = ensureAdbAvailable()
|
|
45
|
+
assert(adb && typeof adb.ok === 'boolean')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('detects xcrun command helper exists', () => {
|
|
49
|
+
const cmd = getXcrunCmd()
|
|
50
|
+
assert(typeof cmd === 'string')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import child_process from 'child_process'
|
|
3
|
+
|
|
4
|
+
import * as androidUtils from '../../src/utils/android/utils.js'
|
|
5
|
+
import * as iosUtils from '../../src/utils/ios/utils.js'
|
|
6
|
+
import * as systemStatus from '../../src/system/index.js'
|
|
7
|
+
|
|
8
|
+
const origExecSync = child_process.execSync
|
|
9
|
+
const origEnsure = (androidUtils as any).ensureAdbAvailable
|
|
10
|
+
const origGetXcrun = (iosUtils as any).getXcrunCmd
|
|
11
|
+
|
|
12
|
+
function mockExec(behaviour: (cmd: string) => string) {
|
|
13
|
+
(child_process as any).execSync = (cmd: string) => {
|
|
14
|
+
const s = typeof cmd === 'string' ? cmd : (Array.isArray(cmd) ? cmd.join(' ') : String(cmd))
|
|
15
|
+
const out = behaviour(s)
|
|
16
|
+
if (out instanceof Error) throw out
|
|
17
|
+
return out
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function restoreExec() {
|
|
22
|
+
(child_process as any).execSync = origExecSync
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mockEnsure(returnVal: any) {
|
|
26
|
+
(androidUtils as any).ensureAdbAvailable = () => returnVal
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function restoreEnsure() {
|
|
30
|
+
(androidUtils as any).ensureAdbAvailable = origEnsure
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function mockGetXcrun(val: string) {
|
|
34
|
+
(iosUtils as any).getXcrunCmd = () => val
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function restoreGetXcrun() {
|
|
38
|
+
(iosUtils as any).getXcrunCmd = origGetXcrun
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('system_status checks', () => {
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
restoreExec(); restoreEnsure(); restoreGetXcrun()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('reports healthy system when adb and xcrun present', async () => {
|
|
47
|
+
mockEnsure({ adbCmd: 'adb', ok: true, version: '8.1.0' })
|
|
48
|
+
mockGetXcrun('xcrun')
|
|
49
|
+
|
|
50
|
+
mockExec((cmd) => {
|
|
51
|
+
if (cmd.startsWith('adb devices')) return 'List of devices attached\nemulator-5554\tdevice'
|
|
52
|
+
if (cmd.includes('adb logcat')) return 'I/Tag: ok'
|
|
53
|
+
if (cmd.includes('adb shell pm path')) return 'package:/data/app/com.example-1/base.apk'
|
|
54
|
+
if (cmd.startsWith('xcrun --version')) return 'xcrun version 123'
|
|
55
|
+
if (cmd.includes('simctl list devices booted --json')) return JSON.stringify({ devices: {} })
|
|
56
|
+
return ''
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const res = await systemStatus.getSystemStatus()
|
|
60
|
+
assert.strictEqual(res.success, true)
|
|
61
|
+
assert.strictEqual(res.adbAvailable, true)
|
|
62
|
+
assert.strictEqual(typeof res.adbVersion, 'string')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('reports adb missing', async () => {
|
|
66
|
+
mockEnsure({ adbCmd: 'adb', ok: false, error: 'not found' })
|
|
67
|
+
mockGetXcrun('xcrun')
|
|
68
|
+
mockExec((cmd) => {
|
|
69
|
+
if (cmd.startsWith('xcrun --version')) return 'xcrun version'
|
|
70
|
+
return ''
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const res = await systemStatus.getSystemStatus()
|
|
74
|
+
assert.strictEqual(res.success, false)
|
|
75
|
+
assert(res.issues.some((i: string) => i.includes('ADB')))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('detects unauthorized/offline devices', async () => {
|
|
79
|
+
mockEnsure({ adbCmd: 'adb', ok: true, version: '8.1.0' })
|
|
80
|
+
mockGetXcrun('xcrun')
|
|
81
|
+
mockExec((cmd) => {
|
|
82
|
+
if (cmd.startsWith('adb devices')) return 'List of devices attached\nserial1\tunauthorized\nserial2\toffline\n'
|
|
83
|
+
if (cmd.startsWith('xcrun --version')) return 'xcrun version'
|
|
84
|
+
return ''
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const res = await systemStatus.getSystemStatus()
|
|
88
|
+
assert.strictEqual(res.success, false)
|
|
89
|
+
assert(res.issues.some((i: string) => i.includes('unauthorized')))
|
|
90
|
+
assert(res.issues.some((i: string) => i.includes('offline')))
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('handles missing xcrun gracefully', async () => {
|
|
94
|
+
mockEnsure({ adbCmd: 'adb', ok: true, version: '8.1.0' })
|
|
95
|
+
mockGetXcrun('xcrun')
|
|
96
|
+
mockExec((cmd) => {
|
|
97
|
+
if (cmd.startsWith('adb devices')) return 'List of devices attached\nemulator-5554\tdevice'
|
|
98
|
+
if (cmd.startsWith('xcrun --version')) throw new Error('not found')
|
|
99
|
+
return ''
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const res = await systemStatus.getSystemStatus()
|
|
103
|
+
// Expect iOS check to be false and Android to be healthy
|
|
104
|
+
assert.strictEqual(res.iosAvailable, false)
|
|
105
|
+
assert.strictEqual(res.adbAvailable, true)
|
|
106
|
+
// overall success may still be true (Android ok) but issues should include an xcrun-related message
|
|
107
|
+
assert(res.issues.some((i: string) => i.toLowerCase().includes('xcrun') || i.toLowerCase().includes('ios')))
|
|
108
|
+
})
|
|
109
|
+
})
|