mcp-maestro-mobile-ai 1.3.1 → 1.6.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 +344 -152
- package/ROADMAP.md +21 -8
- package/package.json +9 -3
- package/src/mcp-server/index.js +1394 -826
- package/src/mcp-server/schemas/toolSchemas.js +820 -0
- package/src/mcp-server/tools/contextTools.js +309 -2
- package/src/mcp-server/tools/runTools.js +409 -31
- package/src/mcp-server/utils/knownIssues.js +564 -0
- package/src/mcp-server/utils/maestro.js +265 -29
- package/src/mcp-server/utils/promptAnalyzer.js +701 -0
- package/src/mcp-server/utils/security.js +1200 -0
- package/src/mcp-server/utils/yamlCache.js +381 -0
- package/src/mcp-server/utils/yamlGenerator.js +426 -0
- package/src/mcp-server/utils/yamlTemplate.js +303 -0
|
@@ -20,7 +20,26 @@ import {
|
|
|
20
20
|
TEST_PATTERNS,
|
|
21
21
|
validateYamlStructure,
|
|
22
22
|
getScreenAnalysisInstructions,
|
|
23
|
+
getInteractionPatternHints,
|
|
24
|
+
getYamlGenerationRules,
|
|
25
|
+
getBestPractices,
|
|
26
|
+
PatternCategory,
|
|
23
27
|
} from "../utils/yamlTemplate.js";
|
|
28
|
+
import {
|
|
29
|
+
INTERACTION_PATTERNS,
|
|
30
|
+
getPatternsByCategory,
|
|
31
|
+
getPatternCategories,
|
|
32
|
+
} from "../utils/knownIssues.js";
|
|
33
|
+
import {
|
|
34
|
+
analyzePrompt,
|
|
35
|
+
generateClarificationQuestions,
|
|
36
|
+
generateWarnings,
|
|
37
|
+
CompletenessLevel,
|
|
38
|
+
} from "../utils/promptAnalyzer.js";
|
|
39
|
+
import {
|
|
40
|
+
generateYamlFromAnalysis,
|
|
41
|
+
formatGenerationResponse,
|
|
42
|
+
} from "../utils/yamlGenerator.js";
|
|
24
43
|
|
|
25
44
|
/**
|
|
26
45
|
* Format result as MCP response
|
|
@@ -259,6 +278,9 @@ export async function getTestPattern(patternName) {
|
|
|
259
278
|
list: TEST_PATTERNS.list,
|
|
260
279
|
settings: TEST_PATTERNS.settings,
|
|
261
280
|
logout: TEST_PATTERNS.logout,
|
|
281
|
+
dropdown: TEST_PATTERNS.dropdown,
|
|
282
|
+
flutter_login_with_dropdown: TEST_PATTERNS.flutter_login_with_dropdown,
|
|
283
|
+
flutter_multi_dropdown: TEST_PATTERNS.flutter_multi_dropdown,
|
|
262
284
|
};
|
|
263
285
|
|
|
264
286
|
const pattern = patterns[patternName?.toLowerCase()];
|
|
@@ -268,7 +290,7 @@ export async function getTestPattern(patternName) {
|
|
|
268
290
|
success: false,
|
|
269
291
|
error: `Unknown pattern: ${patternName}`,
|
|
270
292
|
availablePatterns: Object.keys(patterns),
|
|
271
|
-
hint: "Use: login, form, search, navigation, list, settings, or
|
|
293
|
+
hint: "Use: login, form, search, navigation, list, settings, logout, dropdown, flutter_login_with_dropdown, or flutter_multi_dropdown",
|
|
272
294
|
});
|
|
273
295
|
}
|
|
274
296
|
|
|
@@ -276,7 +298,7 @@ export async function getTestPattern(patternName) {
|
|
|
276
298
|
success: true,
|
|
277
299
|
pattern: patternName,
|
|
278
300
|
template: pattern,
|
|
279
|
-
message: "Replace placeholders in {} with actual values. REMEMBER: Always use tapOn before inputText!",
|
|
301
|
+
message: "Replace placeholders in {} with actual values. REMEMBER: Always use tapOn before inputText! For dropdowns: hideKeyboard -> tapCurrentValue -> waitForAnimationToEnd -> tapDesiredValue",
|
|
280
302
|
});
|
|
281
303
|
}
|
|
282
304
|
|
|
@@ -299,6 +321,284 @@ export async function getScreenAnalysis() {
|
|
|
299
321
|
});
|
|
300
322
|
}
|
|
301
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Get interaction patterns for reliable YAML generation
|
|
326
|
+
* These patterns are framework-agnostic and work across all mobile apps.
|
|
327
|
+
*/
|
|
328
|
+
export async function getKnownIssues(category = null) {
|
|
329
|
+
let patterns = INTERACTION_PATTERNS;
|
|
330
|
+
|
|
331
|
+
if (category) {
|
|
332
|
+
patterns = getPatternsByCategory(category);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Format patterns for display
|
|
336
|
+
const formattedPatterns = Object.entries(patterns).map(([key, pattern]) => ({
|
|
337
|
+
name: key,
|
|
338
|
+
category: pattern.category,
|
|
339
|
+
situation: pattern.situation,
|
|
340
|
+
strategy: pattern.strategy,
|
|
341
|
+
rationale: pattern.rationale,
|
|
342
|
+
}));
|
|
343
|
+
|
|
344
|
+
return formatResponse({
|
|
345
|
+
success: true,
|
|
346
|
+
count: formattedPatterns.length,
|
|
347
|
+
patterns: formattedPatterns,
|
|
348
|
+
availableCategories: getPatternCategories(),
|
|
349
|
+
message: "These patterns are automatically included in YAML generation. Apply them to create reliable tests.",
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get interaction patterns by category
|
|
355
|
+
*/
|
|
356
|
+
export async function getPlatformRules(category) {
|
|
357
|
+
const validCategories = getPatternCategories();
|
|
358
|
+
|
|
359
|
+
if (!category || !validCategories.includes(category)) {
|
|
360
|
+
return formatResponse({
|
|
361
|
+
success: false,
|
|
362
|
+
error: `Invalid category: ${category}`,
|
|
363
|
+
validCategories,
|
|
364
|
+
hint: "Use: keyboard_handling, element_interaction, timing_synchronization, input_handling, dropdown_picker, navigation, scroll_visibility, or fallback_strategies",
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const patterns = getPatternsByCategory(category);
|
|
369
|
+
|
|
370
|
+
return formatResponse({
|
|
371
|
+
success: true,
|
|
372
|
+
category,
|
|
373
|
+
patterns: Object.entries(patterns).map(([key, p]) => ({
|
|
374
|
+
name: key,
|
|
375
|
+
situation: p.situation,
|
|
376
|
+
strategy: p.strategy,
|
|
377
|
+
yamlPattern: p.yamlPattern,
|
|
378
|
+
})),
|
|
379
|
+
message: "Apply these patterns for the specified interaction type.",
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get all YAML generation rules and best practices
|
|
385
|
+
*/
|
|
386
|
+
export async function getGenerationRules() {
|
|
387
|
+
const rules = getYamlGenerationRules();
|
|
388
|
+
const hints = getInteractionPatternHints();
|
|
389
|
+
const bestPractices = getBestPractices();
|
|
390
|
+
|
|
391
|
+
return formatResponse({
|
|
392
|
+
success: true,
|
|
393
|
+
rules,
|
|
394
|
+
hints,
|
|
395
|
+
bestPractices,
|
|
396
|
+
message: "These rules are automatically applied during YAML generation. They are framework-agnostic and improve test reliability.",
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Validate and analyze user prompt for YAML generation
|
|
402
|
+
* Uses Action-Element-Verification model for generic prompt analysis
|
|
403
|
+
*
|
|
404
|
+
* @param {string} userPrompt - The user's test description
|
|
405
|
+
* @param {object} options - Options: appId, forceGenerate
|
|
406
|
+
* @returns {object} Analysis result with YAML or clarification questions
|
|
407
|
+
*/
|
|
408
|
+
export async function validateAndGenerate(userPrompt, options = {}) {
|
|
409
|
+
const { appId = null, forceGenerate = false } = options;
|
|
410
|
+
|
|
411
|
+
if (!userPrompt || typeof userPrompt !== 'string' || userPrompt.trim().length < 5) {
|
|
412
|
+
return formatResponse({
|
|
413
|
+
success: false,
|
|
414
|
+
error: "Prompt is too short or empty",
|
|
415
|
+
hint: "Provide a description of the test you want to create. Example: 'Test login with username abc and password 1234'",
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Step 1: Analyze the prompt
|
|
420
|
+
const analysis = analyzePrompt(userPrompt, { appId });
|
|
421
|
+
|
|
422
|
+
// Step 2: Check completeness level
|
|
423
|
+
const { assessment, extracted } = analysis;
|
|
424
|
+
|
|
425
|
+
// INSUFFICIENT: Cannot proceed without critical info
|
|
426
|
+
if (assessment.level === CompletenessLevel.INSUFFICIENT) {
|
|
427
|
+
const questions = generateClarificationQuestions(analysis);
|
|
428
|
+
const criticalQuestions = questions.filter(q => q.priority === 'critical');
|
|
429
|
+
|
|
430
|
+
return formatResponse({
|
|
431
|
+
success: false,
|
|
432
|
+
status: 'insufficient',
|
|
433
|
+
message: 'Cannot generate YAML: missing required information',
|
|
434
|
+
understood: {
|
|
435
|
+
appContext: extracted.appContext,
|
|
436
|
+
detectedActions: extracted.actions.map(a => a.type),
|
|
437
|
+
detectedElements: extracted.elements.map(e => e.type),
|
|
438
|
+
valuesFound: Object.keys(extracted.values),
|
|
439
|
+
},
|
|
440
|
+
requiredInfo: criticalQuestions.map(q => ({
|
|
441
|
+
field: q.field,
|
|
442
|
+
question: q.question,
|
|
443
|
+
})),
|
|
444
|
+
hint: 'Please provide at least the app ID (package name) to continue.',
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// NEEDS_CLARIFICATION: Should ask, but can force generate
|
|
449
|
+
if (assessment.level === CompletenessLevel.NEEDS_CLARIFICATION && !forceGenerate) {
|
|
450
|
+
const questions = generateClarificationQuestions(analysis);
|
|
451
|
+
|
|
452
|
+
return formatResponse({
|
|
453
|
+
success: false,
|
|
454
|
+
status: 'needs_clarification',
|
|
455
|
+
message: 'Additional information recommended for reliable test generation',
|
|
456
|
+
completenessScore: assessment.score,
|
|
457
|
+
understood: {
|
|
458
|
+
appId: extracted.appContext.appId,
|
|
459
|
+
appName: extracted.appContext.appName,
|
|
460
|
+
detectedActions: extracted.actions.map(a => ({ type: a.type, context: a.context })),
|
|
461
|
+
detectedElements: extracted.elements.map(e => ({ type: e.type, label: e.label })),
|
|
462
|
+
valuesFound: extracted.values,
|
|
463
|
+
verifications: extracted.verifications.map(v => ({ type: v.type, target: v.target })),
|
|
464
|
+
},
|
|
465
|
+
questions: questions.map(q => ({
|
|
466
|
+
priority: q.priority,
|
|
467
|
+
field: q.field,
|
|
468
|
+
question: q.question,
|
|
469
|
+
required: q.required,
|
|
470
|
+
default: q.default,
|
|
471
|
+
})),
|
|
472
|
+
canForceGenerate: true,
|
|
473
|
+
hint: 'Answer the questions for better accuracy, or use forceGenerate: true to proceed with assumptions.',
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// GENERATABLE: Can generate with assumptions (if forceGenerate is true)
|
|
478
|
+
// or ask for clarification
|
|
479
|
+
if (assessment.level === CompletenessLevel.GENERATABLE && !forceGenerate) {
|
|
480
|
+
const questions = generateClarificationQuestions(analysis);
|
|
481
|
+
const recommendedQuestions = questions.filter(q => q.priority === 'recommended');
|
|
482
|
+
|
|
483
|
+
return formatResponse({
|
|
484
|
+
success: false,
|
|
485
|
+
status: 'can_improve',
|
|
486
|
+
message: 'Prompt understood, but additional details would improve test reliability',
|
|
487
|
+
completenessScore: assessment.score,
|
|
488
|
+
understood: {
|
|
489
|
+
appId: extracted.appContext.appId,
|
|
490
|
+
detectedActions: extracted.actions.map(a => ({ type: a.type, context: a.context })),
|
|
491
|
+
detectedElements: extracted.elements.map(e => ({ type: e.type, label: e.label })),
|
|
492
|
+
valuesFound: extracted.values,
|
|
493
|
+
},
|
|
494
|
+
recommendations: recommendedQuestions.map(q => ({
|
|
495
|
+
field: q.field,
|
|
496
|
+
question: q.question,
|
|
497
|
+
default: q.default,
|
|
498
|
+
})),
|
|
499
|
+
assumptionsIfForced: assessment.assumptions,
|
|
500
|
+
canForceGenerate: true,
|
|
501
|
+
hint: 'Provide additional details or use forceGenerate: true to generate with assumptions.',
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// COMPLETE or forceGenerate: Generate YAML
|
|
506
|
+
const result = generateYamlFromAnalysis(analysis, {
|
|
507
|
+
forceGenerate,
|
|
508
|
+
includeComments: true,
|
|
509
|
+
appId: appId || extracted.appContext.appId,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
if (!result.success) {
|
|
513
|
+
return formatResponse({
|
|
514
|
+
success: false,
|
|
515
|
+
error: result.error,
|
|
516
|
+
missing: result.missing,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Generate warnings if force generated
|
|
521
|
+
const warnings = forceGenerate && assessment.level !== CompletenessLevel.COMPLETE
|
|
522
|
+
? generateWarnings(analysis)
|
|
523
|
+
: [];
|
|
524
|
+
|
|
525
|
+
return formatResponse({
|
|
526
|
+
success: true,
|
|
527
|
+
status: result.forceGenerated ? 'generated_with_warnings' : 'generated',
|
|
528
|
+
message: result.forceGenerated
|
|
529
|
+
? 'YAML generated with assumptions. Review and update placeholders before running.'
|
|
530
|
+
: 'YAML generated successfully.',
|
|
531
|
+
yaml: result.yaml,
|
|
532
|
+
completenessScore: result.completenessScore,
|
|
533
|
+
forceGenerated: result.forceGenerated || false,
|
|
534
|
+
warnings: [
|
|
535
|
+
...result.warnings,
|
|
536
|
+
...warnings.map(w => w.message),
|
|
537
|
+
],
|
|
538
|
+
assumptions: result.assumptions,
|
|
539
|
+
analysis: {
|
|
540
|
+
detectedActions: extracted.actions.length,
|
|
541
|
+
detectedElements: extracted.elements.length,
|
|
542
|
+
valuesFound: Object.keys(extracted.values).length,
|
|
543
|
+
verifications: extracted.verifications.length,
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Analyze prompt without generating YAML
|
|
550
|
+
* Useful for understanding what the system detected
|
|
551
|
+
*/
|
|
552
|
+
export async function analyzeTestPrompt(userPrompt, appId = null) {
|
|
553
|
+
if (!userPrompt || typeof userPrompt !== 'string') {
|
|
554
|
+
return formatResponse({
|
|
555
|
+
success: false,
|
|
556
|
+
error: "Invalid prompt",
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const analysis = analyzePrompt(userPrompt, { appId });
|
|
561
|
+
const { extracted, assessment } = analysis;
|
|
562
|
+
const questions = generateClarificationQuestions(analysis);
|
|
563
|
+
|
|
564
|
+
return formatResponse({
|
|
565
|
+
success: true,
|
|
566
|
+
analysis: {
|
|
567
|
+
appContext: extracted.appContext,
|
|
568
|
+
actions: extracted.actions.map(a => ({
|
|
569
|
+
type: a.type,
|
|
570
|
+
keyword: a.keyword,
|
|
571
|
+
context: a.context,
|
|
572
|
+
})),
|
|
573
|
+
elements: extracted.elements.map(e => ({
|
|
574
|
+
type: e.type,
|
|
575
|
+
label: e.label,
|
|
576
|
+
keyword: e.keyword,
|
|
577
|
+
})),
|
|
578
|
+
values: extracted.values,
|
|
579
|
+
verifications: extracted.verifications.map(v => ({
|
|
580
|
+
type: v.type,
|
|
581
|
+
target: v.target,
|
|
582
|
+
})),
|
|
583
|
+
sequence: extracted.sequence,
|
|
584
|
+
},
|
|
585
|
+
assessment: {
|
|
586
|
+
level: assessment.level,
|
|
587
|
+
score: assessment.score,
|
|
588
|
+
issues: assessment.issues,
|
|
589
|
+
missing: assessment.missing,
|
|
590
|
+
assumptions: assessment.assumptions,
|
|
591
|
+
canForceGenerate: assessment.canForceGenerate,
|
|
592
|
+
},
|
|
593
|
+
clarificationQuestions: questions.map(q => ({
|
|
594
|
+
priority: q.priority,
|
|
595
|
+
field: q.field,
|
|
596
|
+
question: q.question,
|
|
597
|
+
required: q.required,
|
|
598
|
+
})),
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
302
602
|
export default {
|
|
303
603
|
registerAppElements,
|
|
304
604
|
registerAppScreen,
|
|
@@ -313,5 +613,12 @@ export default {
|
|
|
313
613
|
validateYamlBeforeRun,
|
|
314
614
|
getTestPattern,
|
|
315
615
|
getScreenAnalysis,
|
|
616
|
+
// Known Issues Tools
|
|
617
|
+
getKnownIssues,
|
|
618
|
+
getPlatformRules,
|
|
619
|
+
getGenerationRules,
|
|
620
|
+
// Prompt Analysis & Generation
|
|
621
|
+
validateAndGenerate,
|
|
622
|
+
analyzeTestPrompt,
|
|
316
623
|
};
|
|
317
624
|
|