@bbearai/mcp-server 0.7.0 → 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 +331 -0
- package/package.json +1 -1
- package/src/index.ts +365 -0
package/dist/index.js
CHANGED
|
@@ -193,6 +193,25 @@ const tools = [
|
|
|
193
193
|
required: ['report_id'],
|
|
194
194
|
},
|
|
195
195
|
},
|
|
196
|
+
{
|
|
197
|
+
name: 'resolve_conversations',
|
|
198
|
+
description: 'Mark one or more discussion threads as resolved (closed) or reopen them. Use this to close conversations after issues are addressed.',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
thread_ids: {
|
|
203
|
+
type: 'array',
|
|
204
|
+
items: { type: 'string' },
|
|
205
|
+
description: 'UUIDs of the discussion threads to resolve/reopen',
|
|
206
|
+
},
|
|
207
|
+
resolved: {
|
|
208
|
+
type: 'boolean',
|
|
209
|
+
description: 'true to mark as resolved (default), false to reopen',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
required: ['thread_ids'],
|
|
213
|
+
},
|
|
214
|
+
},
|
|
196
215
|
{
|
|
197
216
|
name: 'get_project_info',
|
|
198
217
|
description: 'Get project information including QA tracks, test case counts, and common bug patterns',
|
|
@@ -1015,6 +1034,33 @@ const tools = [
|
|
|
1015
1034
|
required: ['name'],
|
|
1016
1035
|
},
|
|
1017
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
|
+
},
|
|
1018
1064
|
{
|
|
1019
1065
|
name: 'list_test_assignments',
|
|
1020
1066
|
description: 'List test assignments showing which tests are assigned to which testers, with status tracking. Joins test case and tester info.',
|
|
@@ -1261,6 +1307,24 @@ const tools = [
|
|
|
1261
1307
|
},
|
|
1262
1308
|
},
|
|
1263
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
|
+
},
|
|
1264
1328
|
// === PROJECT MANAGEMENT TOOLS ===
|
|
1265
1329
|
{
|
|
1266
1330
|
name: 'list_projects',
|
|
@@ -1432,7 +1496,159 @@ const tools = [
|
|
|
1432
1496
|
},
|
|
1433
1497
|
},
|
|
1434
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
|
+
},
|
|
1435
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
|
+
}
|
|
1436
1652
|
// === TEST EXECUTION INTELLIGENCE ===
|
|
1437
1653
|
async function getTestImpact(args) {
|
|
1438
1654
|
const projectId = requireProject();
|
|
@@ -2155,6 +2371,38 @@ async function getReportComments(args) {
|
|
|
2155
2371
|
return { error: error.message };
|
|
2156
2372
|
return { comments: (messages || []).map(m => ({ id: m.id, sender_type: m.sender_type, content: m.content, created_at: m.created_at, attachments: m.attachments })), total: (messages || []).length };
|
|
2157
2373
|
}
|
|
2374
|
+
async function resolveConversations(args) {
|
|
2375
|
+
if (!args.thread_ids || args.thread_ids.length === 0) {
|
|
2376
|
+
return { error: 'At least one thread_id is required' };
|
|
2377
|
+
}
|
|
2378
|
+
if (args.thread_ids.length > 50) {
|
|
2379
|
+
return { error: 'Maximum 50 threads per request' };
|
|
2380
|
+
}
|
|
2381
|
+
for (const id of args.thread_ids) {
|
|
2382
|
+
if (!isValidUUID(id))
|
|
2383
|
+
return { error: `Invalid thread_id format: ${id}` };
|
|
2384
|
+
}
|
|
2385
|
+
const resolved = args.resolved !== false;
|
|
2386
|
+
const { data, error } = await supabase
|
|
2387
|
+
.from('discussion_threads')
|
|
2388
|
+
.update({ is_resolved: resolved })
|
|
2389
|
+
.eq('project_id', currentProjectId)
|
|
2390
|
+
.in('id', args.thread_ids)
|
|
2391
|
+
.select('id, subject, is_resolved');
|
|
2392
|
+
if (error)
|
|
2393
|
+
return { error: error.message };
|
|
2394
|
+
const updated = data || [];
|
|
2395
|
+
const updatedIds = new Set(updated.map((t) => t.id));
|
|
2396
|
+
const notFound = args.thread_ids.filter(id => !updatedIds.has(id));
|
|
2397
|
+
return {
|
|
2398
|
+
success: true,
|
|
2399
|
+
updatedCount: updated.length,
|
|
2400
|
+
resolved,
|
|
2401
|
+
notFound: notFound.length > 0 ? notFound : undefined,
|
|
2402
|
+
threads: updated.map((t) => ({ id: t.id, subject: t.subject, is_resolved: t.is_resolved })),
|
|
2403
|
+
message: `${resolved ? 'Resolved' : 'Reopened'} ${updated.length} conversation(s).${notFound.length > 0 ? ` ${notFound.length} not found.` : ''}`,
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2158
2406
|
async function getProjectInfo() {
|
|
2159
2407
|
// Get project details
|
|
2160
2408
|
const { data: project, error: projectError } = await supabase
|
|
@@ -5296,6 +5544,54 @@ async function createTestRun(args) {
|
|
|
5296
5544
|
message: `Test run "${data.name}" created. Use assign_tests to add test assignments.`,
|
|
5297
5545
|
};
|
|
5298
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
|
+
}
|
|
5299
5595
|
async function listTestAssignments(args) {
|
|
5300
5596
|
const limit = Math.min(args.limit || 50, 200);
|
|
5301
5597
|
if (args.tester_id && !isValidUUID(args.tester_id)) {
|
|
@@ -6073,6 +6369,22 @@ async function getTestingVelocity(args) {
|
|
|
6073
6369
|
daily: dailyArray,
|
|
6074
6370
|
};
|
|
6075
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
|
+
}
|
|
6076
6388
|
// Main server setup
|
|
6077
6389
|
async function main() {
|
|
6078
6390
|
initSupabase();
|
|
@@ -6122,6 +6434,9 @@ async function main() {
|
|
|
6122
6434
|
case 'get_report_comments':
|
|
6123
6435
|
result = await getReportComments(args);
|
|
6124
6436
|
break;
|
|
6437
|
+
case 'resolve_conversations':
|
|
6438
|
+
result = await resolveConversations(args);
|
|
6439
|
+
break;
|
|
6125
6440
|
case 'get_project_info':
|
|
6126
6441
|
result = await getProjectInfo();
|
|
6127
6442
|
break;
|
|
@@ -6229,6 +6544,9 @@ async function main() {
|
|
|
6229
6544
|
case 'create_test_run':
|
|
6230
6545
|
result = await createTestRun(args);
|
|
6231
6546
|
break;
|
|
6547
|
+
case 'update_test_run':
|
|
6548
|
+
result = await updateTestRun(args);
|
|
6549
|
+
break;
|
|
6232
6550
|
case 'list_test_assignments':
|
|
6233
6551
|
result = await listTestAssignments(args);
|
|
6234
6552
|
break;
|
|
@@ -6263,6 +6581,9 @@ async function main() {
|
|
|
6263
6581
|
case 'get_testing_velocity':
|
|
6264
6582
|
result = await getTestingVelocity(args);
|
|
6265
6583
|
break;
|
|
6584
|
+
case 'get_tester_report':
|
|
6585
|
+
result = await getTesterReport(args);
|
|
6586
|
+
break;
|
|
6266
6587
|
// === PROJECT MANAGEMENT ===
|
|
6267
6588
|
case 'list_projects':
|
|
6268
6589
|
result = await listProjects();
|
|
@@ -6299,6 +6620,16 @@ async function main() {
|
|
|
6299
6620
|
case 'generate_tests_from_errors':
|
|
6300
6621
|
result = await generateTestsFromErrors(args);
|
|
6301
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;
|
|
6302
6633
|
default:
|
|
6303
6634
|
return {
|
|
6304
6635
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -210,6 +210,25 @@ const tools = [
|
|
|
210
210
|
required: ['report_id'],
|
|
211
211
|
},
|
|
212
212
|
},
|
|
213
|
+
{
|
|
214
|
+
name: 'resolve_conversations',
|
|
215
|
+
description: 'Mark one or more discussion threads as resolved (closed) or reopen them. Use this to close conversations after issues are addressed.',
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object' as const,
|
|
218
|
+
properties: {
|
|
219
|
+
thread_ids: {
|
|
220
|
+
type: 'array',
|
|
221
|
+
items: { type: 'string' },
|
|
222
|
+
description: 'UUIDs of the discussion threads to resolve/reopen',
|
|
223
|
+
},
|
|
224
|
+
resolved: {
|
|
225
|
+
type: 'boolean',
|
|
226
|
+
description: 'true to mark as resolved (default), false to reopen',
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
required: ['thread_ids'],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
213
232
|
{
|
|
214
233
|
name: 'get_project_info',
|
|
215
234
|
description: 'Get project information including QA tracks, test case counts, and common bug patterns',
|
|
@@ -1032,6 +1051,33 @@ const tools = [
|
|
|
1032
1051
|
required: ['name'],
|
|
1033
1052
|
},
|
|
1034
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
|
+
},
|
|
1035
1081
|
{
|
|
1036
1082
|
name: 'list_test_assignments',
|
|
1037
1083
|
description: 'List test assignments showing which tests are assigned to which testers, with status tracking. Joins test case and tester info.',
|
|
@@ -1278,6 +1324,24 @@ const tools = [
|
|
|
1278
1324
|
},
|
|
1279
1325
|
},
|
|
1280
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
|
+
},
|
|
1281
1345
|
// === PROJECT MANAGEMENT TOOLS ===
|
|
1282
1346
|
{
|
|
1283
1347
|
name: 'list_projects',
|
|
@@ -1449,8 +1513,177 @@ const tools = [
|
|
|
1449
1513
|
},
|
|
1450
1514
|
},
|
|
1451
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
|
+
},
|
|
1452
1579
|
];
|
|
1453
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
|
+
|
|
1454
1687
|
// === TEST EXECUTION INTELLIGENCE ===
|
|
1455
1688
|
|
|
1456
1689
|
async function getTestImpact(args: { changed_files: string[] }) {
|
|
@@ -2283,6 +2516,42 @@ async function getReportComments(args: { report_id: string }) {
|
|
|
2283
2516
|
return { comments: (messages || []).map(m => ({ id: m.id, sender_type: m.sender_type, content: m.content, created_at: m.created_at, attachments: m.attachments })), total: (messages || []).length };
|
|
2284
2517
|
}
|
|
2285
2518
|
|
|
2519
|
+
async function resolveConversations(args: { thread_ids: string[]; resolved?: boolean }) {
|
|
2520
|
+
if (!args.thread_ids || args.thread_ids.length === 0) {
|
|
2521
|
+
return { error: 'At least one thread_id is required' };
|
|
2522
|
+
}
|
|
2523
|
+
if (args.thread_ids.length > 50) {
|
|
2524
|
+
return { error: 'Maximum 50 threads per request' };
|
|
2525
|
+
}
|
|
2526
|
+
for (const id of args.thread_ids) {
|
|
2527
|
+
if (!isValidUUID(id)) return { error: `Invalid thread_id format: ${id}` };
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
const resolved = args.resolved !== false;
|
|
2531
|
+
|
|
2532
|
+
const { data, error } = await supabase
|
|
2533
|
+
.from('discussion_threads')
|
|
2534
|
+
.update({ is_resolved: resolved })
|
|
2535
|
+
.eq('project_id', currentProjectId)
|
|
2536
|
+
.in('id', args.thread_ids)
|
|
2537
|
+
.select('id, subject, is_resolved');
|
|
2538
|
+
|
|
2539
|
+
if (error) return { error: error.message };
|
|
2540
|
+
|
|
2541
|
+
const updated = data || [];
|
|
2542
|
+
const updatedIds = new Set(updated.map((t: any) => t.id));
|
|
2543
|
+
const notFound = args.thread_ids.filter(id => !updatedIds.has(id));
|
|
2544
|
+
|
|
2545
|
+
return {
|
|
2546
|
+
success: true,
|
|
2547
|
+
updatedCount: updated.length,
|
|
2548
|
+
resolved,
|
|
2549
|
+
notFound: notFound.length > 0 ? notFound : undefined,
|
|
2550
|
+
threads: updated.map((t: any) => ({ id: t.id, subject: t.subject, is_resolved: t.is_resolved })),
|
|
2551
|
+
message: `${resolved ? 'Resolved' : 'Reopened'} ${updated.length} conversation(s).${notFound.length > 0 ? ` ${notFound.length} not found.` : ''}`,
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2286
2555
|
async function getProjectInfo() {
|
|
2287
2556
|
// Get project details
|
|
2288
2557
|
const { data: project, error: projectError } = await supabase
|
|
@@ -5992,6 +6261,64 @@ async function createTestRun(args: {
|
|
|
5992
6261
|
};
|
|
5993
6262
|
}
|
|
5994
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
|
+
|
|
5995
6322
|
async function listTestAssignments(args: {
|
|
5996
6323
|
tester_id?: string;
|
|
5997
6324
|
test_run_id?: string;
|
|
@@ -6897,6 +7224,25 @@ async function getTestingVelocity(args: {
|
|
|
6897
7224
|
};
|
|
6898
7225
|
}
|
|
6899
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
|
+
|
|
6900
7246
|
// Main server setup
|
|
6901
7247
|
async function main() {
|
|
6902
7248
|
initSupabase();
|
|
@@ -6955,6 +7301,9 @@ async function main() {
|
|
|
6955
7301
|
case 'get_report_comments':
|
|
6956
7302
|
result = await getReportComments(args as any);
|
|
6957
7303
|
break;
|
|
7304
|
+
case 'resolve_conversations':
|
|
7305
|
+
result = await resolveConversations(args as any);
|
|
7306
|
+
break;
|
|
6958
7307
|
case 'get_project_info':
|
|
6959
7308
|
result = await getProjectInfo();
|
|
6960
7309
|
break;
|
|
@@ -7062,6 +7411,9 @@ async function main() {
|
|
|
7062
7411
|
case 'create_test_run':
|
|
7063
7412
|
result = await createTestRun(args as any);
|
|
7064
7413
|
break;
|
|
7414
|
+
case 'update_test_run':
|
|
7415
|
+
result = await updateTestRun(args as any);
|
|
7416
|
+
break;
|
|
7065
7417
|
case 'list_test_assignments':
|
|
7066
7418
|
result = await listTestAssignments(args as any);
|
|
7067
7419
|
break;
|
|
@@ -7096,6 +7448,9 @@ async function main() {
|
|
|
7096
7448
|
case 'get_testing_velocity':
|
|
7097
7449
|
result = await getTestingVelocity(args as any);
|
|
7098
7450
|
break;
|
|
7451
|
+
case 'get_tester_report':
|
|
7452
|
+
result = await getTesterReport(args as any);
|
|
7453
|
+
break;
|
|
7099
7454
|
// === PROJECT MANAGEMENT ===
|
|
7100
7455
|
case 'list_projects':
|
|
7101
7456
|
result = await listProjects();
|
|
@@ -7132,6 +7487,16 @@ async function main() {
|
|
|
7132
7487
|
case 'generate_tests_from_errors':
|
|
7133
7488
|
result = await generateTestsFromErrors(args as any);
|
|
7134
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;
|
|
7135
7500
|
default:
|
|
7136
7501
|
return {
|
|
7137
7502
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|