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,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates HTML and JSON reports from test results
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
|
|
11
|
+
// Report storage location
|
|
12
|
+
const USER_HOME = os.homedir();
|
|
13
|
+
const REPORTS_DIR = path.join(USER_HOME, '.maestro-mcp', 'reports');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ensure reports directory exists
|
|
17
|
+
*/
|
|
18
|
+
async function ensureReportsDir() {
|
|
19
|
+
await fs.mkdir(REPORTS_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generate a unique report ID
|
|
24
|
+
*/
|
|
25
|
+
function generateReportId() {
|
|
26
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
27
|
+
return `report-${timestamp}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate JSON report
|
|
32
|
+
*/
|
|
33
|
+
export async function generateJsonReport(results, metadata = {}) {
|
|
34
|
+
await ensureReportsDir();
|
|
35
|
+
|
|
36
|
+
const reportId = generateReportId();
|
|
37
|
+
const report = {
|
|
38
|
+
reportId,
|
|
39
|
+
generatedAt: new Date().toISOString(),
|
|
40
|
+
metadata: {
|
|
41
|
+
promptFile: metadata.promptFile || 'unknown',
|
|
42
|
+
appId: metadata.appId || 'unknown',
|
|
43
|
+
totalTests: results.length,
|
|
44
|
+
...metadata,
|
|
45
|
+
},
|
|
46
|
+
summary: {
|
|
47
|
+
total: results.length,
|
|
48
|
+
passed: results.filter(r => r.success).length,
|
|
49
|
+
failed: results.filter(r => !r.success).length,
|
|
50
|
+
passRate: results.length > 0
|
|
51
|
+
? Math.round((results.filter(r => r.success).length / results.length) * 100)
|
|
52
|
+
: 0,
|
|
53
|
+
},
|
|
54
|
+
tests: results.map((result, index) => ({
|
|
55
|
+
index: index + 1,
|
|
56
|
+
name: result.name,
|
|
57
|
+
success: result.success,
|
|
58
|
+
duration: result.duration || 'N/A',
|
|
59
|
+
attempts: result.attempts || 1,
|
|
60
|
+
error: result.error || null,
|
|
61
|
+
screenshot: result.screenshot || null,
|
|
62
|
+
})),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const jsonPath = path.join(REPORTS_DIR, `${reportId}.json`);
|
|
66
|
+
await fs.writeFile(jsonPath, JSON.stringify(report, null, 2), 'utf8');
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
reportId,
|
|
70
|
+
jsonPath,
|
|
71
|
+
report,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Generate HTML report
|
|
77
|
+
*/
|
|
78
|
+
export async function generateHtmlReport(results, metadata = {}) {
|
|
79
|
+
await ensureReportsDir();
|
|
80
|
+
|
|
81
|
+
const reportId = generateReportId();
|
|
82
|
+
const timestamp = new Date().toLocaleString();
|
|
83
|
+
|
|
84
|
+
const total = results.length;
|
|
85
|
+
const passed = results.filter(r => r.success).length;
|
|
86
|
+
const failed = total - passed;
|
|
87
|
+
const passRate = total > 0 ? Math.round((passed / total) * 100) : 0;
|
|
88
|
+
|
|
89
|
+
// Generate test rows
|
|
90
|
+
const testRows = results.map((result, index) => {
|
|
91
|
+
const statusClass = result.success ? 'passed' : 'failed';
|
|
92
|
+
const statusIcon = result.success ? '✅' : '❌';
|
|
93
|
+
const errorSection = result.error
|
|
94
|
+
? `<div class="error-details">${escapeHtml(result.error)}</div>`
|
|
95
|
+
: '';
|
|
96
|
+
|
|
97
|
+
return `
|
|
98
|
+
<tr class="${statusClass}">
|
|
99
|
+
<td>${index + 1}</td>
|
|
100
|
+
<td>${escapeHtml(result.name)}</td>
|
|
101
|
+
<td class="status-${statusClass}">${statusIcon} ${result.success ? 'PASSED' : 'FAILED'}</td>
|
|
102
|
+
<td>${result.duration || 'N/A'}</td>
|
|
103
|
+
<td>${result.attempts || 1}</td>
|
|
104
|
+
<td>${errorSection}</td>
|
|
105
|
+
</tr>
|
|
106
|
+
`;
|
|
107
|
+
}).join('\n');
|
|
108
|
+
|
|
109
|
+
const html = `<!DOCTYPE html>
|
|
110
|
+
<html lang="en">
|
|
111
|
+
<head>
|
|
112
|
+
<meta charset="UTF-8">
|
|
113
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
114
|
+
<title>Test Report - ${reportId}</title>
|
|
115
|
+
<style>
|
|
116
|
+
:root {
|
|
117
|
+
--bg-color: #0d1117;
|
|
118
|
+
--card-bg: #161b22;
|
|
119
|
+
--text-color: #c9d1d9;
|
|
120
|
+
--text-muted: #8b949e;
|
|
121
|
+
--border-color: #30363d;
|
|
122
|
+
--success-color: #238636;
|
|
123
|
+
--success-bg: #1a4721;
|
|
124
|
+
--error-color: #f85149;
|
|
125
|
+
--error-bg: #4a1d1d;
|
|
126
|
+
--primary-color: #58a6ff;
|
|
127
|
+
--header-bg: #21262d;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
* {
|
|
131
|
+
box-sizing: border-box;
|
|
132
|
+
margin: 0;
|
|
133
|
+
padding: 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
body {
|
|
137
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
138
|
+
background-color: var(--bg-color);
|
|
139
|
+
color: var(--text-color);
|
|
140
|
+
line-height: 1.6;
|
|
141
|
+
padding: 20px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.container {
|
|
145
|
+
max-width: 1200px;
|
|
146
|
+
margin: 0 auto;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
header {
|
|
150
|
+
background: var(--card-bg);
|
|
151
|
+
border: 1px solid var(--border-color);
|
|
152
|
+
border-radius: 12px;
|
|
153
|
+
padding: 24px;
|
|
154
|
+
margin-bottom: 24px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
h1 {
|
|
158
|
+
font-size: 24px;
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
margin-bottom: 8px;
|
|
161
|
+
color: var(--primary-color);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.meta {
|
|
165
|
+
color: var(--text-muted);
|
|
166
|
+
font-size: 14px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.summary-cards {
|
|
170
|
+
display: grid;
|
|
171
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
172
|
+
gap: 16px;
|
|
173
|
+
margin-bottom: 24px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.card {
|
|
177
|
+
background: var(--card-bg);
|
|
178
|
+
border: 1px solid var(--border-color);
|
|
179
|
+
border-radius: 12px;
|
|
180
|
+
padding: 20px;
|
|
181
|
+
text-align: center;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.card-value {
|
|
185
|
+
font-size: 36px;
|
|
186
|
+
font-weight: 700;
|
|
187
|
+
margin-bottom: 4px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.card-label {
|
|
191
|
+
color: var(--text-muted);
|
|
192
|
+
font-size: 14px;
|
|
193
|
+
text-transform: uppercase;
|
|
194
|
+
letter-spacing: 0.5px;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.card-passed .card-value { color: var(--success-color); }
|
|
198
|
+
.card-failed .card-value { color: var(--error-color); }
|
|
199
|
+
.card-rate .card-value { color: var(--primary-color); }
|
|
200
|
+
|
|
201
|
+
.results-table {
|
|
202
|
+
background: var(--card-bg);
|
|
203
|
+
border: 1px solid var(--border-color);
|
|
204
|
+
border-radius: 12px;
|
|
205
|
+
overflow: hidden;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
table {
|
|
209
|
+
width: 100%;
|
|
210
|
+
border-collapse: collapse;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
th {
|
|
214
|
+
background: var(--header-bg);
|
|
215
|
+
padding: 14px 16px;
|
|
216
|
+
text-align: left;
|
|
217
|
+
font-weight: 600;
|
|
218
|
+
font-size: 13px;
|
|
219
|
+
text-transform: uppercase;
|
|
220
|
+
letter-spacing: 0.5px;
|
|
221
|
+
color: var(--text-muted);
|
|
222
|
+
border-bottom: 1px solid var(--border-color);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
td {
|
|
226
|
+
padding: 14px 16px;
|
|
227
|
+
border-bottom: 1px solid var(--border-color);
|
|
228
|
+
font-size: 14px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
tr:last-child td {
|
|
232
|
+
border-bottom: none;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
tr.passed {
|
|
236
|
+
background: var(--success-bg);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
tr.failed {
|
|
240
|
+
background: var(--error-bg);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.status-passed {
|
|
244
|
+
color: var(--success-color);
|
|
245
|
+
font-weight: 600;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.status-failed {
|
|
249
|
+
color: var(--error-color);
|
|
250
|
+
font-weight: 600;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.error-details {
|
|
254
|
+
background: rgba(0,0,0,0.3);
|
|
255
|
+
padding: 8px 12px;
|
|
256
|
+
border-radius: 6px;
|
|
257
|
+
font-size: 12px;
|
|
258
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
259
|
+
color: var(--error-color);
|
|
260
|
+
margin-top: 8px;
|
|
261
|
+
max-width: 400px;
|
|
262
|
+
overflow: auto;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
footer {
|
|
266
|
+
margin-top: 24px;
|
|
267
|
+
text-align: center;
|
|
268
|
+
color: var(--text-muted);
|
|
269
|
+
font-size: 12px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.progress-bar {
|
|
273
|
+
height: 8px;
|
|
274
|
+
background: var(--border-color);
|
|
275
|
+
border-radius: 4px;
|
|
276
|
+
overflow: hidden;
|
|
277
|
+
margin-top: 10px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.progress-fill {
|
|
281
|
+
height: 100%;
|
|
282
|
+
background: linear-gradient(90deg, var(--success-color), #2ea043);
|
|
283
|
+
transition: width 0.3s ease;
|
|
284
|
+
}
|
|
285
|
+
</style>
|
|
286
|
+
</head>
|
|
287
|
+
<body>
|
|
288
|
+
<div class="container">
|
|
289
|
+
<header>
|
|
290
|
+
<h1>🧪 MCP Maestro Test Report</h1>
|
|
291
|
+
<div class="meta">
|
|
292
|
+
<p><strong>Report ID:</strong> ${reportId}</p>
|
|
293
|
+
<p><strong>Generated:</strong> ${timestamp}</p>
|
|
294
|
+
<p><strong>Prompt File:</strong> ${escapeHtml(metadata.promptFile || 'N/A')}</p>
|
|
295
|
+
<p><strong>App ID:</strong> ${escapeHtml(metadata.appId || 'N/A')}</p>
|
|
296
|
+
</div>
|
|
297
|
+
</header>
|
|
298
|
+
|
|
299
|
+
<div class="summary-cards">
|
|
300
|
+
<div class="card">
|
|
301
|
+
<div class="card-value">${total}</div>
|
|
302
|
+
<div class="card-label">Total Tests</div>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="card card-passed">
|
|
305
|
+
<div class="card-value">${passed}</div>
|
|
306
|
+
<div class="card-label">Passed</div>
|
|
307
|
+
</div>
|
|
308
|
+
<div class="card card-failed">
|
|
309
|
+
<div class="card-value">${failed}</div>
|
|
310
|
+
<div class="card-label">Failed</div>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="card card-rate">
|
|
313
|
+
<div class="card-value">${passRate}%</div>
|
|
314
|
+
<div class="card-label">Pass Rate</div>
|
|
315
|
+
<div class="progress-bar">
|
|
316
|
+
<div class="progress-fill" style="width: ${passRate}%"></div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div class="results-table">
|
|
322
|
+
<table>
|
|
323
|
+
<thead>
|
|
324
|
+
<tr>
|
|
325
|
+
<th>#</th>
|
|
326
|
+
<th>Test Name</th>
|
|
327
|
+
<th>Status</th>
|
|
328
|
+
<th>Duration</th>
|
|
329
|
+
<th>Attempts</th>
|
|
330
|
+
<th>Details</th>
|
|
331
|
+
</tr>
|
|
332
|
+
</thead>
|
|
333
|
+
<tbody>
|
|
334
|
+
${testRows}
|
|
335
|
+
</tbody>
|
|
336
|
+
</table>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<footer>
|
|
340
|
+
<p>Generated by MCP Maestro Mobile AI</p>
|
|
341
|
+
<p>Report Location: ${REPORTS_DIR}</p>
|
|
342
|
+
</footer>
|
|
343
|
+
</div>
|
|
344
|
+
</body>
|
|
345
|
+
</html>`;
|
|
346
|
+
|
|
347
|
+
const htmlPath = path.join(REPORTS_DIR, `${reportId}.html`);
|
|
348
|
+
await fs.writeFile(htmlPath, html, 'utf8');
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
reportId,
|
|
352
|
+
htmlPath,
|
|
353
|
+
summary: {
|
|
354
|
+
total,
|
|
355
|
+
passed,
|
|
356
|
+
failed,
|
|
357
|
+
passRate,
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Generate both JSON and HTML reports
|
|
364
|
+
*/
|
|
365
|
+
export async function generateReport(results, metadata = {}) {
|
|
366
|
+
const jsonResult = await generateJsonReport(results, metadata);
|
|
367
|
+
const htmlResult = await generateHtmlReport(results, metadata);
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
reportId: jsonResult.reportId,
|
|
371
|
+
jsonPath: jsonResult.jsonPath,
|
|
372
|
+
htmlPath: htmlResult.htmlPath,
|
|
373
|
+
summary: htmlResult.summary,
|
|
374
|
+
report: jsonResult.report,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Get reports directory path
|
|
380
|
+
*/
|
|
381
|
+
export function getReportsDir() {
|
|
382
|
+
return REPORTS_DIR;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* List all generated reports
|
|
387
|
+
*/
|
|
388
|
+
export async function listReports() {
|
|
389
|
+
await ensureReportsDir();
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const files = await fs.readdir(REPORTS_DIR);
|
|
393
|
+
const htmlReports = files.filter(f => f.endsWith('.html'));
|
|
394
|
+
|
|
395
|
+
const reports = [];
|
|
396
|
+
for (const file of htmlReports) {
|
|
397
|
+
const reportId = file.replace('.html', '');
|
|
398
|
+
const jsonPath = path.join(REPORTS_DIR, `${reportId}.json`);
|
|
399
|
+
const htmlPath = path.join(REPORTS_DIR, file);
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const jsonContent = await fs.readFile(jsonPath, 'utf8');
|
|
403
|
+
const data = JSON.parse(jsonContent);
|
|
404
|
+
reports.push({
|
|
405
|
+
reportId,
|
|
406
|
+
generatedAt: data.generatedAt,
|
|
407
|
+
promptFile: data.metadata?.promptFile,
|
|
408
|
+
summary: data.summary,
|
|
409
|
+
htmlPath,
|
|
410
|
+
jsonPath,
|
|
411
|
+
});
|
|
412
|
+
} catch {
|
|
413
|
+
// JSON file might not exist
|
|
414
|
+
reports.push({
|
|
415
|
+
reportId,
|
|
416
|
+
htmlPath,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Sort by date (newest first)
|
|
422
|
+
reports.sort((a, b) => {
|
|
423
|
+
if (a.generatedAt && b.generatedAt) {
|
|
424
|
+
return new Date(b.generatedAt) - new Date(a.generatedAt);
|
|
425
|
+
}
|
|
426
|
+
return 0;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
return reports;
|
|
430
|
+
} catch {
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Escape HTML special characters
|
|
437
|
+
*/
|
|
438
|
+
function escapeHtml(text) {
|
|
439
|
+
if (!text) return '';
|
|
440
|
+
return String(text)
|
|
441
|
+
.replace(/&/g, '&')
|
|
442
|
+
.replace(/</g, '<')
|
|
443
|
+
.replace(/>/g, '>')
|
|
444
|
+
.replace(/"/g, '"')
|
|
445
|
+
.replace(/'/g, ''');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export default {
|
|
449
|
+
generateJsonReport,
|
|
450
|
+
generateHtmlReport,
|
|
451
|
+
generateReport,
|
|
452
|
+
getReportsDir,
|
|
453
|
+
listReports,
|
|
454
|
+
};
|
|
455
|
+
|