mcp-maestro-mobile-ai 1.1.0 → 1.3.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/CHANGELOG.md +29 -0
- package/docs/MCP_SETUP.md +246 -41
- package/package.json +4 -1
- package/scripts/check-prerequisites.js +277 -0
- package/src/mcp-server/index.js +202 -11
- package/src/mcp-server/tools/contextTools.js +123 -0
- package/src/mcp-server/tools/runTools.js +227 -4
- package/src/mcp-server/utils/prerequisites.js +390 -0
- package/src/mcp-server/utils/reportGenerator.js +455 -0
- package/src/mcp-server/utils/yamlTemplate.js +559 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Prerequisites Validation
|
|
3
|
+
*
|
|
4
|
+
* This module validates prerequisites when the MCP server starts.
|
|
5
|
+
* Critical missing dependencies will prevent server startup.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { logger } from "./logger.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Execute a command safely and return result
|
|
14
|
+
*/
|
|
15
|
+
function execCommand(command, options = {}) {
|
|
16
|
+
try {
|
|
17
|
+
return {
|
|
18
|
+
success: true,
|
|
19
|
+
output: execSync(command, {
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
timeout: options.timeout || 10000,
|
|
22
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
23
|
+
...options,
|
|
24
|
+
}).trim(),
|
|
25
|
+
};
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return {
|
|
28
|
+
success: false,
|
|
29
|
+
error: error.message,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse version string to extract major version number
|
|
36
|
+
*/
|
|
37
|
+
function parseVersion(versionString) {
|
|
38
|
+
if (!versionString) return null;
|
|
39
|
+
const match = versionString.match(/(\d+)(?:\.(\d+))?/);
|
|
40
|
+
if (match) {
|
|
41
|
+
return {
|
|
42
|
+
major: parseInt(match[1], 10),
|
|
43
|
+
minor: match[2] ? parseInt(match[2], 10) : 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check Node.js version (should always pass since we're running in Node)
|
|
51
|
+
*/
|
|
52
|
+
function checkNodeJs() {
|
|
53
|
+
const version = process.version;
|
|
54
|
+
const parsed = parseVersion(version);
|
|
55
|
+
|
|
56
|
+
if (!parsed || parsed.major < 18) {
|
|
57
|
+
return {
|
|
58
|
+
name: "Node.js",
|
|
59
|
+
status: "error",
|
|
60
|
+
installed: true,
|
|
61
|
+
version: version,
|
|
62
|
+
required: "18+",
|
|
63
|
+
message: `Node.js ${version} is outdated. Requires 18+.`,
|
|
64
|
+
hint: "Upgrade Node.js from https://nodejs.org/",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
name: "Node.js",
|
|
70
|
+
status: "ok",
|
|
71
|
+
installed: true,
|
|
72
|
+
version: version,
|
|
73
|
+
message: `Node.js ${version}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check Java version
|
|
79
|
+
*/
|
|
80
|
+
function checkJava() {
|
|
81
|
+
// Try java --version first (Java 9+)
|
|
82
|
+
let result = execCommand("java --version");
|
|
83
|
+
|
|
84
|
+
// Fall back to java -version
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
result = execCommand("java -version 2>&1");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!result.success) {
|
|
90
|
+
return {
|
|
91
|
+
name: "Java",
|
|
92
|
+
status: "error",
|
|
93
|
+
installed: false,
|
|
94
|
+
required: "17+",
|
|
95
|
+
message: "Java is not installed or not in PATH.",
|
|
96
|
+
hint: "Install Java 17+ from https://adoptium.net/",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Parse version
|
|
101
|
+
const versionMatch = result.output.match(
|
|
102
|
+
/(?:java|openjdk)\s+(?:version\s+)?["']?(\d+)/i
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (versionMatch) {
|
|
106
|
+
const major = parseInt(versionMatch[1], 10);
|
|
107
|
+
|
|
108
|
+
if (major < 17) {
|
|
109
|
+
return {
|
|
110
|
+
name: "Java",
|
|
111
|
+
status: "error",
|
|
112
|
+
installed: true,
|
|
113
|
+
version: `${major}`,
|
|
114
|
+
required: "17+",
|
|
115
|
+
message: `Java ${major} is outdated. Requires 17+.`,
|
|
116
|
+
hint: "Upgrade to Java 17+ from https://adoptium.net/",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
name: "Java",
|
|
122
|
+
status: "ok",
|
|
123
|
+
installed: true,
|
|
124
|
+
version: `${major}`,
|
|
125
|
+
message: `Java ${major}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Can't determine version but Java is installed
|
|
130
|
+
return {
|
|
131
|
+
name: "Java",
|
|
132
|
+
status: "warning",
|
|
133
|
+
installed: true,
|
|
134
|
+
version: "unknown",
|
|
135
|
+
message: "Java installed (version unknown)",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check Maestro CLI
|
|
141
|
+
*/
|
|
142
|
+
function checkMaestro() {
|
|
143
|
+
const result = execCommand("maestro --version");
|
|
144
|
+
|
|
145
|
+
if (!result.success) {
|
|
146
|
+
return {
|
|
147
|
+
name: "Maestro CLI",
|
|
148
|
+
status: "error",
|
|
149
|
+
installed: false,
|
|
150
|
+
message: "Maestro CLI is not installed or not in PATH.",
|
|
151
|
+
hint: "Install Maestro: curl -Ls https://get.maestro.mobile.dev | bash",
|
|
152
|
+
hintWindows: "Install Maestro: iwr https://get.maestro.mobile.dev | iex",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
name: "Maestro CLI",
|
|
158
|
+
status: "ok",
|
|
159
|
+
installed: true,
|
|
160
|
+
version: result.output,
|
|
161
|
+
message: `Maestro ${result.output}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check Android SDK / ADB
|
|
167
|
+
*/
|
|
168
|
+
function checkAndroidSdk() {
|
|
169
|
+
const androidHome = process.env.ANDROID_HOME;
|
|
170
|
+
|
|
171
|
+
if (!androidHome) {
|
|
172
|
+
return {
|
|
173
|
+
name: "Android SDK",
|
|
174
|
+
status: "warning",
|
|
175
|
+
installed: false,
|
|
176
|
+
message: "ANDROID_HOME environment variable is not set.",
|
|
177
|
+
hint: "Set ANDROID_HOME to your Android SDK path for reliable ADB access.",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check if ADB is accessible
|
|
182
|
+
const adbPath = path.join(androidHome, "platform-tools", "adb");
|
|
183
|
+
const result = execCommand(`"${adbPath}" --version`);
|
|
184
|
+
|
|
185
|
+
if (!result.success) {
|
|
186
|
+
// Try system PATH
|
|
187
|
+
const pathResult = execCommand("adb --version");
|
|
188
|
+
if (pathResult.success) {
|
|
189
|
+
return {
|
|
190
|
+
name: "Android SDK",
|
|
191
|
+
status: "ok",
|
|
192
|
+
installed: true,
|
|
193
|
+
message: `Android SDK (ADB available via PATH)`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
name: "Android SDK",
|
|
199
|
+
status: "warning",
|
|
200
|
+
installed: true,
|
|
201
|
+
message: "ANDROID_HOME set but ADB not accessible.",
|
|
202
|
+
hint: "Ensure platform-tools is installed in Android SDK.",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
name: "Android SDK",
|
|
208
|
+
status: "ok",
|
|
209
|
+
installed: true,
|
|
210
|
+
path: androidHome,
|
|
211
|
+
message: `Android SDK configured`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check for connected device/emulator
|
|
217
|
+
*/
|
|
218
|
+
function checkDevice() {
|
|
219
|
+
const androidHome = process.env.ANDROID_HOME;
|
|
220
|
+
let adbCmd = "adb";
|
|
221
|
+
|
|
222
|
+
if (androidHome) {
|
|
223
|
+
adbCmd = `"${path.join(androidHome, "platform-tools", "adb")}"`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const result = execCommand(`${adbCmd} devices`);
|
|
227
|
+
|
|
228
|
+
if (!result.success) {
|
|
229
|
+
return {
|
|
230
|
+
name: "Device/Emulator",
|
|
231
|
+
status: "warning",
|
|
232
|
+
connected: false,
|
|
233
|
+
message: "Could not check for connected devices.",
|
|
234
|
+
hint: "Ensure ADB is properly configured.",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Parse device list
|
|
239
|
+
const lines = result.output.split("\n").filter((l) => l.trim());
|
|
240
|
+
const devices = lines
|
|
241
|
+
.slice(1)
|
|
242
|
+
.filter((l) => l.includes("device") && !l.includes("offline"))
|
|
243
|
+
.map((l) => l.split("\t")[0]);
|
|
244
|
+
|
|
245
|
+
if (devices.length === 0) {
|
|
246
|
+
return {
|
|
247
|
+
name: "Device/Emulator",
|
|
248
|
+
status: "warning",
|
|
249
|
+
connected: false,
|
|
250
|
+
message: "No Android device or emulator connected.",
|
|
251
|
+
hint: "Start an emulator or connect a device via USB.",
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
name: "Device/Emulator",
|
|
257
|
+
status: "ok",
|
|
258
|
+
connected: true,
|
|
259
|
+
devices: devices,
|
|
260
|
+
message: `${devices.length} device(s) connected`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Run all prerequisite checks
|
|
266
|
+
* @param {Object} options - Options
|
|
267
|
+
* @param {boolean} options.exitOnError - Exit process if critical error found
|
|
268
|
+
* @param {boolean} options.checkDevice - Also check for connected device
|
|
269
|
+
* @returns {Object} Results object
|
|
270
|
+
*/
|
|
271
|
+
export async function validatePrerequisites(options = {}) {
|
|
272
|
+
const { exitOnError = true, checkDevice: shouldCheckDevice = false } = options;
|
|
273
|
+
|
|
274
|
+
logger.info("Validating prerequisites...");
|
|
275
|
+
|
|
276
|
+
const checks = [
|
|
277
|
+
{ fn: checkNodeJs, critical: true },
|
|
278
|
+
{ fn: checkJava, critical: true },
|
|
279
|
+
{ fn: checkMaestro, critical: true },
|
|
280
|
+
{ fn: checkAndroidSdk, critical: false },
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
if (shouldCheckDevice) {
|
|
284
|
+
checks.push({ fn: checkDevice, critical: false });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const results = [];
|
|
288
|
+
let hasErrors = false;
|
|
289
|
+
let hasWarnings = false;
|
|
290
|
+
|
|
291
|
+
for (const { fn, critical } of checks) {
|
|
292
|
+
const result = fn();
|
|
293
|
+
results.push({ ...result, critical });
|
|
294
|
+
|
|
295
|
+
if (result.status === "error") {
|
|
296
|
+
if (critical) {
|
|
297
|
+
hasErrors = true;
|
|
298
|
+
logger.error(`❌ ${result.name}: ${result.message}`);
|
|
299
|
+
} else {
|
|
300
|
+
hasWarnings = true;
|
|
301
|
+
logger.warn(`⚠️ ${result.name}: ${result.message}`);
|
|
302
|
+
}
|
|
303
|
+
} else if (result.status === "warning") {
|
|
304
|
+
hasWarnings = true;
|
|
305
|
+
logger.warn(`⚠️ ${result.name}: ${result.message}`);
|
|
306
|
+
} else {
|
|
307
|
+
logger.info(`✅ ${result.name}: ${result.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Collect hints for failed checks
|
|
312
|
+
const hints = results
|
|
313
|
+
.filter((r) => r.hint && (r.status === "error" || r.status === "warning"))
|
|
314
|
+
.map((r) => {
|
|
315
|
+
// Use Windows hint if available and on Windows
|
|
316
|
+
if (process.platform === "win32" && r.hintWindows) {
|
|
317
|
+
return r.hintWindows;
|
|
318
|
+
}
|
|
319
|
+
return r.hint;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
if (hints.length > 0) {
|
|
323
|
+
logger.info("");
|
|
324
|
+
logger.info("💡 To fix issues:");
|
|
325
|
+
hints.forEach((hint) => {
|
|
326
|
+
logger.info(` → ${hint}`);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Handle critical errors
|
|
331
|
+
if (hasErrors && exitOnError) {
|
|
332
|
+
logger.error("");
|
|
333
|
+
logger.error("═══════════════════════════════════════════════════════");
|
|
334
|
+
logger.error(" CRITICAL: Required prerequisites are missing.");
|
|
335
|
+
logger.error(" The MCP server cannot start without them.");
|
|
336
|
+
logger.error("═══════════════════════════════════════════════════════");
|
|
337
|
+
logger.error("");
|
|
338
|
+
process.exit(2); // Exit code 2 for infrastructure errors
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
success: !hasErrors,
|
|
343
|
+
hasWarnings,
|
|
344
|
+
results,
|
|
345
|
+
hints,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Quick check for critical prerequisites only
|
|
351
|
+
* Used for fast startup validation
|
|
352
|
+
*/
|
|
353
|
+
export function quickCheck() {
|
|
354
|
+
const java = checkJava();
|
|
355
|
+
const maestro = checkMaestro();
|
|
356
|
+
|
|
357
|
+
const errors = [];
|
|
358
|
+
|
|
359
|
+
if (java.status === "error") {
|
|
360
|
+
errors.push({
|
|
361
|
+
name: "Java",
|
|
362
|
+
message: java.message,
|
|
363
|
+
hint: java.hint,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (maestro.status === "error") {
|
|
368
|
+
errors.push({
|
|
369
|
+
name: "Maestro CLI",
|
|
370
|
+
message: maestro.message,
|
|
371
|
+
hint: process.platform === "win32" ? maestro.hintWindows : maestro.hint,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
success: errors.length === 0,
|
|
377
|
+
errors,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export default {
|
|
382
|
+
validatePrerequisites,
|
|
383
|
+
quickCheck,
|
|
384
|
+
checkNodeJs,
|
|
385
|
+
checkJava,
|
|
386
|
+
checkMaestro,
|
|
387
|
+
checkAndroidSdk,
|
|
388
|
+
checkDevice,
|
|
389
|
+
};
|
|
390
|
+
|