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
|
@@ -14,6 +14,13 @@ import {
|
|
|
14
14
|
clearAppContext,
|
|
15
15
|
listAppContexts,
|
|
16
16
|
} from "../utils/appContext.js";
|
|
17
|
+
import {
|
|
18
|
+
YAML_GENERATION_INSTRUCTIONS,
|
|
19
|
+
getYamlGenerationContext,
|
|
20
|
+
TEST_PATTERNS,
|
|
21
|
+
validateYamlStructure,
|
|
22
|
+
getScreenAnalysisInstructions,
|
|
23
|
+
} from "../utils/yamlTemplate.js";
|
|
17
24
|
|
|
18
25
|
/**
|
|
19
26
|
* Format result as MCP response
|
|
@@ -180,6 +187,118 @@ export async function listContexts() {
|
|
|
180
187
|
return formatResponse(result);
|
|
181
188
|
}
|
|
182
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Get YAML generation instructions
|
|
192
|
+
* AI MUST call this before generating any Maestro YAML
|
|
193
|
+
*/
|
|
194
|
+
export async function getYamlInstructions(appId) {
|
|
195
|
+
if (!appId) {
|
|
196
|
+
// Return generic instructions without app context
|
|
197
|
+
return formatResponse({
|
|
198
|
+
success: true,
|
|
199
|
+
instructions: YAML_GENERATION_INSTRUCTIONS,
|
|
200
|
+
patterns: TEST_PATTERNS,
|
|
201
|
+
message: "Use these instructions when generating Maestro YAML. Always follow the tapOn → inputText pattern for text input.",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Get app-specific context
|
|
206
|
+
const appContext = await loadAppContext(appId);
|
|
207
|
+
const instructions = getYamlGenerationContext(appId, appContext);
|
|
208
|
+
|
|
209
|
+
return formatResponse({
|
|
210
|
+
success: true,
|
|
211
|
+
appId,
|
|
212
|
+
instructions,
|
|
213
|
+
patterns: TEST_PATTERNS,
|
|
214
|
+
hasAppContext: Object.keys(appContext.elements || {}).length > 0,
|
|
215
|
+
message: "IMPORTANT: Follow these instructions EXACTLY when generating YAML. Always use tapOn before inputText.",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Validate YAML before running
|
|
221
|
+
* Checks for common issues like missing tapOn before inputText
|
|
222
|
+
*/
|
|
223
|
+
export async function validateYamlBeforeRun(yamlContent) {
|
|
224
|
+
if (!yamlContent) {
|
|
225
|
+
return formatResponse({
|
|
226
|
+
success: false,
|
|
227
|
+
error: "yamlContent is required",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const validation = validateYamlStructure(yamlContent);
|
|
232
|
+
|
|
233
|
+
if (!validation.valid) {
|
|
234
|
+
return formatResponse({
|
|
235
|
+
success: false,
|
|
236
|
+
valid: false,
|
|
237
|
+
issues: validation.issues,
|
|
238
|
+
message: "YAML has structural issues that may cause test failures. Please fix before running.",
|
|
239
|
+
fixedYaml: null, // Could auto-fix in future
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return formatResponse({
|
|
244
|
+
success: true,
|
|
245
|
+
valid: true,
|
|
246
|
+
message: "YAML structure is valid.",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get a test pattern template
|
|
252
|
+
*/
|
|
253
|
+
export async function getTestPattern(patternName) {
|
|
254
|
+
const patterns = {
|
|
255
|
+
login: TEST_PATTERNS.login,
|
|
256
|
+
search: TEST_PATTERNS.search,
|
|
257
|
+
navigation: TEST_PATTERNS.navigation,
|
|
258
|
+
form: TEST_PATTERNS.form,
|
|
259
|
+
list: TEST_PATTERNS.list,
|
|
260
|
+
settings: TEST_PATTERNS.settings,
|
|
261
|
+
logout: TEST_PATTERNS.logout,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const pattern = patterns[patternName?.toLowerCase()];
|
|
265
|
+
|
|
266
|
+
if (!pattern) {
|
|
267
|
+
return formatResponse({
|
|
268
|
+
success: false,
|
|
269
|
+
error: `Unknown pattern: ${patternName}`,
|
|
270
|
+
availablePatterns: Object.keys(patterns),
|
|
271
|
+
hint: "Use: login, form, search, navigation, list, settings, or logout",
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return formatResponse({
|
|
276
|
+
success: true,
|
|
277
|
+
pattern: patternName,
|
|
278
|
+
template: pattern,
|
|
279
|
+
message: "Replace placeholders in {} with actual values. REMEMBER: Always use tapOn before inputText!",
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get screen analysis instructions
|
|
285
|
+
* Helps AI understand how to gather UI element information
|
|
286
|
+
*/
|
|
287
|
+
export async function getScreenAnalysis() {
|
|
288
|
+
const instructions = getScreenAnalysisInstructions();
|
|
289
|
+
|
|
290
|
+
return formatResponse({
|
|
291
|
+
success: true,
|
|
292
|
+
instructions,
|
|
293
|
+
message: "Use these instructions to gather UI element information before generating YAML.",
|
|
294
|
+
requiredInfo: [
|
|
295
|
+
"Field labels/placeholders for text inputs",
|
|
296
|
+
"Button text for actions",
|
|
297
|
+
"Success/error indicators to verify results",
|
|
298
|
+
],
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
183
302
|
export default {
|
|
184
303
|
registerAppElements,
|
|
185
304
|
registerAppScreen,
|
|
@@ -190,5 +309,9 @@ export default {
|
|
|
190
309
|
getAppContext,
|
|
191
310
|
clearContext,
|
|
192
311
|
listContexts,
|
|
312
|
+
getYamlInstructions,
|
|
313
|
+
validateYamlBeforeRun,
|
|
314
|
+
getTestPattern,
|
|
315
|
+
getScreenAnalysis,
|
|
193
316
|
};
|
|
194
317
|
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import fs from 'fs/promises';
|
|
7
|
-
import path from 'path';
|
|
8
7
|
import { fileURLToPath } from 'url';
|
|
9
8
|
import { dirname, join } from 'path';
|
|
10
9
|
import { logger } from '../utils/logger.js';
|
|
11
10
|
import { runMaestroFlow, checkDeviceConnection, checkAppInstalled, getConfig } from '../utils/maestro.js';
|
|
12
11
|
import { validateMaestroYaml } from './validateTools.js';
|
|
12
|
+
import { validateYamlStructure } from '../utils/yamlTemplate.js';
|
|
13
|
+
import { generateReport, getReportsDir, listReports } from '../utils/reportGenerator.js';
|
|
13
14
|
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
16
|
const __dirname = dirname(__filename);
|
|
@@ -47,7 +48,49 @@ export async function runTest(yamlContent, testName, options = {}) {
|
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
// Validate YAML
|
|
51
|
+
// STEP 1: Validate YAML structure (catches common AI generation errors)
|
|
52
|
+
const structureValidation = validateYamlStructure(yamlContent);
|
|
53
|
+
|
|
54
|
+
if (!structureValidation.valid) {
|
|
55
|
+
logger.warn(`YAML structure validation failed for: ${testName}`, { errors: structureValidation.errors });
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: 'text',
|
|
61
|
+
text: JSON.stringify({
|
|
62
|
+
success: false,
|
|
63
|
+
name: testName,
|
|
64
|
+
error: 'YAML STRUCTURE ERROR - Please fix before running',
|
|
65
|
+
structureErrors: structureValidation.errors,
|
|
66
|
+
warnings: structureValidation.warnings,
|
|
67
|
+
summary: structureValidation.summary,
|
|
68
|
+
instructions: `
|
|
69
|
+
⚠️ YOUR YAML HAS CRITICAL ISSUES. Please follow these rules:
|
|
70
|
+
|
|
71
|
+
1. ALWAYS use tapOn BEFORE inputText:
|
|
72
|
+
CORRECT:
|
|
73
|
+
- tapOn: "Username"
|
|
74
|
+
- inputText: "value"
|
|
75
|
+
|
|
76
|
+
WRONG (text goes to wrong field!):
|
|
77
|
+
- inputText: "value"
|
|
78
|
+
|
|
79
|
+
2. ALWAYS start with:
|
|
80
|
+
- clearState
|
|
81
|
+
- launchApp
|
|
82
|
+
|
|
83
|
+
3. ALWAYS include appId at the top.
|
|
84
|
+
|
|
85
|
+
Please regenerate the YAML following these rules.
|
|
86
|
+
`,
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// STEP 2: Validate YAML syntax
|
|
51
94
|
const validation = await validateMaestroYaml(yamlContent);
|
|
52
95
|
const validationResult = JSON.parse(validation.content[0].text);
|
|
53
96
|
|
|
@@ -59,9 +102,9 @@ export async function runTest(yamlContent, testName, options = {}) {
|
|
|
59
102
|
text: JSON.stringify({
|
|
60
103
|
success: false,
|
|
61
104
|
name: testName,
|
|
62
|
-
error: 'YAML validation failed',
|
|
105
|
+
error: 'YAML syntax validation failed',
|
|
63
106
|
validationErrors: validationResult.errors,
|
|
64
|
-
hint: 'Fix the YAML errors and try again.
|
|
107
|
+
hint: 'Fix the YAML syntax errors and try again.',
|
|
65
108
|
}),
|
|
66
109
|
},
|
|
67
110
|
],
|
|
@@ -351,7 +394,187 @@ async function autoCleanupResults() {
|
|
|
351
394
|
}
|
|
352
395
|
}
|
|
353
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Generate a report from test results
|
|
399
|
+
*/
|
|
400
|
+
export async function generateTestReport(results, metadata = {}) {
|
|
401
|
+
try {
|
|
402
|
+
const reportResult = await generateReport(results, metadata);
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
content: [
|
|
406
|
+
{
|
|
407
|
+
type: 'text',
|
|
408
|
+
text: JSON.stringify({
|
|
409
|
+
success: true,
|
|
410
|
+
reportId: reportResult.reportId,
|
|
411
|
+
htmlPath: reportResult.htmlPath,
|
|
412
|
+
jsonPath: reportResult.jsonPath,
|
|
413
|
+
summary: reportResult.summary,
|
|
414
|
+
message: `Report generated successfully! Open the HTML file to view: ${reportResult.htmlPath}`,
|
|
415
|
+
reportsDir: getReportsDir(),
|
|
416
|
+
}),
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
};
|
|
420
|
+
} catch (error) {
|
|
421
|
+
logger.error('Report generation error', { error: error.message });
|
|
422
|
+
return {
|
|
423
|
+
content: [
|
|
424
|
+
{
|
|
425
|
+
type: 'text',
|
|
426
|
+
text: JSON.stringify({
|
|
427
|
+
success: false,
|
|
428
|
+
error: error.message,
|
|
429
|
+
}),
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* List all generated reports
|
|
438
|
+
*/
|
|
439
|
+
export async function listTestReports() {
|
|
440
|
+
try {
|
|
441
|
+
const reports = await listReports();
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
content: [
|
|
445
|
+
{
|
|
446
|
+
type: 'text',
|
|
447
|
+
text: JSON.stringify({
|
|
448
|
+
success: true,
|
|
449
|
+
reportsDir: getReportsDir(),
|
|
450
|
+
reports,
|
|
451
|
+
count: reports.length,
|
|
452
|
+
}),
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
};
|
|
456
|
+
} catch (error) {
|
|
457
|
+
logger.error('List reports error', { error: error.message });
|
|
458
|
+
return {
|
|
459
|
+
content: [
|
|
460
|
+
{
|
|
461
|
+
type: 'text',
|
|
462
|
+
text: JSON.stringify({
|
|
463
|
+
success: false,
|
|
464
|
+
error: error.message,
|
|
465
|
+
}),
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Run test suite and generate report
|
|
474
|
+
*/
|
|
475
|
+
export async function runTestSuiteWithReport(tests, options = {}) {
|
|
476
|
+
try {
|
|
477
|
+
const results = [];
|
|
478
|
+
const startTime = Date.now();
|
|
479
|
+
|
|
480
|
+
for (let i = 0; i < tests.length; i++) {
|
|
481
|
+
const test = tests[i];
|
|
482
|
+
logger.info(`Running test ${i + 1}/${tests.length}: ${test.name}`);
|
|
483
|
+
|
|
484
|
+
// Validate structure first
|
|
485
|
+
const structureValidation = validateYamlStructure(test.yaml);
|
|
486
|
+
if (!structureValidation.valid) {
|
|
487
|
+
results.push({
|
|
488
|
+
name: test.name,
|
|
489
|
+
success: false,
|
|
490
|
+
error: 'YAML structure error: ' + structureValidation.errors.map(e => e.issue).join(', '),
|
|
491
|
+
structureErrors: structureValidation.errors,
|
|
492
|
+
});
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Validate syntax
|
|
497
|
+
const syntaxValidation = await validateMaestroYaml(test.yaml);
|
|
498
|
+
const syntaxResult = JSON.parse(syntaxValidation.content[0].text);
|
|
499
|
+
if (!syntaxResult.valid) {
|
|
500
|
+
results.push({
|
|
501
|
+
name: test.name,
|
|
502
|
+
success: false,
|
|
503
|
+
error: 'YAML syntax error',
|
|
504
|
+
syntaxErrors: syntaxResult.errors,
|
|
505
|
+
});
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Run the test
|
|
510
|
+
const testResult = await runMaestroFlow(test.yaml, test.name, {
|
|
511
|
+
retries: options.retries,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
results.push(testResult);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const totalDuration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
518
|
+
|
|
519
|
+
// Generate report
|
|
520
|
+
const reportResult = await generateReport(results, {
|
|
521
|
+
promptFile: options.promptFile || 'Manual Test Suite',
|
|
522
|
+
appId: options.appId || process.env.APP_ID || 'unknown',
|
|
523
|
+
totalDuration,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const passed = results.filter(r => r.success).length;
|
|
527
|
+
const failed = results.length - passed;
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
content: [
|
|
531
|
+
{
|
|
532
|
+
type: 'text',
|
|
533
|
+
text: JSON.stringify({
|
|
534
|
+
success: failed === 0,
|
|
535
|
+
totalDuration: `${totalDuration}s`,
|
|
536
|
+
summary: {
|
|
537
|
+
total: results.length,
|
|
538
|
+
passed,
|
|
539
|
+
failed,
|
|
540
|
+
passRate: reportResult.summary.passRate,
|
|
541
|
+
},
|
|
542
|
+
report: {
|
|
543
|
+
reportId: reportResult.reportId,
|
|
544
|
+
htmlPath: reportResult.htmlPath,
|
|
545
|
+
jsonPath: reportResult.jsonPath,
|
|
546
|
+
message: `📊 Report generated: ${reportResult.htmlPath}`,
|
|
547
|
+
},
|
|
548
|
+
tests: results.map(r => ({
|
|
549
|
+
name: r.name,
|
|
550
|
+
success: r.success,
|
|
551
|
+
duration: r.duration,
|
|
552
|
+
error: r.error || null,
|
|
553
|
+
})),
|
|
554
|
+
}),
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
};
|
|
558
|
+
} catch (error) {
|
|
559
|
+
logger.error('Test suite with report error', { error: error.message });
|
|
560
|
+
return {
|
|
561
|
+
content: [
|
|
562
|
+
{
|
|
563
|
+
type: 'text',
|
|
564
|
+
text: JSON.stringify({
|
|
565
|
+
success: false,
|
|
566
|
+
error: error.message,
|
|
567
|
+
}),
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
354
574
|
export default {
|
|
355
575
|
runTest,
|
|
356
576
|
runTestSuite,
|
|
577
|
+
generateTestReport,
|
|
578
|
+
listTestReports,
|
|
579
|
+
runTestSuiteWithReport,
|
|
357
580
|
};
|