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
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML Generator with Warnings Support
|
|
3
|
+
*
|
|
4
|
+
* Generates Maestro YAML from analyzed prompts.
|
|
5
|
+
* Supports:
|
|
6
|
+
* - Clean YAML for complete prompts
|
|
7
|
+
* - YAML with warnings for force-generated (incomplete) prompts
|
|
8
|
+
* - Automatic application of interaction patterns
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { CompletenessLevel, getDefaultAssumptions } from './promptAnalyzer.js';
|
|
12
|
+
import { getInteractionPatternHints, getYamlGenerationRules } from './knownIssues.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate YAML from analysis result
|
|
16
|
+
*
|
|
17
|
+
* @param {object} analysis - Result from analyzePrompt()
|
|
18
|
+
* @param {object} options - Generation options
|
|
19
|
+
* @returns {object} { yaml, warnings, assumptions, success }
|
|
20
|
+
*/
|
|
21
|
+
export function generateYamlFromAnalysis(analysis, options = {}) {
|
|
22
|
+
const { extracted, assessment } = analysis;
|
|
23
|
+
const { forceGenerate = false, includeComments = true } = options;
|
|
24
|
+
|
|
25
|
+
// Check if generation is possible
|
|
26
|
+
if (assessment.level === CompletenessLevel.INSUFFICIENT && !forceGenerate) {
|
|
27
|
+
return {
|
|
28
|
+
success: false,
|
|
29
|
+
yaml: null,
|
|
30
|
+
error: 'Cannot generate YAML: missing required information (app ID)',
|
|
31
|
+
missing: assessment.missing,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (assessment.level === CompletenessLevel.NEEDS_CLARIFICATION && !forceGenerate) {
|
|
36
|
+
return {
|
|
37
|
+
success: false,
|
|
38
|
+
yaml: null,
|
|
39
|
+
needsClarification: true,
|
|
40
|
+
level: assessment.level,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Get defaults for assumptions
|
|
45
|
+
const defaults = getDefaultAssumptions(analysis);
|
|
46
|
+
const warnings = [];
|
|
47
|
+
const assumptions = [];
|
|
48
|
+
|
|
49
|
+
// Build YAML
|
|
50
|
+
const yamlParts = [];
|
|
51
|
+
|
|
52
|
+
// App ID header
|
|
53
|
+
const appId = extracted.appContext.appId || options.appId;
|
|
54
|
+
if (!appId) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
yaml: null,
|
|
58
|
+
error: 'App ID is required',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
yamlParts.push(`appId: ${appId}`);
|
|
63
|
+
yamlParts.push('---');
|
|
64
|
+
|
|
65
|
+
// Add warning header if force generating
|
|
66
|
+
if (forceGenerate && assessment.level !== CompletenessLevel.COMPLETE) {
|
|
67
|
+
const warningHeader = generateWarningHeader(assessment, defaults);
|
|
68
|
+
yamlParts.push(warningHeader);
|
|
69
|
+
warnings.push(...assessment.assumptions);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Standard start
|
|
73
|
+
yamlParts.push('- clearState');
|
|
74
|
+
yamlParts.push('- launchApp');
|
|
75
|
+
yamlParts.push('');
|
|
76
|
+
|
|
77
|
+
// Wait for initial screen
|
|
78
|
+
if (includeComments) {
|
|
79
|
+
yamlParts.push('# Wait for app to fully load');
|
|
80
|
+
}
|
|
81
|
+
yamlParts.push('- waitForAnimationToEnd');
|
|
82
|
+
yamlParts.push('');
|
|
83
|
+
|
|
84
|
+
// Generate steps based on detected pattern
|
|
85
|
+
const steps = generateStepsFromSequence(analysis, defaults, options);
|
|
86
|
+
yamlParts.push(...steps.yaml);
|
|
87
|
+
warnings.push(...steps.warnings);
|
|
88
|
+
assumptions.push(...steps.assumptions);
|
|
89
|
+
|
|
90
|
+
// Add verification if detected or assumed
|
|
91
|
+
const verification = generateVerification(analysis, defaults, options);
|
|
92
|
+
if (verification.yaml.length > 0) {
|
|
93
|
+
yamlParts.push('');
|
|
94
|
+
yamlParts.push(...verification.yaml);
|
|
95
|
+
if (verification.assumed) {
|
|
96
|
+
assumptions.push('Verification element assumed based on common patterns');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
yaml: yamlParts.join('\n'),
|
|
103
|
+
warnings,
|
|
104
|
+
assumptions,
|
|
105
|
+
forceGenerated: forceGenerate && assessment.level !== CompletenessLevel.COMPLETE,
|
|
106
|
+
completenessScore: assessment.score,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generate warning header for force-generated YAML
|
|
112
|
+
*/
|
|
113
|
+
function generateWarningHeader(assessment, defaults) {
|
|
114
|
+
const lines = [
|
|
115
|
+
'# ⚠️ GENERATED WITH ASSUMPTIONS',
|
|
116
|
+
'# The following values were assumed - verify they match your app:',
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
for (const assumption of assessment.assumptions) {
|
|
120
|
+
lines.push(`# - ${assumption}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (defaults.alternatives) {
|
|
124
|
+
lines.push('#');
|
|
125
|
+
lines.push('# Alternative selectors to try if defaults fail:');
|
|
126
|
+
for (const [field, alts] of Object.entries(defaults.alternatives)) {
|
|
127
|
+
lines.push(`# ${field}: ${alts.join(' / ')}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
lines.push('#');
|
|
132
|
+
lines.push('');
|
|
133
|
+
|
|
134
|
+
return lines.join('\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generate steps from analyzed sequence
|
|
139
|
+
*/
|
|
140
|
+
function generateStepsFromSequence(analysis, defaults, options) {
|
|
141
|
+
const { extracted } = analysis;
|
|
142
|
+
const yaml = [];
|
|
143
|
+
const warnings = [];
|
|
144
|
+
const assumptions = [];
|
|
145
|
+
|
|
146
|
+
const includeComments = options.includeComments !== false;
|
|
147
|
+
|
|
148
|
+
// Track if we've had text input (need to hide keyboard before buttons)
|
|
149
|
+
let hadTextInput = false;
|
|
150
|
+
|
|
151
|
+
for (const step of extracted.sequence) {
|
|
152
|
+
switch (step.action) {
|
|
153
|
+
case 'INPUT': {
|
|
154
|
+
const fieldLabel = step.element || defaults.usernameField || '{field_name}';
|
|
155
|
+
const value = step.value || extracted.values[step.elementType?.toLowerCase()] || '{value}';
|
|
156
|
+
|
|
157
|
+
if (includeComments) {
|
|
158
|
+
yaml.push(`# Enter ${step.elementType || 'text'}`);
|
|
159
|
+
}
|
|
160
|
+
yaml.push(`- tapOn: "${fieldLabel}"`);
|
|
161
|
+
yaml.push(`- inputText: "${value}"`);
|
|
162
|
+
yaml.push('');
|
|
163
|
+
|
|
164
|
+
hadTextInput = true;
|
|
165
|
+
|
|
166
|
+
if (!step.element) {
|
|
167
|
+
assumptions.push(`Field label assumed: "${fieldLabel}"`);
|
|
168
|
+
}
|
|
169
|
+
if (!step.value) {
|
|
170
|
+
warnings.push(`Value placeholder used for: ${fieldLabel}`);
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case 'TAP':
|
|
176
|
+
case 'SUBMIT': {
|
|
177
|
+
// Hide keyboard before tapping if we had text input
|
|
178
|
+
if (hadTextInput) {
|
|
179
|
+
if (includeComments) {
|
|
180
|
+
yaml.push('# Hide keyboard before tapping button');
|
|
181
|
+
}
|
|
182
|
+
yaml.push('- hideKeyboard');
|
|
183
|
+
yaml.push('- waitForAnimationToEnd');
|
|
184
|
+
yaml.push('');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const buttonLabel = step.element || defaults.submitButton || '{button_text}';
|
|
188
|
+
|
|
189
|
+
if (includeComments) {
|
|
190
|
+
yaml.push(`# Tap ${step.action === 'SUBMIT' ? 'submit button' : 'element'}`);
|
|
191
|
+
}
|
|
192
|
+
yaml.push(`- tapOn: "${buttonLabel}"`);
|
|
193
|
+
yaml.push('');
|
|
194
|
+
|
|
195
|
+
if (!step.element) {
|
|
196
|
+
assumptions.push(`Button text assumed: "${buttonLabel}"`);
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
case 'SELECT': {
|
|
202
|
+
// Hide keyboard before dropdown
|
|
203
|
+
if (hadTextInput) {
|
|
204
|
+
yaml.push('- hideKeyboard');
|
|
205
|
+
yaml.push('- waitForAnimationToEnd');
|
|
206
|
+
yaml.push('');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const dropdownTrigger = step.element || '{dropdown_trigger}';
|
|
210
|
+
const optionValue = step.value || '{option_to_select}';
|
|
211
|
+
|
|
212
|
+
if (includeComments) {
|
|
213
|
+
yaml.push('# Dropdown interaction');
|
|
214
|
+
}
|
|
215
|
+
yaml.push(`- tapOn: "${dropdownTrigger}"`);
|
|
216
|
+
yaml.push('- waitForAnimationToEnd');
|
|
217
|
+
yaml.push(`- tapOn: "${optionValue}"`);
|
|
218
|
+
yaml.push('- waitForAnimationToEnd');
|
|
219
|
+
yaml.push('');
|
|
220
|
+
|
|
221
|
+
if (!step.element) {
|
|
222
|
+
assumptions.push(`Dropdown trigger assumed: "${dropdownTrigger}"`);
|
|
223
|
+
}
|
|
224
|
+
if (!step.value) {
|
|
225
|
+
warnings.push(`Dropdown option placeholder used`);
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
case 'SCROLL': {
|
|
231
|
+
if (includeComments) {
|
|
232
|
+
yaml.push('# Scroll to find element');
|
|
233
|
+
}
|
|
234
|
+
yaml.push('- scroll');
|
|
235
|
+
yaml.push('- waitForAnimationToEnd');
|
|
236
|
+
yaml.push('');
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
case 'SEARCH': {
|
|
241
|
+
const searchField = step.element || defaults.searchField || 'Search';
|
|
242
|
+
const searchValue = step.value || extracted.values.search || '{search_term}';
|
|
243
|
+
|
|
244
|
+
if (includeComments) {
|
|
245
|
+
yaml.push('# Search');
|
|
246
|
+
}
|
|
247
|
+
yaml.push(`- tapOn: "${searchField}"`);
|
|
248
|
+
yaml.push(`- inputText: "${searchValue}"`);
|
|
249
|
+
yaml.push('- pressKey: enter');
|
|
250
|
+
yaml.push('- waitForAnimationToEnd');
|
|
251
|
+
yaml.push('');
|
|
252
|
+
|
|
253
|
+
hadTextInput = true;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case 'NAVIGATE': {
|
|
258
|
+
if (step.element) {
|
|
259
|
+
if (includeComments) {
|
|
260
|
+
yaml.push(`# Navigate to ${step.element}`);
|
|
261
|
+
}
|
|
262
|
+
yaml.push(`- tapOn: "${step.element}"`);
|
|
263
|
+
yaml.push('- waitForAnimationToEnd');
|
|
264
|
+
yaml.push('');
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case 'VERIFY': {
|
|
270
|
+
// Verifications are handled separately
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
default: {
|
|
275
|
+
if (step.element) {
|
|
276
|
+
yaml.push(`- tapOn: "${step.element}"`);
|
|
277
|
+
yaml.push('');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// If no explicit steps but we have values, generate login-like pattern
|
|
284
|
+
if (yaml.length === 0 && Object.keys(extracted.values).length > 0) {
|
|
285
|
+
const { values } = extracted;
|
|
286
|
+
|
|
287
|
+
if (values.username || values.email || values.user) {
|
|
288
|
+
yaml.push(`- tapOn: "${defaults.usernameField || 'Username'}"`);
|
|
289
|
+
yaml.push(`- inputText: "${values.username || values.email || values.user}"`);
|
|
290
|
+
yaml.push('');
|
|
291
|
+
assumptions.push('Assumed login pattern from username/password values');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (values.password || values.pass) {
|
|
295
|
+
yaml.push(`- tapOn: "${defaults.passwordField || 'Password'}"`);
|
|
296
|
+
yaml.push(`- inputText: "${values.password || values.pass}"`);
|
|
297
|
+
yaml.push('');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Add submit
|
|
301
|
+
yaml.push('- hideKeyboard');
|
|
302
|
+
yaml.push('- waitForAnimationToEnd');
|
|
303
|
+
yaml.push('');
|
|
304
|
+
yaml.push(`- tapOn: "${defaults.submitButton || 'Sign In'}"`);
|
|
305
|
+
yaml.push('');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { yaml, warnings, assumptions };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Generate verification steps
|
|
313
|
+
*/
|
|
314
|
+
function generateVerification(analysis, defaults, options) {
|
|
315
|
+
const { extracted } = analysis;
|
|
316
|
+
const yaml = [];
|
|
317
|
+
let assumed = false;
|
|
318
|
+
|
|
319
|
+
const includeComments = options.includeComments !== false;
|
|
320
|
+
|
|
321
|
+
if (extracted.verifications.length > 0) {
|
|
322
|
+
if (includeComments) {
|
|
323
|
+
yaml.push('# Verify expected result');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Wait for navigation/loading
|
|
327
|
+
yaml.push('- extendedWaitUntil:');
|
|
328
|
+
|
|
329
|
+
for (const verification of extracted.verifications) {
|
|
330
|
+
if (verification.target) {
|
|
331
|
+
yaml.push(` visible: "${verification.target}"`);
|
|
332
|
+
yaml.push(' timeout: 30000');
|
|
333
|
+
yaml.push('');
|
|
334
|
+
yaml.push(`- assertVisible: "${verification.target}"`);
|
|
335
|
+
break; // Just use first verification for extendedWaitUntil
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Add remaining verifications as assertions
|
|
340
|
+
for (let i = 1; i < extracted.verifications.length; i++) {
|
|
341
|
+
const v = extracted.verifications[i];
|
|
342
|
+
if (v.target) {
|
|
343
|
+
yaml.push(`- assertVisible: "${v.target}"`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
// No explicit verification - add warning comment
|
|
348
|
+
if (includeComments) {
|
|
349
|
+
yaml.push('# ⚠️ WARNING: No success verification specified');
|
|
350
|
+
yaml.push('# Add: - assertVisible: "{your_success_element}"');
|
|
351
|
+
}
|
|
352
|
+
yaml.push('- waitForAnimationToEnd');
|
|
353
|
+
assumed = true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return { yaml, assumed };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Generate YAML structure with applied interaction patterns
|
|
361
|
+
* This ensures best practices are followed
|
|
362
|
+
*/
|
|
363
|
+
export function applyInteractionPatterns(yaml) {
|
|
364
|
+
const rules = getYamlGenerationRules();
|
|
365
|
+
let modifiedYaml = yaml;
|
|
366
|
+
const appliedPatterns = [];
|
|
367
|
+
|
|
368
|
+
// Pattern: tapOn before inputText
|
|
369
|
+
if (rules.mandatory.tapBeforeInput) {
|
|
370
|
+
// Check for inputText without preceding tapOn
|
|
371
|
+
const lines = modifiedYaml.split('\n');
|
|
372
|
+
const fixedLines = [];
|
|
373
|
+
|
|
374
|
+
for (let i = 0; i < lines.length; i++) {
|
|
375
|
+
const line = lines[i];
|
|
376
|
+
const prevLine = i > 0 ? lines[i - 1] : '';
|
|
377
|
+
|
|
378
|
+
if (line.trim().startsWith('- inputText:') && !prevLine.trim().startsWith('- tapOn:')) {
|
|
379
|
+
// This is a pattern violation - add warning comment
|
|
380
|
+
fixedLines.push('# ⚠️ PATTERN: tapOn should precede inputText');
|
|
381
|
+
appliedPatterns.push('Added tapOn reminder before inputText');
|
|
382
|
+
}
|
|
383
|
+
fixedLines.push(line);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
modifiedYaml = fixedLines.join('\n');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
yaml: modifiedYaml,
|
|
391
|
+
appliedPatterns,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Format a complete YAML generation response
|
|
397
|
+
*/
|
|
398
|
+
export function formatGenerationResponse(result) {
|
|
399
|
+
if (!result.success) {
|
|
400
|
+
return {
|
|
401
|
+
success: false,
|
|
402
|
+
error: result.error,
|
|
403
|
+
needsClarification: result.needsClarification || false,
|
|
404
|
+
missing: result.missing || [],
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
success: true,
|
|
410
|
+
yaml: result.yaml,
|
|
411
|
+
forceGenerated: result.forceGenerated || false,
|
|
412
|
+
completenessScore: result.completenessScore,
|
|
413
|
+
warnings: result.warnings || [],
|
|
414
|
+
assumptions: result.assumptions || [],
|
|
415
|
+
message: result.forceGenerated
|
|
416
|
+
? 'YAML generated with assumptions. Review warnings and update placeholders.'
|
|
417
|
+
: 'YAML generated successfully.',
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export default {
|
|
422
|
+
generateYamlFromAnalysis,
|
|
423
|
+
applyInteractionPatterns,
|
|
424
|
+
formatGenerationResponse,
|
|
425
|
+
};
|
|
426
|
+
|