@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.
Files changed (3) hide show
  1. package/dist/index.js +331 -0
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/mcp-server",
3
- "version": "0.7.0",
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
@@ -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}` }],