@bbearai/mcp-server 0.7.1 → 0.7.2
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/dist/index.js +277 -0
- package/package.json +1 -1
- package/src/index.ts +307 -0
package/dist/index.js
CHANGED
|
@@ -1034,6 +1034,33 @@ const tools = [
|
|
|
1034
1034
|
required: ['name'],
|
|
1035
1035
|
},
|
|
1036
1036
|
},
|
|
1037
|
+
{
|
|
1038
|
+
name: 'update_test_run',
|
|
1039
|
+
description: 'Update a test run\'s status or metadata. Use to activate, pause, complete, or archive a test run.',
|
|
1040
|
+
inputSchema: {
|
|
1041
|
+
type: 'object',
|
|
1042
|
+
properties: {
|
|
1043
|
+
test_run_id: {
|
|
1044
|
+
type: 'string',
|
|
1045
|
+
description: 'UUID of the test run to update (required)',
|
|
1046
|
+
},
|
|
1047
|
+
status: {
|
|
1048
|
+
type: 'string',
|
|
1049
|
+
enum: ['draft', 'active', 'paused', 'completed', 'archived'],
|
|
1050
|
+
description: 'New status for the test run',
|
|
1051
|
+
},
|
|
1052
|
+
name: {
|
|
1053
|
+
type: 'string',
|
|
1054
|
+
description: 'Updated name for the test run',
|
|
1055
|
+
},
|
|
1056
|
+
description: {
|
|
1057
|
+
type: 'string',
|
|
1058
|
+
description: 'Updated description for the test run',
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
required: ['test_run_id'],
|
|
1062
|
+
},
|
|
1063
|
+
},
|
|
1037
1064
|
{
|
|
1038
1065
|
name: 'list_test_assignments',
|
|
1039
1066
|
description: 'List test assignments showing which tests are assigned to which testers, with status tracking. Joins test case and tester info.',
|
|
@@ -1280,6 +1307,24 @@ const tools = [
|
|
|
1280
1307
|
},
|
|
1281
1308
|
},
|
|
1282
1309
|
},
|
|
1310
|
+
{
|
|
1311
|
+
name: 'get_tester_report',
|
|
1312
|
+
description: 'Get a comprehensive analytics report for a single tester — time spent, pass/fail ratios, comment quality, activity streaks, and recent tests. All computed server-side in one query.',
|
|
1313
|
+
inputSchema: {
|
|
1314
|
+
type: 'object',
|
|
1315
|
+
properties: {
|
|
1316
|
+
tester_id: {
|
|
1317
|
+
type: 'string',
|
|
1318
|
+
description: 'UUID of the tester (required)',
|
|
1319
|
+
},
|
|
1320
|
+
days: {
|
|
1321
|
+
type: 'number',
|
|
1322
|
+
description: 'Number of days to look back (default: 30, 0 = all time, max: 365)',
|
|
1323
|
+
},
|
|
1324
|
+
},
|
|
1325
|
+
required: ['tester_id'],
|
|
1326
|
+
},
|
|
1327
|
+
},
|
|
1283
1328
|
// === PROJECT MANAGEMENT TOOLS ===
|
|
1284
1329
|
{
|
|
1285
1330
|
name: 'list_projects',
|
|
@@ -1451,7 +1496,159 @@ const tools = [
|
|
|
1451
1496
|
},
|
|
1452
1497
|
},
|
|
1453
1498
|
},
|
|
1499
|
+
// === EMAIL TESTING TOOLS ===
|
|
1500
|
+
{
|
|
1501
|
+
name: 'list_captured_emails',
|
|
1502
|
+
description: 'List captured emails for the project. Use to review intercepted email sends during QA testing. Returns subject, recipients, template, delivery status.',
|
|
1503
|
+
inputSchema: {
|
|
1504
|
+
type: 'object',
|
|
1505
|
+
properties: {
|
|
1506
|
+
template_id: {
|
|
1507
|
+
type: 'string',
|
|
1508
|
+
description: 'Filter by template ID',
|
|
1509
|
+
},
|
|
1510
|
+
delivery_status: {
|
|
1511
|
+
type: 'string',
|
|
1512
|
+
enum: ['pending', 'sent', 'delivered', 'bounced', 'dropped', 'deferred'],
|
|
1513
|
+
description: 'Filter by delivery status',
|
|
1514
|
+
},
|
|
1515
|
+
capture_mode: {
|
|
1516
|
+
type: 'string',
|
|
1517
|
+
enum: ['capture', 'intercept'],
|
|
1518
|
+
description: 'Filter by capture mode',
|
|
1519
|
+
},
|
|
1520
|
+
search: {
|
|
1521
|
+
type: 'string',
|
|
1522
|
+
description: 'Search subject line (case-insensitive)',
|
|
1523
|
+
},
|
|
1524
|
+
limit: {
|
|
1525
|
+
type: 'number',
|
|
1526
|
+
description: 'Max results (default: 20, max: 100)',
|
|
1527
|
+
},
|
|
1528
|
+
offset: {
|
|
1529
|
+
type: 'number',
|
|
1530
|
+
description: 'Offset for pagination (default: 0)',
|
|
1531
|
+
},
|
|
1532
|
+
},
|
|
1533
|
+
},
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
name: 'get_email_preview',
|
|
1537
|
+
description: 'Get full email content including HTML body, metadata, delivery tracking, and extracted links. Use to inspect a specific captured email.',
|
|
1538
|
+
inputSchema: {
|
|
1539
|
+
type: 'object',
|
|
1540
|
+
properties: {
|
|
1541
|
+
email_id: {
|
|
1542
|
+
type: 'string',
|
|
1543
|
+
description: 'UUID of the email capture to preview',
|
|
1544
|
+
},
|
|
1545
|
+
},
|
|
1546
|
+
required: ['email_id'],
|
|
1547
|
+
},
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
name: 'get_email_coverage',
|
|
1551
|
+
description: 'Get email template coverage — shows which templates have been captured/tested and how often. Use to identify untested email templates.',
|
|
1552
|
+
inputSchema: {
|
|
1553
|
+
type: 'object',
|
|
1554
|
+
properties: {
|
|
1555
|
+
days: {
|
|
1556
|
+
type: 'number',
|
|
1557
|
+
description: 'Look-back period in days (default: 30)',
|
|
1558
|
+
},
|
|
1559
|
+
},
|
|
1560
|
+
},
|
|
1561
|
+
},
|
|
1454
1562
|
];
|
|
1563
|
+
// === EMAIL TESTING ===
|
|
1564
|
+
async function listCapturedEmails(args) {
|
|
1565
|
+
const projectId = requireProject();
|
|
1566
|
+
const limit = Math.min(args.limit || 20, 100);
|
|
1567
|
+
const offset = args.offset || 0;
|
|
1568
|
+
let query = supabase
|
|
1569
|
+
.from('email_captures')
|
|
1570
|
+
.select('id, to_addresses, from_address, subject, template_id, capture_mode, was_delivered, delivery_status, created_at, tester_id, metadata', { count: 'exact' })
|
|
1571
|
+
.eq('project_id', projectId)
|
|
1572
|
+
.order('created_at', { ascending: false })
|
|
1573
|
+
.range(offset, offset + limit - 1);
|
|
1574
|
+
if (args.template_id)
|
|
1575
|
+
query = query.eq('template_id', args.template_id);
|
|
1576
|
+
if (args.delivery_status)
|
|
1577
|
+
query = query.eq('delivery_status', args.delivery_status);
|
|
1578
|
+
if (args.capture_mode)
|
|
1579
|
+
query = query.eq('capture_mode', args.capture_mode);
|
|
1580
|
+
if (args.search) {
|
|
1581
|
+
const escaped = args.search.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
1582
|
+
query = query.ilike('subject', `%${escaped}%`);
|
|
1583
|
+
}
|
|
1584
|
+
const { data, error, count } = await query;
|
|
1585
|
+
if (error)
|
|
1586
|
+
return { error: error.message };
|
|
1587
|
+
return {
|
|
1588
|
+
emails: data || [],
|
|
1589
|
+
total: count ?? data?.length ?? 0,
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
async function getEmailPreview(args) {
|
|
1593
|
+
const projectId = requireProject();
|
|
1594
|
+
if (!args.email_id || !UUID_REGEX.test(args.email_id)) {
|
|
1595
|
+
return { error: 'email_id must be a valid UUID' };
|
|
1596
|
+
}
|
|
1597
|
+
const { data, error } = await supabase
|
|
1598
|
+
.from('email_captures')
|
|
1599
|
+
.select('id, to_addresses, from_address, subject, html_content, text_content, template_id, metadata, capture_mode, was_delivered, delivery_status, opened_at, clicked_at, bounced_at, delivery_error, created_at')
|
|
1600
|
+
.eq('id', args.email_id)
|
|
1601
|
+
.eq('project_id', projectId)
|
|
1602
|
+
.single();
|
|
1603
|
+
if (error || !data)
|
|
1604
|
+
return { error: error?.message || 'Email not found' };
|
|
1605
|
+
// Extract links from HTML
|
|
1606
|
+
const links = [];
|
|
1607
|
+
if (data.html_content) {
|
|
1608
|
+
const hrefRegex = /href="([^"]+)"/gi;
|
|
1609
|
+
let match;
|
|
1610
|
+
while ((match = hrefRegex.exec(data.html_content)) !== null) {
|
|
1611
|
+
if (match[1] && !match[1].startsWith('mailto:')) {
|
|
1612
|
+
links.push(match[1]);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return { email: data, links };
|
|
1617
|
+
}
|
|
1618
|
+
async function getEmailCoverage(args) {
|
|
1619
|
+
const projectId = requireProject();
|
|
1620
|
+
const days = args.days || 30;
|
|
1621
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
1622
|
+
const { data, error } = await supabase
|
|
1623
|
+
.from('email_captures')
|
|
1624
|
+
.select('template_id, created_at')
|
|
1625
|
+
.eq('project_id', projectId)
|
|
1626
|
+
.gte('created_at', since)
|
|
1627
|
+
.not('template_id', 'is', null)
|
|
1628
|
+
.order('created_at', { ascending: false });
|
|
1629
|
+
if (error)
|
|
1630
|
+
return { error: error.message };
|
|
1631
|
+
// Group by template_id
|
|
1632
|
+
const templates = new Map();
|
|
1633
|
+
for (const row of data || []) {
|
|
1634
|
+
const existing = templates.get(row.template_id);
|
|
1635
|
+
if (existing) {
|
|
1636
|
+
existing.count++;
|
|
1637
|
+
}
|
|
1638
|
+
else {
|
|
1639
|
+
templates.set(row.template_id, { count: 1, lastCaptured: row.created_at });
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return {
|
|
1643
|
+
templates: Array.from(templates.entries()).map(([id, stats]) => ({
|
|
1644
|
+
templateId: id,
|
|
1645
|
+
captureCount: stats.count,
|
|
1646
|
+
lastCaptured: stats.lastCaptured,
|
|
1647
|
+
})),
|
|
1648
|
+
totalCaptures: data?.length || 0,
|
|
1649
|
+
periodDays: days,
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1455
1652
|
// === TEST EXECUTION INTELLIGENCE ===
|
|
1456
1653
|
async function getTestImpact(args) {
|
|
1457
1654
|
const projectId = requireProject();
|
|
@@ -5347,6 +5544,54 @@ async function createTestRun(args) {
|
|
|
5347
5544
|
message: `Test run "${data.name}" created. Use assign_tests to add test assignments.`,
|
|
5348
5545
|
};
|
|
5349
5546
|
}
|
|
5547
|
+
async function updateTestRun(args) {
|
|
5548
|
+
if (!isValidUUID(args.test_run_id)) {
|
|
5549
|
+
return { error: 'Invalid test_run_id format' };
|
|
5550
|
+
}
|
|
5551
|
+
const validStatuses = ['draft', 'active', 'paused', 'completed', 'archived'];
|
|
5552
|
+
if (args.status && !validStatuses.includes(args.status)) {
|
|
5553
|
+
return { error: `Invalid status. Must be one of: ${validStatuses.join(', ')}` };
|
|
5554
|
+
}
|
|
5555
|
+
const updates = {};
|
|
5556
|
+
if (args.status)
|
|
5557
|
+
updates.status = args.status;
|
|
5558
|
+
if (args.name)
|
|
5559
|
+
updates.name = args.name.trim();
|
|
5560
|
+
if (args.description !== undefined)
|
|
5561
|
+
updates.description = args.description?.trim() || null;
|
|
5562
|
+
if (args.status === 'active' && !updates.started_at) {
|
|
5563
|
+
updates.started_at = new Date().toISOString();
|
|
5564
|
+
}
|
|
5565
|
+
if (args.status === 'completed') {
|
|
5566
|
+
updates.completed_at = new Date().toISOString();
|
|
5567
|
+
}
|
|
5568
|
+
if (Object.keys(updates).length === 0) {
|
|
5569
|
+
return { error: 'No updates provided. Specify at least one of: status, name, description.' };
|
|
5570
|
+
}
|
|
5571
|
+
const { data, error } = await supabase
|
|
5572
|
+
.from('test_runs')
|
|
5573
|
+
.update(updates)
|
|
5574
|
+
.eq('id', args.test_run_id)
|
|
5575
|
+
.eq('project_id', currentProjectId)
|
|
5576
|
+
.select('id, name, description, status, total_tests, started_at, completed_at')
|
|
5577
|
+
.single();
|
|
5578
|
+
if (error) {
|
|
5579
|
+
return { error: error.message };
|
|
5580
|
+
}
|
|
5581
|
+
return {
|
|
5582
|
+
success: true,
|
|
5583
|
+
testRun: {
|
|
5584
|
+
id: data.id,
|
|
5585
|
+
name: data.name,
|
|
5586
|
+
description: data.description,
|
|
5587
|
+
status: data.status,
|
|
5588
|
+
totalTests: data.total_tests,
|
|
5589
|
+
startedAt: data.started_at,
|
|
5590
|
+
completedAt: data.completed_at,
|
|
5591
|
+
},
|
|
5592
|
+
message: `Test run "${data.name}" updated to status: ${data.status}.`,
|
|
5593
|
+
};
|
|
5594
|
+
}
|
|
5350
5595
|
async function listTestAssignments(args) {
|
|
5351
5596
|
const limit = Math.min(args.limit || 50, 200);
|
|
5352
5597
|
if (args.tester_id && !isValidUUID(args.tester_id)) {
|
|
@@ -6124,6 +6369,22 @@ async function getTestingVelocity(args) {
|
|
|
6124
6369
|
daily: dailyArray,
|
|
6125
6370
|
};
|
|
6126
6371
|
}
|
|
6372
|
+
async function getTesterReport(args) {
|
|
6373
|
+
const projectId = requireProject();
|
|
6374
|
+
if (!isValidUUID(args.tester_id))
|
|
6375
|
+
return { error: 'Invalid tester_id format' };
|
|
6376
|
+
const days = args.days !== undefined ? Math.min(Math.max(args.days, 0), 365) : 30;
|
|
6377
|
+
const { data, error } = await supabase.rpc('get_tester_report', {
|
|
6378
|
+
p_project_id: projectId,
|
|
6379
|
+
p_tester_id: args.tester_id,
|
|
6380
|
+
p_days: days || null,
|
|
6381
|
+
});
|
|
6382
|
+
if (error)
|
|
6383
|
+
return { error: error.message };
|
|
6384
|
+
if (data?.error)
|
|
6385
|
+
return { error: data.error };
|
|
6386
|
+
return data;
|
|
6387
|
+
}
|
|
6127
6388
|
// Main server setup
|
|
6128
6389
|
async function main() {
|
|
6129
6390
|
initSupabase();
|
|
@@ -6283,6 +6544,9 @@ async function main() {
|
|
|
6283
6544
|
case 'create_test_run':
|
|
6284
6545
|
result = await createTestRun(args);
|
|
6285
6546
|
break;
|
|
6547
|
+
case 'update_test_run':
|
|
6548
|
+
result = await updateTestRun(args);
|
|
6549
|
+
break;
|
|
6286
6550
|
case 'list_test_assignments':
|
|
6287
6551
|
result = await listTestAssignments(args);
|
|
6288
6552
|
break;
|
|
@@ -6317,6 +6581,9 @@ async function main() {
|
|
|
6317
6581
|
case 'get_testing_velocity':
|
|
6318
6582
|
result = await getTestingVelocity(args);
|
|
6319
6583
|
break;
|
|
6584
|
+
case 'get_tester_report':
|
|
6585
|
+
result = await getTesterReport(args);
|
|
6586
|
+
break;
|
|
6320
6587
|
// === PROJECT MANAGEMENT ===
|
|
6321
6588
|
case 'list_projects':
|
|
6322
6589
|
result = await listProjects();
|
|
@@ -6353,6 +6620,16 @@ async function main() {
|
|
|
6353
6620
|
case 'generate_tests_from_errors':
|
|
6354
6621
|
result = await generateTestsFromErrors(args);
|
|
6355
6622
|
break;
|
|
6623
|
+
// === EMAIL TESTING TOOLS ===
|
|
6624
|
+
case 'list_captured_emails':
|
|
6625
|
+
result = await listCapturedEmails(args);
|
|
6626
|
+
break;
|
|
6627
|
+
case 'get_email_preview':
|
|
6628
|
+
result = await getEmailPreview(args);
|
|
6629
|
+
break;
|
|
6630
|
+
case 'get_email_coverage':
|
|
6631
|
+
result = await getEmailCoverage(args);
|
|
6632
|
+
break;
|
|
6356
6633
|
default:
|
|
6357
6634
|
return {
|
|
6358
6635
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1051,6 +1051,33 @@ const tools = [
|
|
|
1051
1051
|
required: ['name'],
|
|
1052
1052
|
},
|
|
1053
1053
|
},
|
|
1054
|
+
{
|
|
1055
|
+
name: 'update_test_run',
|
|
1056
|
+
description: 'Update a test run\'s status or metadata. Use to activate, pause, complete, or archive a test run.',
|
|
1057
|
+
inputSchema: {
|
|
1058
|
+
type: 'object' as const,
|
|
1059
|
+
properties: {
|
|
1060
|
+
test_run_id: {
|
|
1061
|
+
type: 'string',
|
|
1062
|
+
description: 'UUID of the test run to update (required)',
|
|
1063
|
+
},
|
|
1064
|
+
status: {
|
|
1065
|
+
type: 'string',
|
|
1066
|
+
enum: ['draft', 'active', 'paused', 'completed', 'archived'],
|
|
1067
|
+
description: 'New status for the test run',
|
|
1068
|
+
},
|
|
1069
|
+
name: {
|
|
1070
|
+
type: 'string',
|
|
1071
|
+
description: 'Updated name for the test run',
|
|
1072
|
+
},
|
|
1073
|
+
description: {
|
|
1074
|
+
type: 'string',
|
|
1075
|
+
description: 'Updated description for the test run',
|
|
1076
|
+
},
|
|
1077
|
+
},
|
|
1078
|
+
required: ['test_run_id'],
|
|
1079
|
+
},
|
|
1080
|
+
},
|
|
1054
1081
|
{
|
|
1055
1082
|
name: 'list_test_assignments',
|
|
1056
1083
|
description: 'List test assignments showing which tests are assigned to which testers, with status tracking. Joins test case and tester info.',
|
|
@@ -1297,6 +1324,24 @@ const tools = [
|
|
|
1297
1324
|
},
|
|
1298
1325
|
},
|
|
1299
1326
|
},
|
|
1327
|
+
{
|
|
1328
|
+
name: 'get_tester_report',
|
|
1329
|
+
description: 'Get a comprehensive analytics report for a single tester — time spent, pass/fail ratios, comment quality, activity streaks, and recent tests. All computed server-side in one query.',
|
|
1330
|
+
inputSchema: {
|
|
1331
|
+
type: 'object' as const,
|
|
1332
|
+
properties: {
|
|
1333
|
+
tester_id: {
|
|
1334
|
+
type: 'string',
|
|
1335
|
+
description: 'UUID of the tester (required)',
|
|
1336
|
+
},
|
|
1337
|
+
days: {
|
|
1338
|
+
type: 'number',
|
|
1339
|
+
description: 'Number of days to look back (default: 30, 0 = all time, max: 365)',
|
|
1340
|
+
},
|
|
1341
|
+
},
|
|
1342
|
+
required: ['tester_id'],
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1300
1345
|
// === PROJECT MANAGEMENT TOOLS ===
|
|
1301
1346
|
{
|
|
1302
1347
|
name: 'list_projects',
|
|
@@ -1468,8 +1513,177 @@ const tools = [
|
|
|
1468
1513
|
},
|
|
1469
1514
|
},
|
|
1470
1515
|
},
|
|
1516
|
+
// === EMAIL TESTING TOOLS ===
|
|
1517
|
+
{
|
|
1518
|
+
name: 'list_captured_emails',
|
|
1519
|
+
description: 'List captured emails for the project. Use to review intercepted email sends during QA testing. Returns subject, recipients, template, delivery status.',
|
|
1520
|
+
inputSchema: {
|
|
1521
|
+
type: 'object' as const,
|
|
1522
|
+
properties: {
|
|
1523
|
+
template_id: {
|
|
1524
|
+
type: 'string',
|
|
1525
|
+
description: 'Filter by template ID',
|
|
1526
|
+
},
|
|
1527
|
+
delivery_status: {
|
|
1528
|
+
type: 'string',
|
|
1529
|
+
enum: ['pending', 'sent', 'delivered', 'bounced', 'dropped', 'deferred'],
|
|
1530
|
+
description: 'Filter by delivery status',
|
|
1531
|
+
},
|
|
1532
|
+
capture_mode: {
|
|
1533
|
+
type: 'string',
|
|
1534
|
+
enum: ['capture', 'intercept'],
|
|
1535
|
+
description: 'Filter by capture mode',
|
|
1536
|
+
},
|
|
1537
|
+
search: {
|
|
1538
|
+
type: 'string',
|
|
1539
|
+
description: 'Search subject line (case-insensitive)',
|
|
1540
|
+
},
|
|
1541
|
+
limit: {
|
|
1542
|
+
type: 'number',
|
|
1543
|
+
description: 'Max results (default: 20, max: 100)',
|
|
1544
|
+
},
|
|
1545
|
+
offset: {
|
|
1546
|
+
type: 'number',
|
|
1547
|
+
description: 'Offset for pagination (default: 0)',
|
|
1548
|
+
},
|
|
1549
|
+
},
|
|
1550
|
+
},
|
|
1551
|
+
},
|
|
1552
|
+
{
|
|
1553
|
+
name: 'get_email_preview',
|
|
1554
|
+
description: 'Get full email content including HTML body, metadata, delivery tracking, and extracted links. Use to inspect a specific captured email.',
|
|
1555
|
+
inputSchema: {
|
|
1556
|
+
type: 'object' as const,
|
|
1557
|
+
properties: {
|
|
1558
|
+
email_id: {
|
|
1559
|
+
type: 'string',
|
|
1560
|
+
description: 'UUID of the email capture to preview',
|
|
1561
|
+
},
|
|
1562
|
+
},
|
|
1563
|
+
required: ['email_id'],
|
|
1564
|
+
},
|
|
1565
|
+
},
|
|
1566
|
+
{
|
|
1567
|
+
name: 'get_email_coverage',
|
|
1568
|
+
description: 'Get email template coverage — shows which templates have been captured/tested and how often. Use to identify untested email templates.',
|
|
1569
|
+
inputSchema: {
|
|
1570
|
+
type: 'object' as const,
|
|
1571
|
+
properties: {
|
|
1572
|
+
days: {
|
|
1573
|
+
type: 'number',
|
|
1574
|
+
description: 'Look-back period in days (default: 30)',
|
|
1575
|
+
},
|
|
1576
|
+
},
|
|
1577
|
+
},
|
|
1578
|
+
},
|
|
1471
1579
|
];
|
|
1472
1580
|
|
|
1581
|
+
// === EMAIL TESTING ===
|
|
1582
|
+
|
|
1583
|
+
async function listCapturedEmails(args: {
|
|
1584
|
+
template_id?: string;
|
|
1585
|
+
delivery_status?: string;
|
|
1586
|
+
capture_mode?: string;
|
|
1587
|
+
search?: string;
|
|
1588
|
+
limit?: number;
|
|
1589
|
+
offset?: number;
|
|
1590
|
+
}) {
|
|
1591
|
+
const projectId = requireProject();
|
|
1592
|
+
const limit = Math.min(args.limit || 20, 100);
|
|
1593
|
+
const offset = args.offset || 0;
|
|
1594
|
+
|
|
1595
|
+
let query = supabase
|
|
1596
|
+
.from('email_captures')
|
|
1597
|
+
.select('id, to_addresses, from_address, subject, template_id, capture_mode, was_delivered, delivery_status, created_at, tester_id, metadata', { count: 'exact' })
|
|
1598
|
+
.eq('project_id', projectId)
|
|
1599
|
+
.order('created_at', { ascending: false })
|
|
1600
|
+
.range(offset, offset + limit - 1);
|
|
1601
|
+
|
|
1602
|
+
if (args.template_id) query = query.eq('template_id', args.template_id);
|
|
1603
|
+
if (args.delivery_status) query = query.eq('delivery_status', args.delivery_status);
|
|
1604
|
+
if (args.capture_mode) query = query.eq('capture_mode', args.capture_mode);
|
|
1605
|
+
if (args.search) {
|
|
1606
|
+
const escaped = args.search.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
1607
|
+
query = query.ilike('subject', `%${escaped}%`);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
const { data, error, count } = await query;
|
|
1611
|
+
if (error) return { error: error.message };
|
|
1612
|
+
|
|
1613
|
+
return {
|
|
1614
|
+
emails: data || [],
|
|
1615
|
+
total: count ?? data?.length ?? 0,
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
async function getEmailPreview(args: { email_id: string }) {
|
|
1620
|
+
const projectId = requireProject();
|
|
1621
|
+
|
|
1622
|
+
if (!args.email_id || !UUID_REGEX.test(args.email_id)) {
|
|
1623
|
+
return { error: 'email_id must be a valid UUID' };
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
const { data, error } = await supabase
|
|
1627
|
+
.from('email_captures')
|
|
1628
|
+
.select('id, to_addresses, from_address, subject, html_content, text_content, template_id, metadata, capture_mode, was_delivered, delivery_status, opened_at, clicked_at, bounced_at, delivery_error, created_at')
|
|
1629
|
+
.eq('id', args.email_id)
|
|
1630
|
+
.eq('project_id', projectId)
|
|
1631
|
+
.single();
|
|
1632
|
+
|
|
1633
|
+
if (error || !data) return { error: error?.message || 'Email not found' };
|
|
1634
|
+
|
|
1635
|
+
// Extract links from HTML
|
|
1636
|
+
const links: string[] = [];
|
|
1637
|
+
if (data.html_content) {
|
|
1638
|
+
const hrefRegex = /href="([^"]+)"/gi;
|
|
1639
|
+
let match;
|
|
1640
|
+
while ((match = hrefRegex.exec(data.html_content)) !== null) {
|
|
1641
|
+
if (match[1] && !match[1].startsWith('mailto:')) {
|
|
1642
|
+
links.push(match[1]);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
return { email: data, links };
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
async function getEmailCoverage(args: { days?: number }) {
|
|
1651
|
+
const projectId = requireProject();
|
|
1652
|
+
const days = args.days || 30;
|
|
1653
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
1654
|
+
|
|
1655
|
+
const { data, error } = await supabase
|
|
1656
|
+
.from('email_captures')
|
|
1657
|
+
.select('template_id, created_at')
|
|
1658
|
+
.eq('project_id', projectId)
|
|
1659
|
+
.gte('created_at', since)
|
|
1660
|
+
.not('template_id', 'is', null)
|
|
1661
|
+
.order('created_at', { ascending: false });
|
|
1662
|
+
|
|
1663
|
+
if (error) return { error: error.message };
|
|
1664
|
+
|
|
1665
|
+
// Group by template_id
|
|
1666
|
+
const templates = new Map<string, { count: number; lastCaptured: string }>();
|
|
1667
|
+
for (const row of data || []) {
|
|
1668
|
+
const existing = templates.get(row.template_id!);
|
|
1669
|
+
if (existing) {
|
|
1670
|
+
existing.count++;
|
|
1671
|
+
} else {
|
|
1672
|
+
templates.set(row.template_id!, { count: 1, lastCaptured: row.created_at });
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
return {
|
|
1677
|
+
templates: Array.from(templates.entries()).map(([id, stats]) => ({
|
|
1678
|
+
templateId: id,
|
|
1679
|
+
captureCount: stats.count,
|
|
1680
|
+
lastCaptured: stats.lastCaptured,
|
|
1681
|
+
})),
|
|
1682
|
+
totalCaptures: data?.length || 0,
|
|
1683
|
+
periodDays: days,
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1473
1687
|
// === TEST EXECUTION INTELLIGENCE ===
|
|
1474
1688
|
|
|
1475
1689
|
async function getTestImpact(args: { changed_files: string[] }) {
|
|
@@ -6047,6 +6261,64 @@ async function createTestRun(args: {
|
|
|
6047
6261
|
};
|
|
6048
6262
|
}
|
|
6049
6263
|
|
|
6264
|
+
async function updateTestRun(args: {
|
|
6265
|
+
test_run_id: string;
|
|
6266
|
+
status?: string;
|
|
6267
|
+
name?: string;
|
|
6268
|
+
description?: string;
|
|
6269
|
+
}) {
|
|
6270
|
+
if (!isValidUUID(args.test_run_id)) {
|
|
6271
|
+
return { error: 'Invalid test_run_id format' };
|
|
6272
|
+
}
|
|
6273
|
+
|
|
6274
|
+
const validStatuses = ['draft', 'active', 'paused', 'completed', 'archived'];
|
|
6275
|
+
if (args.status && !validStatuses.includes(args.status)) {
|
|
6276
|
+
return { error: `Invalid status. Must be one of: ${validStatuses.join(', ')}` };
|
|
6277
|
+
}
|
|
6278
|
+
|
|
6279
|
+
const updates: Record<string, any> = {};
|
|
6280
|
+
if (args.status) updates.status = args.status;
|
|
6281
|
+
if (args.name) updates.name = args.name.trim();
|
|
6282
|
+
if (args.description !== undefined) updates.description = args.description?.trim() || null;
|
|
6283
|
+
|
|
6284
|
+
if (args.status === 'active' && !updates.started_at) {
|
|
6285
|
+
updates.started_at = new Date().toISOString();
|
|
6286
|
+
}
|
|
6287
|
+
if (args.status === 'completed') {
|
|
6288
|
+
updates.completed_at = new Date().toISOString();
|
|
6289
|
+
}
|
|
6290
|
+
|
|
6291
|
+
if (Object.keys(updates).length === 0) {
|
|
6292
|
+
return { error: 'No updates provided. Specify at least one of: status, name, description.' };
|
|
6293
|
+
}
|
|
6294
|
+
|
|
6295
|
+
const { data, error } = await supabase
|
|
6296
|
+
.from('test_runs')
|
|
6297
|
+
.update(updates)
|
|
6298
|
+
.eq('id', args.test_run_id)
|
|
6299
|
+
.eq('project_id', currentProjectId)
|
|
6300
|
+
.select('id, name, description, status, total_tests, started_at, completed_at')
|
|
6301
|
+
.single();
|
|
6302
|
+
|
|
6303
|
+
if (error) {
|
|
6304
|
+
return { error: error.message };
|
|
6305
|
+
}
|
|
6306
|
+
|
|
6307
|
+
return {
|
|
6308
|
+
success: true,
|
|
6309
|
+
testRun: {
|
|
6310
|
+
id: data.id,
|
|
6311
|
+
name: data.name,
|
|
6312
|
+
description: data.description,
|
|
6313
|
+
status: data.status,
|
|
6314
|
+
totalTests: data.total_tests,
|
|
6315
|
+
startedAt: data.started_at,
|
|
6316
|
+
completedAt: data.completed_at,
|
|
6317
|
+
},
|
|
6318
|
+
message: `Test run "${data.name}" updated to status: ${data.status}.`,
|
|
6319
|
+
};
|
|
6320
|
+
}
|
|
6321
|
+
|
|
6050
6322
|
async function listTestAssignments(args: {
|
|
6051
6323
|
tester_id?: string;
|
|
6052
6324
|
test_run_id?: string;
|
|
@@ -6952,6 +7224,25 @@ async function getTestingVelocity(args: {
|
|
|
6952
7224
|
};
|
|
6953
7225
|
}
|
|
6954
7226
|
|
|
7227
|
+
async function getTesterReport(args: {
|
|
7228
|
+
tester_id: string;
|
|
7229
|
+
days?: number;
|
|
7230
|
+
}) {
|
|
7231
|
+
const projectId = requireProject();
|
|
7232
|
+
if (!isValidUUID(args.tester_id)) return { error: 'Invalid tester_id format' };
|
|
7233
|
+
const days = args.days !== undefined ? Math.min(Math.max(args.days, 0), 365) : 30;
|
|
7234
|
+
|
|
7235
|
+
const { data, error } = await supabase.rpc('get_tester_report', {
|
|
7236
|
+
p_project_id: projectId,
|
|
7237
|
+
p_tester_id: args.tester_id,
|
|
7238
|
+
p_days: days || null,
|
|
7239
|
+
});
|
|
7240
|
+
|
|
7241
|
+
if (error) return { error: error.message };
|
|
7242
|
+
if (data?.error) return { error: data.error };
|
|
7243
|
+
return data;
|
|
7244
|
+
}
|
|
7245
|
+
|
|
6955
7246
|
// Main server setup
|
|
6956
7247
|
async function main() {
|
|
6957
7248
|
initSupabase();
|
|
@@ -7120,6 +7411,9 @@ async function main() {
|
|
|
7120
7411
|
case 'create_test_run':
|
|
7121
7412
|
result = await createTestRun(args as any);
|
|
7122
7413
|
break;
|
|
7414
|
+
case 'update_test_run':
|
|
7415
|
+
result = await updateTestRun(args as any);
|
|
7416
|
+
break;
|
|
7123
7417
|
case 'list_test_assignments':
|
|
7124
7418
|
result = await listTestAssignments(args as any);
|
|
7125
7419
|
break;
|
|
@@ -7154,6 +7448,9 @@ async function main() {
|
|
|
7154
7448
|
case 'get_testing_velocity':
|
|
7155
7449
|
result = await getTestingVelocity(args as any);
|
|
7156
7450
|
break;
|
|
7451
|
+
case 'get_tester_report':
|
|
7452
|
+
result = await getTesterReport(args as any);
|
|
7453
|
+
break;
|
|
7157
7454
|
// === PROJECT MANAGEMENT ===
|
|
7158
7455
|
case 'list_projects':
|
|
7159
7456
|
result = await listProjects();
|
|
@@ -7190,6 +7487,16 @@ async function main() {
|
|
|
7190
7487
|
case 'generate_tests_from_errors':
|
|
7191
7488
|
result = await generateTestsFromErrors(args as any);
|
|
7192
7489
|
break;
|
|
7490
|
+
// === EMAIL TESTING TOOLS ===
|
|
7491
|
+
case 'list_captured_emails':
|
|
7492
|
+
result = await listCapturedEmails(args as any);
|
|
7493
|
+
break;
|
|
7494
|
+
case 'get_email_preview':
|
|
7495
|
+
result = await getEmailPreview(args as any);
|
|
7496
|
+
break;
|
|
7497
|
+
case 'get_email_coverage':
|
|
7498
|
+
result = await getEmailCoverage(args as any);
|
|
7499
|
+
break;
|
|
7193
7500
|
default:
|
|
7194
7501
|
return {
|
|
7195
7502
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|