@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.
Files changed (3) hide show
  1. package/dist/index.js +277 -0
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/mcp-server",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "MCP server for BugBear - allows Claude Code to query bug reports",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
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}` }],