mobile-debug-mcp 0.12.2 → 0.12.3
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/android/utils.js +11 -1
- package/dist/ios/manage.js +22 -2
- package/dist/ios/utils.js +41 -22
- package/dist/server.js +8 -6
- package/dist/tools/manage.js +180 -19
- package/dist/utils/resolve-device.js +8 -0
- package/docs/CHANGELOG.md +7 -0
- package/docs/TOOLS.md +7 -268
- package/docs/interact.md +43 -0
- package/docs/manage.md +140 -0
- package/docs/observe.md +86 -0
- package/package.json +1 -1
- package/src/android/utils.ts +13 -1
- package/src/ios/manage.ts +26 -2
- package/src/ios/utils.ts +38 -21
- package/src/server.ts +9 -6
- package/src/tools/manage.ts +164 -15
- package/src/utils/resolve-device.ts +7 -0
- package/test/device/manage/run-install-kmp.ts +18 -0
- package/test/unit/index.ts +2 -0
- package/test/unit/manage/detection.test.ts +48 -0
- package/test/unit/manage/diagnostics.test.ts +13 -8
- package/test/unit/manage/install.test.ts +6 -1
- package/test/unit/manage/mcp_disable_autodetect.test.ts +35 -0
- package/test/unit/observe/wait_for_element_mock.ts +1 -1
package/dist/android/utils.js
CHANGED
|
@@ -11,7 +11,17 @@ export async function prepareGradle(projectPath) {
|
|
|
11
11
|
const gradlewPath = path.join(projectPath, 'gradlew');
|
|
12
12
|
const gradleCmd = existsSync(gradlewPath) ? './gradlew' : 'gradle';
|
|
13
13
|
const execCmd = existsSync(gradlewPath) ? gradlewPath : gradleCmd;
|
|
14
|
-
|
|
14
|
+
// Start with a default task; callers may append/override via env flags
|
|
15
|
+
const gradleArgs = [process.env.MCP_GRADLE_TASK || 'assembleDebug'];
|
|
16
|
+
// Respect generic MCP_BUILD_JOBS and Android-specific MCP_GRADLE_WORKERS
|
|
17
|
+
const workers = process.env.MCP_GRADLE_WORKERS || process.env.MCP_BUILD_JOBS;
|
|
18
|
+
if (workers) {
|
|
19
|
+
gradleArgs.push(`--max-workers=${workers}`);
|
|
20
|
+
}
|
|
21
|
+
// Respect gradle cache env: default enabled; set MCP_GRADLE_CACHE=0 to disable
|
|
22
|
+
if (process.env.MCP_GRADLE_CACHE === '0') {
|
|
23
|
+
gradleArgs.push('-Dorg.gradle.caching=false');
|
|
24
|
+
}
|
|
15
25
|
const detectedJavaHome = await detectJavaHome().catch(() => undefined);
|
|
16
26
|
const env = Object.assign({}, process.env);
|
|
17
27
|
if (detectedJavaHome) {
|
package/dist/ios/manage.js
CHANGED
|
@@ -72,8 +72,17 @@ export class iOSManage {
|
|
|
72
72
|
catch { }
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
|
+
// Prepare build flags and paths (support incremental builds)
|
|
75
76
|
let buildArgs;
|
|
76
77
|
let chosenScheme = null;
|
|
78
|
+
// Derived data and result bundle (agent-configurable)
|
|
79
|
+
const derivedDataPath = process.env.MCP_DERIVED_DATA || path.join(projectRootDir, 'build', 'DerivedData');
|
|
80
|
+
const resultBundlePath = path.join(projectRootDir, 'build', 'xcresults', 'ResultBundle.xcresult');
|
|
81
|
+
const xcodeJobs = parseInt(process.env.MCP_XCODE_JOBS || '', 10) || 4;
|
|
82
|
+
const forceClean = process.env.MCP_FORCE_CLEAN === '1';
|
|
83
|
+
// ensure result dirs exist
|
|
84
|
+
await fs.mkdir(path.dirname(resultBundlePath), { recursive: true }).catch(() => { });
|
|
85
|
+
await fs.mkdir(derivedDataPath, { recursive: true }).catch(() => { });
|
|
77
86
|
if (workspace) {
|
|
78
87
|
const workspacePath = path.join(projectRootDir, workspace);
|
|
79
88
|
chosenScheme = await detectScheme(xcodeCmd, workspacePath, undefined, projectRootDir);
|
|
@@ -86,16 +95,27 @@ export class iOSManage {
|
|
|
86
95
|
const scheme = chosenScheme || proj.replace(/\.xcodeproj$/, '');
|
|
87
96
|
buildArgs = ['-project', projectPathFull, '-scheme', scheme, '-configuration', 'Debug', '-sdk', 'iphonesimulator', 'build'];
|
|
88
97
|
}
|
|
98
|
+
// Insert clean if explicitly requested via env
|
|
99
|
+
if (forceClean) {
|
|
100
|
+
const idx = buildArgs.indexOf('build');
|
|
101
|
+
if (idx >= 0)
|
|
102
|
+
buildArgs.splice(idx, 0, 'clean');
|
|
103
|
+
}
|
|
89
104
|
// If we have a destination UDID, add an explicit destination to avoid xcodebuild picking an ambiguous target
|
|
90
105
|
if (destinationUDID) {
|
|
91
106
|
buildArgs.push('-destination', `platform=iOS Simulator,id=${destinationUDID}`);
|
|
92
107
|
}
|
|
93
|
-
// Add result bundle
|
|
108
|
+
// Add derived data and result bundle for diagnostics and faster incremental builds
|
|
109
|
+
buildArgs.push('-derivedDataPath', derivedDataPath);
|
|
110
|
+
buildArgs.push('-resultBundlePath', resultBundlePath);
|
|
111
|
+
// parallelisation and jobs
|
|
112
|
+
buildArgs.push('-parallelizeTargets');
|
|
113
|
+
buildArgs.push('-jobs', String(xcodeJobs));
|
|
114
|
+
// Prepare results directory for backwards-compatible logs
|
|
94
115
|
const resultsDir = path.join(projectPath, 'build-results');
|
|
95
116
|
// Remove any stale results to avoid xcodebuild complaining about existing result bundles
|
|
96
117
|
await fs.rm(resultsDir, { recursive: true, force: true }).catch(() => { });
|
|
97
118
|
await fs.mkdir(resultsDir, { recursive: true }).catch(() => { });
|
|
98
|
-
// Skip specifying -resultBundlePath to avoid platform-specific collisions; rely on stdout/stderr logs
|
|
99
119
|
const XCODEBUILD_TIMEOUT = parseInt(process.env.MCP_XCODEBUILD_TIMEOUT || '', 10) || 180000; // default 3 minutes
|
|
100
120
|
const MAX_RETRIES = parseInt(process.env.MCP_XCODEBUILD_RETRIES || '', 10) || 1;
|
|
101
121
|
const tries = MAX_RETRIES + 1;
|
package/dist/ios/utils.js
CHANGED
|
@@ -239,6 +239,7 @@ export async function getIOSDeviceMetadata(deviceId = "booted") {
|
|
|
239
239
|
}
|
|
240
240
|
export async function listIOSDevices(appId) {
|
|
241
241
|
return new Promise((resolve) => {
|
|
242
|
+
// Query all devices and separately query booted devices to mark them
|
|
242
243
|
execFile(getXcrunCmd(), ['simctl', 'list', 'devices', '--json'], (err, stdout) => {
|
|
243
244
|
if (err || !stdout)
|
|
244
245
|
return resolve([]);
|
|
@@ -247,32 +248,50 @@ export async function listIOSDevices(appId) {
|
|
|
247
248
|
const devicesMap = data.devices || {};
|
|
248
249
|
const out = [];
|
|
249
250
|
const checks = [];
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// check if installed
|
|
263
|
-
const p = execCommand(['simctl', 'get_app_container', device.udid, appId, 'data'], device.udid)
|
|
264
|
-
.then(() => { info.appInstalled = true; })
|
|
265
|
-
.catch(() => { info.appInstalled = false; })
|
|
266
|
-
.then(() => { out.push(info); });
|
|
267
|
-
checks.push(p);
|
|
251
|
+
// Get booted devices set
|
|
252
|
+
execFile(getXcrunCmd(), ['simctl', 'list', 'devices', 'booted', '--json'], (err2, stdout2) => {
|
|
253
|
+
const bootedSet = new Set();
|
|
254
|
+
if (!err2 && stdout2) {
|
|
255
|
+
try {
|
|
256
|
+
const bdata = JSON.parse(stdout2);
|
|
257
|
+
const bmap = bdata.devices || {};
|
|
258
|
+
for (const rt in bmap) {
|
|
259
|
+
const devs = bmap[rt];
|
|
260
|
+
if (Array.isArray(devs))
|
|
261
|
+
for (const d of devs)
|
|
262
|
+
bootedSet.add(d.udid);
|
|
268
263
|
}
|
|
269
|
-
|
|
270
|
-
|
|
264
|
+
}
|
|
265
|
+
catch { }
|
|
266
|
+
}
|
|
267
|
+
for (const runtime in devicesMap) {
|
|
268
|
+
const devices = devicesMap[runtime];
|
|
269
|
+
if (Array.isArray(devices)) {
|
|
270
|
+
for (const device of devices) {
|
|
271
|
+
const info = {
|
|
272
|
+
platform: 'ios',
|
|
273
|
+
id: device.udid,
|
|
274
|
+
osVersion: parseRuntimeName(runtime),
|
|
275
|
+
model: device.name,
|
|
276
|
+
simulator: true,
|
|
277
|
+
booted: bootedSet.has(device.udid)
|
|
278
|
+
};
|
|
279
|
+
if (appId) {
|
|
280
|
+
// check if installed
|
|
281
|
+
const p = execCommand(['simctl', 'get_app_container', device.udid, appId, 'data'], device.udid)
|
|
282
|
+
.then(() => { info.appInstalled = true; })
|
|
283
|
+
.catch(() => { info.appInstalled = false; })
|
|
284
|
+
.then(() => { out.push(info); });
|
|
285
|
+
checks.push(p);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
out.push(info);
|
|
289
|
+
}
|
|
271
290
|
}
|
|
272
291
|
}
|
|
273
292
|
}
|
|
274
|
-
|
|
275
|
-
|
|
293
|
+
Promise.all(checks).then(() => resolve(out)).catch(() => resolve(out));
|
|
294
|
+
});
|
|
276
295
|
}
|
|
277
296
|
catch {
|
|
278
297
|
resolve([]);
|
package/dist/server.js
CHANGED
|
@@ -120,6 +120,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
120
120
|
type: "object",
|
|
121
121
|
properties: {
|
|
122
122
|
platform: { type: "string", enum: ["android", "ios"], description: "Optional. If omitted the server will attempt to detect platform from appPath/project files." },
|
|
123
|
+
projectType: { type: "string", enum: ["native", "kmp", "react-native", "flutter"], description: "Optional project type to guide build tool selection (e.g., kmp, react-native)." },
|
|
123
124
|
appPath: { type: "string", description: "Path to APK, .app, .ipa, or project directory" },
|
|
124
125
|
deviceId: { type: "string", description: "Device UDID (iOS) or Serial (Android). Defaults to booted/connected." }
|
|
125
126
|
},
|
|
@@ -133,6 +134,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
133
134
|
type: "object",
|
|
134
135
|
properties: {
|
|
135
136
|
platform: { type: "string", enum: ["android", "ios"], description: "Optional. If omitted the server will attempt to detect platform from projectPath files." },
|
|
137
|
+
projectType: { type: "string", enum: ["native", "kmp", "react-native", "flutter"], description: "Optional project type to guide build tool selection (e.g., kmp, react-native)." },
|
|
136
138
|
projectPath: { type: "string", description: "Path to project directory (contains gradlew or xcodeproj/xcworkspace)" },
|
|
137
139
|
variant: { type: "string", description: "Optional build variant (e.g., Debug/Release)" }
|
|
138
140
|
},
|
|
@@ -414,8 +416,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
414
416
|
return wrapResponse(response);
|
|
415
417
|
}
|
|
416
418
|
if (name === "install_app") {
|
|
417
|
-
const { platform, appPath, deviceId } = args;
|
|
418
|
-
const res = await ToolsManage.installAppHandler({ platform, appPath, deviceId });
|
|
419
|
+
const { platform, projectType, appPath, deviceId } = args;
|
|
420
|
+
const res = await ToolsManage.installAppHandler({ platform, appPath, deviceId, projectType });
|
|
419
421
|
const response = {
|
|
420
422
|
device: res.device,
|
|
421
423
|
installed: res.installed,
|
|
@@ -425,13 +427,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
425
427
|
return wrapResponse(response);
|
|
426
428
|
}
|
|
427
429
|
if (name === "build_app") {
|
|
428
|
-
const { platform, projectPath, variant } = args;
|
|
429
|
-
const res = await ToolsManage.buildAppHandler({ platform, projectPath, variant });
|
|
430
|
+
const { platform, projectType, projectPath, variant } = args;
|
|
431
|
+
const res = await ToolsManage.buildAppHandler({ platform, projectPath, variant, projectType });
|
|
430
432
|
return wrapResponse(res);
|
|
431
433
|
}
|
|
432
434
|
if (name === 'build_and_install') {
|
|
433
|
-
const { platform, projectPath, deviceId, timeout } = args;
|
|
434
|
-
const res = await ToolsManage.buildAndInstallHandler({ platform, projectPath, deviceId, timeout });
|
|
435
|
+
const { platform, projectType, projectPath, deviceId, timeout } = args;
|
|
436
|
+
const res = await ToolsManage.buildAndInstallHandler({ platform, projectPath, deviceId, timeout, projectType });
|
|
435
437
|
// res: { ndjson, result }
|
|
436
438
|
return {
|
|
437
439
|
content: [
|
package/dist/tools/manage.js
CHANGED
|
@@ -3,8 +3,149 @@ import path from 'path';
|
|
|
3
3
|
import { resolveTargetDevice, listDevices } from '../utils/resolve-device.js';
|
|
4
4
|
import { AndroidManage } from '../android/manage.js';
|
|
5
5
|
import { iOSManage } from '../ios/manage.js';
|
|
6
|
+
import { findApk } from '../android/utils.js';
|
|
7
|
+
import { findAppBundle } from '../ios/utils.js';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
export async function detectProjectPlatform(projectPath) {
|
|
10
|
+
try {
|
|
11
|
+
const stat = await fs.stat(projectPath).catch(() => null);
|
|
12
|
+
if (stat && stat.isDirectory()) {
|
|
13
|
+
const files = (await fs.readdir(projectPath).catch(() => []));
|
|
14
|
+
const hasIos = files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'));
|
|
15
|
+
const hasAndroid = files.includes('gradlew') || files.includes('build.gradle') || files.includes('settings.gradle') || (files.includes('app') && (await fs.stat(path.join(projectPath, 'app')).catch(() => null)));
|
|
16
|
+
if (hasIos && !hasAndroid)
|
|
17
|
+
return 'ios';
|
|
18
|
+
if (hasAndroid && !hasIos)
|
|
19
|
+
return 'android';
|
|
20
|
+
if (hasIos && hasAndroid)
|
|
21
|
+
return 'ambiguous';
|
|
22
|
+
return 'unknown';
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const ext = path.extname(projectPath).toLowerCase();
|
|
26
|
+
if (ext === '.apk')
|
|
27
|
+
return 'android';
|
|
28
|
+
if (ext === '.ipa' || ext === '.app')
|
|
29
|
+
return 'ios';
|
|
30
|
+
return 'unknown';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return 'unknown';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
6
37
|
export class ToolsManage {
|
|
7
|
-
static async
|
|
38
|
+
static async build_android({ projectPath, gradleTask, maxWorkers, gradleCache, forceClean }) {
|
|
39
|
+
const android = new AndroidManage();
|
|
40
|
+
// prepare gradle options via environment hints
|
|
41
|
+
if (typeof maxWorkers === 'number')
|
|
42
|
+
process.env.MCP_GRADLE_WORKERS = String(maxWorkers);
|
|
43
|
+
if (typeof gradleCache === 'boolean')
|
|
44
|
+
process.env.MCP_GRADLE_CACHE = gradleCache ? '1' : '0';
|
|
45
|
+
if (forceClean)
|
|
46
|
+
process.env.MCP_FORCE_CLEAN_ANDROID = '1';
|
|
47
|
+
const task = gradleTask || 'assembleDebug';
|
|
48
|
+
const artifact = await android.build(projectPath, task);
|
|
49
|
+
return artifact;
|
|
50
|
+
}
|
|
51
|
+
static async build_ios({ projectPath, workspace: _workspace, project: _project, scheme: _scheme, destinationUDID, derivedDataPath, buildJobs, forceClean }) {
|
|
52
|
+
const ios = new iOSManage();
|
|
53
|
+
// silence unused param lints
|
|
54
|
+
void _workspace;
|
|
55
|
+
void _project;
|
|
56
|
+
void _scheme;
|
|
57
|
+
if (derivedDataPath)
|
|
58
|
+
process.env.MCP_DERIVED_DATA = derivedDataPath;
|
|
59
|
+
if (typeof buildJobs === 'number')
|
|
60
|
+
process.env.MCP_BUILD_JOBS = String(buildJobs);
|
|
61
|
+
if (forceClean)
|
|
62
|
+
process.env.MCP_FORCE_CLEAN_IOS = '1';
|
|
63
|
+
if (destinationUDID)
|
|
64
|
+
process.env.MCP_XCODE_DESTINATION_UDID = destinationUDID;
|
|
65
|
+
const artifact = await ios.build(projectPath);
|
|
66
|
+
return artifact;
|
|
67
|
+
}
|
|
68
|
+
static async build_flutter({ projectPath, platform, buildMode, maxWorkers: _maxWorkers, forceClean: _forceClean }) {
|
|
69
|
+
// Prefer using flutter CLI when available; otherwise delegate to native subproject builders
|
|
70
|
+
const flutterCmd = process.env.FLUTTER_PATH || 'flutter';
|
|
71
|
+
// silence unused params
|
|
72
|
+
void _maxWorkers;
|
|
73
|
+
void _forceClean;
|
|
74
|
+
try {
|
|
75
|
+
// Check flutter presence without streaming output
|
|
76
|
+
execSync(`${flutterCmd} --version`, { stdio: 'ignore' });
|
|
77
|
+
if (!platform || platform === 'android') {
|
|
78
|
+
const mode = buildMode || 'debug';
|
|
79
|
+
try {
|
|
80
|
+
const out = execSync(`${flutterCmd} build apk --${mode}`, { cwd: projectPath, encoding: 'utf8' });
|
|
81
|
+
// Try to find built APK
|
|
82
|
+
const apk = await findApk(path.join(projectPath));
|
|
83
|
+
if (apk)
|
|
84
|
+
return { artifactPath: apk, output: out };
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const stdout = err && err.stdout ? String(err.stdout) : '';
|
|
88
|
+
const stderr = err && err.stderr ? String(err.stderr) : '';
|
|
89
|
+
throw new Error(`flutter build apk failed: ${stderr || stdout || err.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!platform || platform === 'ios') {
|
|
93
|
+
const mode = buildMode || 'debug';
|
|
94
|
+
try {
|
|
95
|
+
const out = execSync(`${flutterCmd} build ios --${mode} --no-codesign`, { cwd: projectPath, encoding: 'utf8' });
|
|
96
|
+
const app = await findAppBundle(path.join(projectPath));
|
|
97
|
+
if (app)
|
|
98
|
+
return { artifactPath: app, output: out };
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const stdout = err && err.stdout ? String(err.stdout) : '';
|
|
102
|
+
const stderr = err && err.stderr ? String(err.stderr) : '';
|
|
103
|
+
throw new Error(`flutter build ios failed: ${stderr || stdout || err.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
// If flutter CLI not available or command fails, fall back to native subprojects
|
|
109
|
+
// Preserve error message for diagnostics if needed
|
|
110
|
+
void e;
|
|
111
|
+
}
|
|
112
|
+
// Fallback: try native subproject builds
|
|
113
|
+
if (!platform || platform === 'android') {
|
|
114
|
+
const androidDir = path.join(projectPath, 'android');
|
|
115
|
+
const android = new AndroidManage();
|
|
116
|
+
const artifact = await android.build(androidDir, _forceClean ? 'clean && assembleDebug' : 'assembleDebug');
|
|
117
|
+
return artifact;
|
|
118
|
+
}
|
|
119
|
+
if (!platform || platform === 'ios') {
|
|
120
|
+
const iosDir = path.join(projectPath, 'ios');
|
|
121
|
+
const ios = new iOSManage();
|
|
122
|
+
const artifact = await ios.build(iosDir);
|
|
123
|
+
return artifact;
|
|
124
|
+
}
|
|
125
|
+
return { error: 'Unable to build flutter project' };
|
|
126
|
+
}
|
|
127
|
+
static async build_react_native({ projectPath, platform, variant, maxWorkers: _maxWorkers, forceClean: _forceClean }) {
|
|
128
|
+
// silence unused params
|
|
129
|
+
void _maxWorkers;
|
|
130
|
+
void _forceClean;
|
|
131
|
+
// React Native typically uses native subprojects. Delegate to Android/iOS builders.
|
|
132
|
+
if (!platform || platform === 'android') {
|
|
133
|
+
const androidDir = path.join(projectPath, 'android');
|
|
134
|
+
const android = new AndroidManage();
|
|
135
|
+
const artifact = await android.build(androidDir, variant || 'assembleDebug');
|
|
136
|
+
return artifact;
|
|
137
|
+
}
|
|
138
|
+
if (!platform || platform === 'ios') {
|
|
139
|
+
const iosDir = path.join(projectPath, 'ios');
|
|
140
|
+
// Recommend running `pod install` prior to building in CI; not performed automatically here
|
|
141
|
+
const ios = new iOSManage();
|
|
142
|
+
const artifact = await ios.build(iosDir);
|
|
143
|
+
return artifact;
|
|
144
|
+
}
|
|
145
|
+
return { error: 'Unable to build react-native project' };
|
|
146
|
+
}
|
|
147
|
+
static async buildAppHandler({ platform, projectPath, variant, projectType: _projectType }) {
|
|
148
|
+
void _projectType;
|
|
8
149
|
// delegate to platform-specific build implementations
|
|
9
150
|
const chosen = platform || 'android';
|
|
10
151
|
if (chosen === 'android') {
|
|
@@ -18,8 +159,20 @@ export class ToolsManage {
|
|
|
18
159
|
return artifact;
|
|
19
160
|
}
|
|
20
161
|
}
|
|
21
|
-
static async installAppHandler({ platform, appPath, deviceId }) {
|
|
162
|
+
static async installAppHandler({ platform, appPath, deviceId, projectType }) {
|
|
163
|
+
// Use projectType hint to influence platform detection when explicit platform is not provided
|
|
22
164
|
let chosenPlatform = platform;
|
|
165
|
+
if (!chosenPlatform && projectType) {
|
|
166
|
+
// Heuristic defaults: KMP, React Native and Flutter commonly target Android by default in CI
|
|
167
|
+
if (projectType === 'kmp' || projectType === 'react-native' || projectType === 'flutter') {
|
|
168
|
+
chosenPlatform = 'android';
|
|
169
|
+
console.debug('[manage] projectType hint -> selecting android by default for', projectType);
|
|
170
|
+
}
|
|
171
|
+
else if (projectType === 'native' || projectType === 'ios') {
|
|
172
|
+
chosenPlatform = 'ios';
|
|
173
|
+
console.debug('[manage] projectType hint -> selecting ios by default for', projectType);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
23
176
|
try {
|
|
24
177
|
const stat = await fs.stat(appPath).catch(() => null);
|
|
25
178
|
if (stat && stat.isDirectory()) {
|
|
@@ -106,36 +259,44 @@ export class ToolsManage {
|
|
|
106
259
|
return await new iOSManage().resetAppData(appId, resolved.id);
|
|
107
260
|
}
|
|
108
261
|
}
|
|
109
|
-
static async buildAndInstallHandler({ platform, projectPath, deviceId, timeout }) {
|
|
262
|
+
static async buildAndInstallHandler({ platform, projectPath, deviceId, timeout, projectType }) {
|
|
110
263
|
const events = [];
|
|
111
264
|
const pushEvent = (obj) => events.push(JSON.stringify(obj));
|
|
112
265
|
const effectiveTimeout = timeout ?? 180000; // reserved for future streaming/timeouts
|
|
113
266
|
void effectiveTimeout;
|
|
114
|
-
// determine platform if not provided by inspecting path
|
|
267
|
+
// determine platform if not provided by inspecting path or projectType hint
|
|
115
268
|
let chosenPlatform = platform;
|
|
116
269
|
try {
|
|
117
|
-
const stat = await fs.stat(projectPath).catch(() => null);
|
|
118
270
|
if (!chosenPlatform) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
271
|
+
// If autodetect is disabled, require explicit platform or projectType
|
|
272
|
+
if (process.env.MCP_DISABLE_AUTODETECT === '1') {
|
|
273
|
+
pushEvent({ type: 'build', status: 'failed', error: 'MCP_DISABLE_AUTODETECT=1 requires explicit platform or projectType' });
|
|
274
|
+
return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'MCP_DISABLE_AUTODETECT=1 requires explicit platform or projectType (ios|android).' } };
|
|
275
|
+
}
|
|
276
|
+
// If caller indicated KMP, prefer android by default (most KMP modul8 setups target Android)
|
|
277
|
+
if (projectType === 'kmp') {
|
|
278
|
+
chosenPlatform = 'android';
|
|
279
|
+
pushEvent({ type: 'build', status: 'info', message: 'projectType=kmp -> selecting android platform by default' });
|
|
125
280
|
}
|
|
126
281
|
else {
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
chosenPlatform =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
282
|
+
const det = await detectProjectPlatform(projectPath);
|
|
283
|
+
if (det === 'ios' || det === 'android') {
|
|
284
|
+
chosenPlatform = det;
|
|
285
|
+
}
|
|
286
|
+
else if (det === 'ambiguous') {
|
|
287
|
+
pushEvent({ type: 'build', status: 'failed', error: 'Ambiguous project (contains both iOS and Android). Please provide platform: "ios" or "android".' });
|
|
288
|
+
return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'Ambiguous project - please provide explicit platform parameter (ios|android).' } };
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// Unknown project type - do not guess. Request explicit platform.
|
|
292
|
+
pushEvent({ type: 'build', status: 'failed', error: 'Unknown project type - unable to autodetect platform. Please provide platform or projectType.' });
|
|
293
|
+
return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'Unknown project type - please provide platform or projectType (ios|android).' } };
|
|
294
|
+
}
|
|
134
295
|
}
|
|
135
296
|
}
|
|
136
297
|
}
|
|
137
298
|
catch {
|
|
138
|
-
|
|
299
|
+
// detection failed; avoid guessing a platform
|
|
139
300
|
}
|
|
140
301
|
pushEvent({ type: 'build', status: 'started', platform: chosenPlatform });
|
|
141
302
|
let buildRes;
|
|
@@ -49,6 +49,14 @@ export async function resolveTargetDevice(opts) {
|
|
|
49
49
|
if (physical.length > 1)
|
|
50
50
|
candidates = physical;
|
|
51
51
|
}
|
|
52
|
+
// Prefer booted iOS simulators if present
|
|
53
|
+
if (platform === 'ios') {
|
|
54
|
+
const booted = candidates.filter((d) => !!d.booted);
|
|
55
|
+
if (booted.length === 1)
|
|
56
|
+
return booted[0];
|
|
57
|
+
if (booted.length > 1)
|
|
58
|
+
return booted[0]; // if multiple booted, pick the first
|
|
59
|
+
}
|
|
52
60
|
candidates.sort((a, b) => parseNumericVersion(b.osVersion) - parseNumericVersion(a.osVersion));
|
|
53
61
|
if (candidates.length > 1 && parseNumericVersion(candidates[0].osVersion) > parseNumericVersion(candidates[1].osVersion)) {
|
|
54
62
|
return candidates[0];
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.12.3]
|
|
6
|
+
- Now supports native and cross platform development platforms for building
|
|
7
|
+
- Add MCP_DISABLE_AUTODETECT env var to require explicit platform/projectType for deterministic agent runs. When set to 1, build/install handlers will fail if platform is not provided.
|
|
8
|
+
- Add unit test covering MCP_DISABLE_AUTODETECT behaviour and ambiguous project detection (test/unit/manage/mcp_disable_autodetect.test.ts).
|
|
9
|
+
- Improve build_and_install handler to emit a clear NDJSON event when autodetect is disabled and platform was not supplied.
|
|
10
|
+
|
|
11
|
+
|
|
5
12
|
## [0.12.1]
|
|
6
13
|
- Improve iOS build/install reliability: project auto-scan, explicit simulator destination, configurable watchdog timeout (MCP_XCODEBUILD_TIMEOUT) and retries (MCP_XCODEBUILD_RETRIES), and DerivedData fallback for locating .app artifacts.
|
|
7
14
|
- Make install_app capable of building iOS projects before installing so agents can autonomously fix, build, install and validate apps.
|