@bbearai/mcp-server 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +1365 -126
  2. package/package.json +1 -1
  3. package/src/index.ts +1504 -115
package/dist/index.js CHANGED
@@ -12,26 +12,30 @@ const supabase_js_1 = require("@supabase/supabase-js");
12
12
  // Configuration from environment
13
13
  const SUPABASE_URL = process.env.SUPABASE_URL || 'https://kyxgzjnqgvapvlnvqawz.supabase.co';
14
14
  const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || '';
15
- const PROJECT_ID = process.env.BUGBEAR_PROJECT_ID || '';
15
+ // Active project — set from env var, switchable via switch_project tool
16
+ let currentProjectId = process.env.BUGBEAR_currentProjectId || '';
16
17
  // Initialize Supabase client
17
18
  let supabase;
18
19
  function validateConfig() {
19
- const errors = [];
20
20
  if (!SUPABASE_ANON_KEY) {
21
- errors.push('SUPABASE_ANON_KEY environment variable is required');
21
+ console.error('BugBear MCP Server: SUPABASE_ANON_KEY environment variable is required');
22
+ process.exit(1);
22
23
  }
23
- if (!PROJECT_ID) {
24
- errors.push('BUGBEAR_PROJECT_ID environment variable is required');
24
+ // Validate project ID format if provided
25
+ if (currentProjectId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(currentProjectId)) {
26
+ console.error('BugBear MCP Server: BUGBEAR_currentProjectId must be a valid UUID');
27
+ process.exit(1);
25
28
  }
26
- // Basic UUID format validation for project ID
27
- if (PROJECT_ID && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(PROJECT_ID)) {
28
- errors.push('BUGBEAR_PROJECT_ID must be a valid UUID');
29
+ if (!currentProjectId) {
30
+ console.error('BugBear MCP Server: No BUGBEAR_currentProjectId set. Use list_projects + switch_project to select one.');
29
31
  }
30
- if (errors.length > 0) {
31
- console.error('BugBear MCP Server configuration errors:');
32
- errors.forEach(e => console.error(` - ${e}`));
33
- process.exit(1);
32
+ }
33
+ /** Guard for tools that require a project to be selected */
34
+ function requireProject() {
35
+ if (!currentProjectId) {
36
+ throw new Error('No project selected. Use list_projects to see available projects, then switch_project to select one.');
34
37
  }
38
+ return currentProjectId;
35
39
  }
36
40
  function initSupabase() {
37
41
  validateConfig();
@@ -435,6 +439,10 @@ const tools = [
435
439
  items: { type: 'string' },
436
440
  description: 'List of files that were modified to fix this bug',
437
441
  },
442
+ notify_tester: {
443
+ type: 'boolean',
444
+ description: 'If true, notify the original tester about the fix with a message and verification task. Default: false (silent resolve).',
445
+ },
438
446
  },
439
447
  required: ['report_id', 'commit_sha'],
440
448
  },
@@ -902,13 +910,366 @@ const tools = [
902
910
  },
903
911
  },
904
912
  },
913
+ // === TESTER & ASSIGNMENT MANAGEMENT TOOLS ===
914
+ {
915
+ name: 'list_testers',
916
+ description: 'List all QA testers for the project with their status, platforms, and workload counts.',
917
+ inputSchema: {
918
+ type: 'object',
919
+ properties: {
920
+ status: {
921
+ type: 'string',
922
+ enum: ['active', 'inactive', 'invited'],
923
+ description: 'Filter by tester status (default: all)',
924
+ },
925
+ platform: {
926
+ type: 'string',
927
+ enum: ['ios', 'android', 'web'],
928
+ description: 'Filter by platform support',
929
+ },
930
+ },
931
+ },
932
+ },
933
+ {
934
+ name: 'list_test_runs',
935
+ description: 'List testing campaigns (test runs) for the project with pass/fail stats.',
936
+ inputSchema: {
937
+ type: 'object',
938
+ properties: {
939
+ status: {
940
+ type: 'string',
941
+ enum: ['draft', 'active', 'paused', 'completed', 'archived'],
942
+ description: 'Filter by test run status',
943
+ },
944
+ limit: {
945
+ type: 'number',
946
+ description: 'Maximum number of runs to return (default: 20)',
947
+ },
948
+ },
949
+ },
950
+ },
951
+ {
952
+ name: 'create_test_run',
953
+ description: 'Create a new testing campaign (test run). Tests can then be assigned to testers within this run.',
954
+ inputSchema: {
955
+ type: 'object',
956
+ properties: {
957
+ name: {
958
+ type: 'string',
959
+ description: 'Name for the test run (e.g. "v2.1 QA Pass", "Sprint 5 Testing")',
960
+ },
961
+ description: {
962
+ type: 'string',
963
+ description: 'Optional description of the test run scope and goals',
964
+ },
965
+ },
966
+ required: ['name'],
967
+ },
968
+ },
969
+ {
970
+ name: 'list_test_assignments',
971
+ description: 'List test assignments showing which tests are assigned to which testers, with status tracking. Joins test case and tester info.',
972
+ inputSchema: {
973
+ type: 'object',
974
+ properties: {
975
+ tester_id: {
976
+ type: 'string',
977
+ description: 'Filter by tester UUID',
978
+ },
979
+ test_run_id: {
980
+ type: 'string',
981
+ description: 'Filter by test run UUID',
982
+ },
983
+ status: {
984
+ type: 'string',
985
+ enum: ['pending', 'in_progress', 'passed', 'failed', 'blocked', 'skipped'],
986
+ description: 'Filter by assignment status',
987
+ },
988
+ limit: {
989
+ type: 'number',
990
+ description: 'Maximum number of assignments to return (default: 50, max: 200)',
991
+ },
992
+ },
993
+ },
994
+ },
995
+ {
996
+ name: 'assign_tests',
997
+ description: 'Assign one or more test cases to a tester. Optionally assign within a test run. Skips duplicates gracefully.',
998
+ inputSchema: {
999
+ type: 'object',
1000
+ properties: {
1001
+ tester_id: {
1002
+ type: 'string',
1003
+ description: 'UUID of the tester to assign tests to (required)',
1004
+ },
1005
+ test_case_ids: {
1006
+ type: 'array',
1007
+ items: { type: 'string' },
1008
+ description: 'Array of test case UUIDs to assign (required)',
1009
+ },
1010
+ test_run_id: {
1011
+ type: 'string',
1012
+ description: 'Optional test run UUID to group assignments under',
1013
+ },
1014
+ },
1015
+ required: ['tester_id', 'test_case_ids'],
1016
+ },
1017
+ },
1018
+ {
1019
+ name: 'get_tester_workload',
1020
+ description: 'View a specific tester\'s current workload — assignment counts by status and recent assignments.',
1021
+ inputSchema: {
1022
+ type: 'object',
1023
+ properties: {
1024
+ tester_id: {
1025
+ type: 'string',
1026
+ description: 'UUID of the tester (required)',
1027
+ },
1028
+ },
1029
+ required: ['tester_id'],
1030
+ },
1031
+ },
1032
+ // === NEW TESTER & ANALYTICS TOOLS ===
1033
+ {
1034
+ name: 'create_tester',
1035
+ description: 'Add a new QA tester to the project without opening the dashboard.',
1036
+ inputSchema: {
1037
+ type: 'object',
1038
+ properties: {
1039
+ name: {
1040
+ type: 'string',
1041
+ description: 'Full name of the tester (required)',
1042
+ },
1043
+ email: {
1044
+ type: 'string',
1045
+ description: 'Email address of the tester (required, must be unique per project)',
1046
+ },
1047
+ platforms: {
1048
+ type: 'array',
1049
+ items: { type: 'string', enum: ['ios', 'android', 'web'] },
1050
+ description: 'Platforms the tester can test on (default: ["ios", "web"])',
1051
+ },
1052
+ tier: {
1053
+ type: 'number',
1054
+ description: 'Tester tier 1-3 (default: 1)',
1055
+ },
1056
+ notes: {
1057
+ type: 'string',
1058
+ description: 'Optional notes about the tester',
1059
+ },
1060
+ },
1061
+ required: ['name', 'email'],
1062
+ },
1063
+ },
1064
+ {
1065
+ name: 'update_tester',
1066
+ description: 'Update an existing tester\'s status, platforms, tier, or notes.',
1067
+ inputSchema: {
1068
+ type: 'object',
1069
+ properties: {
1070
+ tester_id: {
1071
+ type: 'string',
1072
+ description: 'UUID of the tester to update (required)',
1073
+ },
1074
+ status: {
1075
+ type: 'string',
1076
+ enum: ['active', 'inactive', 'invited'],
1077
+ description: 'New status for the tester',
1078
+ },
1079
+ platforms: {
1080
+ type: 'array',
1081
+ items: { type: 'string', enum: ['ios', 'android', 'web'] },
1082
+ description: 'Updated platforms array',
1083
+ },
1084
+ tier: {
1085
+ type: 'number',
1086
+ description: 'Updated tier (1-3)',
1087
+ },
1088
+ notes: {
1089
+ type: 'string',
1090
+ description: 'Updated notes',
1091
+ },
1092
+ name: {
1093
+ type: 'string',
1094
+ description: 'Updated name',
1095
+ },
1096
+ },
1097
+ required: ['tester_id'],
1098
+ },
1099
+ },
1100
+ {
1101
+ name: 'bulk_update_reports',
1102
+ description: 'Update the status of multiple bug reports at once. Useful after a fix session to close many bugs.',
1103
+ inputSchema: {
1104
+ type: 'object',
1105
+ properties: {
1106
+ report_ids: {
1107
+ type: 'array',
1108
+ items: { type: 'string' },
1109
+ description: 'Array of report UUIDs to update (required, max 50)',
1110
+ },
1111
+ status: {
1112
+ type: 'string',
1113
+ enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', 'resolved', 'verified', 'wont_fix', 'duplicate', 'closed'],
1114
+ description: 'New status for all reports (required)',
1115
+ },
1116
+ resolution_notes: {
1117
+ type: 'string',
1118
+ description: 'Optional resolution notes applied to all reports',
1119
+ },
1120
+ },
1121
+ required: ['report_ids', 'status'],
1122
+ },
1123
+ },
1124
+ {
1125
+ name: 'get_bug_trends',
1126
+ description: 'Get bug report trends over time — grouped by week, severity, category, or status. Useful for spotting patterns.',
1127
+ inputSchema: {
1128
+ type: 'object',
1129
+ properties: {
1130
+ group_by: {
1131
+ type: 'string',
1132
+ enum: ['week', 'severity', 'category', 'status'],
1133
+ description: 'How to group the trends (default: week)',
1134
+ },
1135
+ days: {
1136
+ type: 'number',
1137
+ description: 'Number of days to look back (default: 30, max: 180)',
1138
+ },
1139
+ },
1140
+ },
1141
+ },
1142
+ {
1143
+ name: 'get_tester_leaderboard',
1144
+ description: 'Rank testers by testing activity — bugs found, tests completed, pass rate, and average test duration.',
1145
+ inputSchema: {
1146
+ type: 'object',
1147
+ properties: {
1148
+ days: {
1149
+ type: 'number',
1150
+ description: 'Number of days to look back (default: 30, max: 180)',
1151
+ },
1152
+ sort_by: {
1153
+ type: 'string',
1154
+ enum: ['bugs_found', 'tests_completed', 'pass_rate'],
1155
+ description: 'Sort metric (default: tests_completed)',
1156
+ },
1157
+ },
1158
+ },
1159
+ },
1160
+ {
1161
+ name: 'export_test_results',
1162
+ description: 'Export test results for a specific test run as structured JSON — includes every assignment, tester, result, and duration.',
1163
+ inputSchema: {
1164
+ type: 'object',
1165
+ properties: {
1166
+ test_run_id: {
1167
+ type: 'string',
1168
+ description: 'UUID of the test run to export (required)',
1169
+ },
1170
+ },
1171
+ required: ['test_run_id'],
1172
+ },
1173
+ },
1174
+ {
1175
+ name: 'get_testing_velocity',
1176
+ description: 'Get a rolling average of test completions per day over the specified window. Shows daily completion counts and trend direction.',
1177
+ inputSchema: {
1178
+ type: 'object',
1179
+ properties: {
1180
+ days: {
1181
+ type: 'number',
1182
+ description: 'Number of days to analyze (default: 14, max: 90)',
1183
+ },
1184
+ },
1185
+ },
1186
+ },
1187
+ // === PROJECT MANAGEMENT TOOLS ===
1188
+ {
1189
+ name: 'list_projects',
1190
+ description: 'List all BugBear projects accessible with the current credentials. Use this to find project IDs for switch_project.',
1191
+ inputSchema: {
1192
+ type: 'object',
1193
+ properties: {},
1194
+ },
1195
+ },
1196
+ {
1197
+ name: 'switch_project',
1198
+ description: 'Switch the active project. All subsequent tool calls will use this project. Use list_projects first to find the project ID.',
1199
+ inputSchema: {
1200
+ type: 'object',
1201
+ properties: {
1202
+ project_id: {
1203
+ type: 'string',
1204
+ description: 'UUID of the project to switch to (required)',
1205
+ },
1206
+ },
1207
+ required: ['project_id'],
1208
+ },
1209
+ },
1210
+ {
1211
+ name: 'get_current_project',
1212
+ description: 'Show which project is currently active.',
1213
+ inputSchema: {
1214
+ type: 'object',
1215
+ properties: {},
1216
+ },
1217
+ },
905
1218
  ];
1219
+ // === Project management handlers ===
1220
+ async function listProjects() {
1221
+ const { data, error } = await supabase
1222
+ .from('projects')
1223
+ .select('id, name, slug, is_qa_enabled, created_at')
1224
+ .order('name');
1225
+ if (error) {
1226
+ return { error: error.message };
1227
+ }
1228
+ return {
1229
+ currentProjectId: currentProjectId || null,
1230
+ projects: data?.map(p => ({
1231
+ id: p.id,
1232
+ name: p.name,
1233
+ slug: p.slug,
1234
+ isQAEnabled: p.is_qa_enabled,
1235
+ isActive: p.id === currentProjectId,
1236
+ createdAt: p.created_at,
1237
+ })) || [],
1238
+ };
1239
+ }
1240
+ async function switchProject(args) {
1241
+ if (!isValidUUID(args.project_id)) {
1242
+ return { error: 'Invalid project_id format — must be a UUID' };
1243
+ }
1244
+ // Verify the project exists and is accessible
1245
+ const { data, error } = await supabase
1246
+ .from('projects')
1247
+ .select('id, name, slug')
1248
+ .eq('id', args.project_id)
1249
+ .single();
1250
+ if (error || !data) {
1251
+ return { error: 'Project not found or not accessible' };
1252
+ }
1253
+ currentProjectId = data.id;
1254
+ return {
1255
+ success: true,
1256
+ message: `Switched to project "${data.name}" (${data.slug})`,
1257
+ projectId: data.id,
1258
+ projectName: data.name,
1259
+ };
1260
+ }
1261
+ function getCurrentProject() {
1262
+ if (!currentProjectId) {
1263
+ return { message: 'No project selected. Use list_projects to see available projects, then switch_project to select one.' };
1264
+ }
1265
+ return { projectId: currentProjectId };
1266
+ }
906
1267
  // Tool handlers
907
1268
  async function listReports(args) {
908
1269
  let query = supabase
909
1270
  .from('reports')
910
- .select('id, report_type, severity, status, description, app_context, created_at, reporter_name, reporter_email, tester:testers(name, email)')
911
- .eq('project_id', PROJECT_ID)
1271
+ .select('id, report_type, severity, status, description, app_context, created_at, reporter_name, tester:testers(name)')
1272
+ .eq('project_id', currentProjectId)
912
1273
  .order('created_at', { ascending: false })
913
1274
  .limit(Math.min(args.limit || 10, 50));
914
1275
  if (args.status)
@@ -944,7 +1305,7 @@ async function getReport(args) {
944
1305
  .from('reports')
945
1306
  .select('*, tester:testers(*), track:qa_tracks(*)')
946
1307
  .eq('id', args.report_id)
947
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
1308
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
948
1309
  .single();
949
1310
  if (error) {
950
1311
  return { error: error.message };
@@ -963,10 +1324,8 @@ async function getReport(args) {
963
1324
  created_at: data.created_at,
964
1325
  reporter: data.tester ? {
965
1326
  name: data.tester.name,
966
- email: data.tester.email,
967
1327
  } : (data.reporter_name ? {
968
1328
  name: data.reporter_name,
969
- email: data.reporter_email,
970
1329
  } : null),
971
1330
  track: data.track ? {
972
1331
  name: data.track.name,
@@ -981,7 +1340,7 @@ async function searchReports(args) {
981
1340
  let query = supabase
982
1341
  .from('reports')
983
1342
  .select('id, report_type, severity, status, description, app_context, created_at')
984
- .eq('project_id', PROJECT_ID)
1343
+ .eq('project_id', currentProjectId)
985
1344
  .order('created_at', { ascending: false })
986
1345
  .limit(20);
987
1346
  if (sanitizedQuery) {
@@ -1024,7 +1383,7 @@ async function updateReportStatus(args) {
1024
1383
  .from('reports')
1025
1384
  .update(updates)
1026
1385
  .eq('id', args.report_id)
1027
- .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
1386
+ .eq('project_id', currentProjectId); // Security: ensure report belongs to this project
1028
1387
  if (error) {
1029
1388
  return { error: error.message };
1030
1389
  }
@@ -1038,7 +1397,7 @@ async function getReportContext(args) {
1038
1397
  .from('reports')
1039
1398
  .select('app_context, device_info, navigation_history, enhanced_context')
1040
1399
  .eq('id', args.report_id)
1041
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
1400
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
1042
1401
  .single();
1043
1402
  if (error) {
1044
1403
  return { error: error.message };
@@ -1057,7 +1416,7 @@ async function getProjectInfo() {
1057
1416
  const { data: project, error: projectError } = await supabase
1058
1417
  .from('projects')
1059
1418
  .select('id, name, slug, is_qa_enabled')
1060
- .eq('id', PROJECT_ID)
1419
+ .eq('id', currentProjectId)
1061
1420
  .single();
1062
1421
  if (projectError) {
1063
1422
  return { error: projectError.message };
@@ -1066,17 +1425,17 @@ async function getProjectInfo() {
1066
1425
  const { data: tracks } = await supabase
1067
1426
  .from('qa_tracks')
1068
1427
  .select('id, name, icon, test_template')
1069
- .eq('project_id', PROJECT_ID);
1428
+ .eq('project_id', currentProjectId);
1070
1429
  // Get test case count
1071
1430
  const { count: testCaseCount } = await supabase
1072
1431
  .from('test_cases')
1073
1432
  .select('id', { count: 'exact', head: true })
1074
- .eq('project_id', PROJECT_ID);
1433
+ .eq('project_id', currentProjectId);
1075
1434
  // Get open bug count
1076
1435
  const { count: openBugCount } = await supabase
1077
1436
  .from('reports')
1078
1437
  .select('id', { count: 'exact', head: true })
1079
- .eq('project_id', PROJECT_ID)
1438
+ .eq('project_id', currentProjectId)
1080
1439
  .eq('report_type', 'bug')
1081
1440
  .in('status', ['new', 'confirmed', 'in_progress']);
1082
1441
  return {
@@ -1103,7 +1462,7 @@ async function getQaTracks() {
1103
1462
  const { data, error } = await supabase
1104
1463
  .from('qa_tracks')
1105
1464
  .select('*')
1106
- .eq('project_id', PROJECT_ID)
1465
+ .eq('project_id', currentProjectId)
1107
1466
  .order('sort_order');
1108
1467
  if (error) {
1109
1468
  return { error: error.message };
@@ -1126,16 +1485,19 @@ async function createTestCase(args) {
1126
1485
  // Find track ID if track name provided
1127
1486
  let trackId = null;
1128
1487
  if (args.track) {
1129
- const { data: trackData } = await supabase
1130
- .from('qa_tracks')
1131
- .select('id')
1132
- .eq('project_id', PROJECT_ID)
1133
- .ilike('name', `%${args.track}%`)
1134
- .single();
1135
- trackId = trackData?.id || null;
1488
+ const sanitizedTrack = sanitizeSearchQuery(args.track);
1489
+ if (sanitizedTrack) {
1490
+ const { data: trackData } = await supabase
1491
+ .from('qa_tracks')
1492
+ .select('id')
1493
+ .eq('project_id', currentProjectId)
1494
+ .ilike('name', `%${sanitizedTrack}%`)
1495
+ .single();
1496
+ trackId = trackData?.id || null;
1497
+ }
1136
1498
  }
1137
1499
  const testCase = {
1138
- project_id: PROJECT_ID,
1500
+ project_id: currentProjectId,
1139
1501
  test_key: args.test_key,
1140
1502
  title: args.title,
1141
1503
  description: args.description || '',
@@ -1175,7 +1537,7 @@ async function updateTestCase(args) {
1175
1537
  const { data: existing } = await supabase
1176
1538
  .from('test_cases')
1177
1539
  .select('id')
1178
- .eq('project_id', PROJECT_ID)
1540
+ .eq('project_id', currentProjectId)
1179
1541
  .eq('test_key', args.test_key)
1180
1542
  .single();
1181
1543
  if (!existing) {
@@ -1206,7 +1568,7 @@ async function updateTestCase(args) {
1206
1568
  .from('test_cases')
1207
1569
  .update(updates)
1208
1570
  .eq('id', testCaseId)
1209
- .eq('project_id', PROJECT_ID)
1571
+ .eq('project_id', currentProjectId)
1210
1572
  .select('id, test_key, title, target_route')
1211
1573
  .single();
1212
1574
  if (error) {
@@ -1251,7 +1613,7 @@ async function deleteTestCases(args) {
1251
1613
  const { data: existing } = await supabase
1252
1614
  .from('test_cases')
1253
1615
  .select('id')
1254
- .eq('project_id', PROJECT_ID)
1616
+ .eq('project_id', currentProjectId)
1255
1617
  .eq('test_key', args.test_key)
1256
1618
  .single();
1257
1619
  if (!existing) {
@@ -1284,7 +1646,7 @@ async function deleteTestCases(args) {
1284
1646
  const { data: existing, error: lookupError } = await supabase
1285
1647
  .from('test_cases')
1286
1648
  .select('id, test_key')
1287
- .eq('project_id', PROJECT_ID)
1649
+ .eq('project_id', currentProjectId)
1288
1650
  .in('test_key', args.test_keys);
1289
1651
  if (lookupError) {
1290
1652
  return { error: lookupError.message };
@@ -1300,7 +1662,7 @@ async function deleteTestCases(args) {
1300
1662
  const { data: toDelete } = await supabase
1301
1663
  .from('test_cases')
1302
1664
  .select('id, test_key, title')
1303
- .eq('project_id', PROJECT_ID)
1665
+ .eq('project_id', currentProjectId)
1304
1666
  .in('id', idsToDelete);
1305
1667
  if (!toDelete || toDelete.length === 0) {
1306
1668
  return { error: 'No matching test cases found in this project' };
@@ -1309,7 +1671,7 @@ async function deleteTestCases(args) {
1309
1671
  const { error: deleteError } = await supabase
1310
1672
  .from('test_cases')
1311
1673
  .delete()
1312
- .eq('project_id', PROJECT_ID)
1674
+ .eq('project_id', currentProjectId)
1313
1675
  .in('id', idsToDelete);
1314
1676
  if (deleteError) {
1315
1677
  return { error: deleteError.message };
@@ -1343,7 +1705,7 @@ async function listTestCases(args) {
1343
1705
  steps,
1344
1706
  track:qa_tracks(id, name, icon, color)
1345
1707
  `)
1346
- .eq('project_id', PROJECT_ID)
1708
+ .eq('project_id', currentProjectId)
1347
1709
  .order('test_key', { ascending: true });
1348
1710
  // Apply filters
1349
1711
  if (args.priority) {
@@ -1390,7 +1752,7 @@ async function getBugPatterns(args) {
1390
1752
  let query = supabase
1391
1753
  .from('reports')
1392
1754
  .select('app_context, severity, status, created_at')
1393
- .eq('project_id', PROJECT_ID)
1755
+ .eq('project_id', currentProjectId)
1394
1756
  .eq('report_type', 'bug')
1395
1757
  .order('created_at', { ascending: false })
1396
1758
  .limit(100);
@@ -1442,7 +1804,7 @@ async function suggestTestCases(args) {
1442
1804
  const { data: existingTests } = await supabase
1443
1805
  .from('test_cases')
1444
1806
  .select('test_key, title')
1445
- .eq('project_id', PROJECT_ID)
1807
+ .eq('project_id', currentProjectId)
1446
1808
  .order('test_key', { ascending: false })
1447
1809
  .limit(1);
1448
1810
  // Calculate next test key number
@@ -1477,7 +1839,7 @@ async function suggestTestCases(args) {
1477
1839
  const { data: relatedBugs } = await supabase
1478
1840
  .from('reports')
1479
1841
  .select('id, description, severity')
1480
- .eq('project_id', PROJECT_ID)
1842
+ .eq('project_id', currentProjectId)
1481
1843
  .eq('report_type', 'bug')
1482
1844
  .limit(10);
1483
1845
  const routeBugs = (relatedBugs || []).filter(bug => {
@@ -1511,12 +1873,16 @@ async function getTestPriorities(args) {
1511
1873
  const minScore = args.min_score || 0;
1512
1874
  const includeFactors = args.include_factors !== false;
1513
1875
  // First, refresh the route stats
1514
- await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
1876
+ const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: currentProjectId });
1877
+ if (refreshError) {
1878
+ // Non-fatal: proceed with potentially stale data but warn
1879
+ console.warn('Failed to refresh route stats:', refreshError.message);
1880
+ }
1515
1881
  // Get prioritized routes
1516
1882
  const { data: routes, error } = await supabase
1517
1883
  .from('route_test_stats')
1518
1884
  .select('*')
1519
- .eq('project_id', PROJECT_ID)
1885
+ .eq('project_id', currentProjectId)
1520
1886
  .gte('priority_score', minScore)
1521
1887
  .order('priority_score', { ascending: false })
1522
1888
  .limit(limit);
@@ -1633,7 +1999,7 @@ async function getCoverageGaps(args) {
1633
1999
  const { data: routesFromReports } = await supabase
1634
2000
  .from('reports')
1635
2001
  .select('app_context')
1636
- .eq('project_id', PROJECT_ID)
2002
+ .eq('project_id', currentProjectId)
1637
2003
  .not('app_context->currentRoute', 'is', null);
1638
2004
  const allRoutes = new Set();
1639
2005
  (routesFromReports || []).forEach(r => {
@@ -1645,7 +2011,7 @@ async function getCoverageGaps(args) {
1645
2011
  const { data: testCases } = await supabase
1646
2012
  .from('test_cases')
1647
2013
  .select('target_route, category, track_id')
1648
- .eq('project_id', PROJECT_ID);
2014
+ .eq('project_id', currentProjectId);
1649
2015
  const coveredRoutes = new Set();
1650
2016
  const routeTrackCoverage = {};
1651
2017
  (testCases || []).forEach(tc => {
@@ -1662,13 +2028,13 @@ async function getCoverageGaps(args) {
1662
2028
  const { data: tracks } = await supabase
1663
2029
  .from('qa_tracks')
1664
2030
  .select('id, name')
1665
- .eq('project_id', PROJECT_ID);
2031
+ .eq('project_id', currentProjectId);
1666
2032
  const trackMap = new Map((tracks || []).map(t => [t.id, t.name]));
1667
2033
  // Get route stats for staleness
1668
2034
  const { data: routeStats } = await supabase
1669
2035
  .from('route_test_stats')
1670
2036
  .select('route, last_tested_at, open_bugs, critical_bugs')
1671
- .eq('project_id', PROJECT_ID);
2037
+ .eq('project_id', currentProjectId);
1672
2038
  const routeStatsMap = new Map((routeStats || []).map(r => [r.route, r]));
1673
2039
  // Find untested routes
1674
2040
  if (gapType === 'all' || gapType === 'untested_routes') {
@@ -1765,14 +2131,14 @@ async function getRegressions(args) {
1765
2131
  const { data: resolvedBugs } = await supabase
1766
2132
  .from('reports')
1767
2133
  .select('id, description, severity, app_context, resolved_at')
1768
- .eq('project_id', PROJECT_ID)
2134
+ .eq('project_id', currentProjectId)
1769
2135
  .eq('report_type', 'bug')
1770
2136
  .in('status', ['resolved', 'fixed', 'verified', 'closed'])
1771
2137
  .gte('resolved_at', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString());
1772
2138
  const { data: newBugs } = await supabase
1773
2139
  .from('reports')
1774
2140
  .select('id, description, severity, app_context, created_at')
1775
- .eq('project_id', PROJECT_ID)
2141
+ .eq('project_id', currentProjectId)
1776
2142
  .eq('report_type', 'bug')
1777
2143
  .in('status', ['new', 'triaging', 'confirmed', 'in_progress', 'reviewed'])
1778
2144
  .gte('created_at', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString());
@@ -1876,21 +2242,23 @@ async function getCoverageMatrix(args) {
1876
2242
  const { data: tracks } = await supabase
1877
2243
  .from('qa_tracks')
1878
2244
  .select('id, name, icon, color')
1879
- .eq('project_id', PROJECT_ID)
2245
+ .eq('project_id', currentProjectId)
1880
2246
  .order('sort_order');
1881
2247
  // Get test cases with track info
1882
2248
  const { data: testCases } = await supabase
1883
2249
  .from('test_cases')
1884
2250
  .select('id, target_route, category, track_id')
1885
- .eq('project_id', PROJECT_ID);
2251
+ .eq('project_id', currentProjectId);
1886
2252
  // Get test assignments for execution data
1887
2253
  let assignments = [];
1888
2254
  if (includeExecution) {
1889
2255
  const { data } = await supabase
1890
2256
  .from('test_assignments')
1891
2257
  .select('test_case_id, status, completed_at')
1892
- .eq('project_id', PROJECT_ID)
1893
- .in('status', ['passed', 'failed']);
2258
+ .eq('project_id', currentProjectId)
2259
+ .in('status', ['passed', 'failed'])
2260
+ .order('completed_at', { ascending: false })
2261
+ .limit(2000);
1894
2262
  assignments = data || [];
1895
2263
  }
1896
2264
  // Get route stats for bug counts
@@ -1899,7 +2267,7 @@ async function getCoverageMatrix(args) {
1899
2267
  const { data } = await supabase
1900
2268
  .from('route_test_stats')
1901
2269
  .select('route, open_bugs, critical_bugs')
1902
- .eq('project_id', PROJECT_ID);
2270
+ .eq('project_id', currentProjectId);
1903
2271
  routeStats = data || [];
1904
2272
  }
1905
2273
  const routeStatsMap = new Map(routeStats.map(r => [r.route, r]));
@@ -2038,12 +2406,16 @@ async function getStaleCoverage(args) {
2038
2406
  const daysThreshold = args.days_threshold || 14;
2039
2407
  const limit = args.limit || 20;
2040
2408
  // Refresh stats first
2041
- await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
2409
+ const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: currentProjectId });
2410
+ if (refreshError) {
2411
+ // Non-fatal: proceed with potentially stale data but warn
2412
+ console.warn('Failed to refresh route stats:', refreshError.message);
2413
+ }
2042
2414
  // Get routes ordered by staleness and risk
2043
2415
  const { data: routes, error } = await supabase
2044
2416
  .from('route_test_stats')
2045
2417
  .select('route, last_tested_at, open_bugs, critical_bugs, test_case_count, priority_score')
2046
- .eq('project_id', PROJECT_ID)
2418
+ .eq('project_id', currentProjectId)
2047
2419
  .order('last_tested_at', { ascending: true, nullsFirst: true })
2048
2420
  .limit(limit * 2); // Get extra to filter
2049
2421
  if (error) {
@@ -2123,17 +2495,34 @@ async function generateDeployChecklist(args) {
2123
2495
  }
2124
2496
  });
2125
2497
  }
2126
- // Get test cases for these routes
2127
- const { data: testCases } = await supabase
2128
- .from('test_cases')
2129
- .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2130
- .eq('project_id', PROJECT_ID)
2131
- .or(routes.map(r => `target_route.eq.${r}`).join(',') + ',' + routes.map(r => `category.eq.${r}`).join(','));
2498
+ // Limit routes array to prevent query explosion
2499
+ const safeRoutes = routes.slice(0, 100);
2500
+ // Get test cases for these routes (use separate queries to avoid filter injection)
2501
+ const [{ data: byRoute }, { data: byCategory }] = await Promise.all([
2502
+ supabase
2503
+ .from('test_cases')
2504
+ .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2505
+ .eq('project_id', currentProjectId)
2506
+ .in('target_route', safeRoutes),
2507
+ supabase
2508
+ .from('test_cases')
2509
+ .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2510
+ .eq('project_id', currentProjectId)
2511
+ .in('category', safeRoutes),
2512
+ ]);
2513
+ // Deduplicate by id
2514
+ const seenIds = new Set();
2515
+ const testCases = [...(byRoute || []), ...(byCategory || [])].filter(tc => {
2516
+ if (seenIds.has(tc.id))
2517
+ return false;
2518
+ seenIds.add(tc.id);
2519
+ return true;
2520
+ });
2132
2521
  // Get route stats for risk assessment
2133
2522
  const { data: routeStats } = await supabase
2134
2523
  .from('route_test_stats')
2135
2524
  .select('*')
2136
- .eq('project_id', PROJECT_ID)
2525
+ .eq('project_id', currentProjectId)
2137
2526
  .in('route', Array.from(allRoutes));
2138
2527
  const routeStatsMap = new Map((routeStats || []).map(r => [r.route, r]));
2139
2528
  // Categorize tests
@@ -2232,30 +2621,30 @@ async function getQAHealth(args) {
2232
2621
  const { data: currentTests } = await supabase
2233
2622
  .from('test_assignments')
2234
2623
  .select('id, status, completed_at')
2235
- .eq('project_id', PROJECT_ID)
2624
+ .eq('project_id', currentProjectId)
2236
2625
  .gte('completed_at', periodStart.toISOString())
2237
2626
  .in('status', ['passed', 'failed']);
2238
2627
  const { data: currentBugs } = await supabase
2239
2628
  .from('reports')
2240
2629
  .select('id, severity, status, created_at')
2241
- .eq('project_id', PROJECT_ID)
2630
+ .eq('project_id', currentProjectId)
2242
2631
  .eq('report_type', 'bug')
2243
2632
  .gte('created_at', periodStart.toISOString());
2244
2633
  const { data: resolvedBugs } = await supabase
2245
2634
  .from('reports')
2246
2635
  .select('id, created_at, resolved_at')
2247
- .eq('project_id', PROJECT_ID)
2636
+ .eq('project_id', currentProjectId)
2248
2637
  .eq('report_type', 'bug')
2249
2638
  .in('status', ['resolved', 'fixed', 'verified', 'closed'])
2250
2639
  .gte('resolved_at', periodStart.toISOString());
2251
2640
  const { data: testers } = await supabase
2252
2641
  .from('testers')
2253
2642
  .select('id, status')
2254
- .eq('project_id', PROJECT_ID);
2643
+ .eq('project_id', currentProjectId);
2255
2644
  const { data: routeStats } = await supabase
2256
2645
  .from('route_test_stats')
2257
2646
  .select('route, test_case_count')
2258
- .eq('project_id', PROJECT_ID);
2647
+ .eq('project_id', currentProjectId);
2259
2648
  // Get previous period data for comparison
2260
2649
  let previousTests = [];
2261
2650
  let previousBugs = [];
@@ -2264,7 +2653,7 @@ async function getQAHealth(args) {
2264
2653
  const { data: pt } = await supabase
2265
2654
  .from('test_assignments')
2266
2655
  .select('id, status')
2267
- .eq('project_id', PROJECT_ID)
2656
+ .eq('project_id', currentProjectId)
2268
2657
  .gte('completed_at', previousStart.toISOString())
2269
2658
  .lt('completed_at', periodStart.toISOString())
2270
2659
  .in('status', ['passed', 'failed']);
@@ -2272,7 +2661,7 @@ async function getQAHealth(args) {
2272
2661
  const { data: pb } = await supabase
2273
2662
  .from('reports')
2274
2663
  .select('id, severity')
2275
- .eq('project_id', PROJECT_ID)
2664
+ .eq('project_id', currentProjectId)
2276
2665
  .eq('report_type', 'bug')
2277
2666
  .gte('created_at', previousStart.toISOString())
2278
2667
  .lt('created_at', periodStart.toISOString());
@@ -2280,7 +2669,7 @@ async function getQAHealth(args) {
2280
2669
  const { data: pr } = await supabase
2281
2670
  .from('reports')
2282
2671
  .select('id')
2283
- .eq('project_id', PROJECT_ID)
2672
+ .eq('project_id', currentProjectId)
2284
2673
  .in('status', ['resolved', 'fixed', 'verified', 'closed'])
2285
2674
  .gte('resolved_at', previousStart.toISOString())
2286
2675
  .lt('resolved_at', periodStart.toISOString());
@@ -2434,7 +2823,7 @@ async function getQASessions(args) {
2434
2823
  findings_count, bugs_filed, created_at,
2435
2824
  tester:testers(id, name, email)
2436
2825
  `)
2437
- .eq('project_id', PROJECT_ID)
2826
+ .eq('project_id', currentProjectId)
2438
2827
  .order('started_at', { ascending: false })
2439
2828
  .limit(limit);
2440
2829
  if (status !== 'all') {
@@ -2484,12 +2873,12 @@ async function getQAAlerts(args) {
2484
2873
  const status = args.status || 'active';
2485
2874
  // Optionally refresh alerts
2486
2875
  if (args.refresh) {
2487
- await supabase.rpc('detect_all_alerts', { p_project_id: PROJECT_ID });
2876
+ await supabase.rpc('detect_all_alerts', { p_project_id: currentProjectId });
2488
2877
  }
2489
2878
  let query = supabase
2490
2879
  .from('qa_alerts')
2491
2880
  .select('*')
2492
- .eq('project_id', PROJECT_ID)
2881
+ .eq('project_id', currentProjectId)
2493
2882
  .order('severity', { ascending: true }) // critical first
2494
2883
  .order('created_at', { ascending: false });
2495
2884
  if (severity !== 'all') {
@@ -2542,7 +2931,7 @@ async function getDeploymentAnalysis(args) {
2542
2931
  .from('deployments')
2543
2932
  .select('*')
2544
2933
  .eq('id', args.deployment_id)
2545
- .eq('project_id', PROJECT_ID)
2934
+ .eq('project_id', currentProjectId)
2546
2935
  .single();
2547
2936
  if (error) {
2548
2937
  return { error: error.message };
@@ -2553,7 +2942,7 @@ async function getDeploymentAnalysis(args) {
2553
2942
  let query = supabase
2554
2943
  .from('deployments')
2555
2944
  .select('*')
2556
- .eq('project_id', PROJECT_ID)
2945
+ .eq('project_id', currentProjectId)
2557
2946
  .order('deployed_at', { ascending: false })
2558
2947
  .limit(limit);
2559
2948
  if (args.environment && args.environment !== 'all') {
@@ -2627,14 +3016,23 @@ async function analyzeCommitForTesting(args) {
2627
3016
  const { data: mappings } = await supabase
2628
3017
  .from('file_route_mapping')
2629
3018
  .select('file_pattern, route, feature, confidence')
2630
- .eq('project_id', PROJECT_ID);
3019
+ .eq('project_id', currentProjectId);
2631
3020
  const affectedRoutes = [];
2632
3021
  for (const mapping of mappings || []) {
2633
3022
  const matchedFiles = filesChanged.filter(file => {
2634
- const pattern = mapping.file_pattern
2635
- .replace(/\*\*/g, '.*')
2636
- .replace(/\*/g, '[^/]*');
2637
- return new RegExp(pattern).test(file);
3023
+ try {
3024
+ // Validate pattern complexity to prevent ReDoS
3025
+ if (mapping.file_pattern.length > 200)
3026
+ return false;
3027
+ const pattern = mapping.file_pattern
3028
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars first
3029
+ .replace(/\\\*\\\*/g, '.*') // Then convert glob ** to .*
3030
+ .replace(/\\\*/g, '[^/]*'); // And glob * to [^/]*
3031
+ return new RegExp(`^${pattern}$`).test(file);
3032
+ }
3033
+ catch {
3034
+ return false; // Skip malformed patterns
3035
+ }
2638
3036
  });
2639
3037
  if (matchedFiles.length > 0) {
2640
3038
  affectedRoutes.push({
@@ -2652,7 +3050,7 @@ async function analyzeCommitForTesting(args) {
2652
3050
  const { data: bugs } = await supabase
2653
3051
  .from('reports')
2654
3052
  .select('id, severity, description, route, created_at')
2655
- .eq('project_id', PROJECT_ID)
3053
+ .eq('project_id', currentProjectId)
2656
3054
  .eq('report_type', 'bug')
2657
3055
  .in('route', routes)
2658
3056
  .gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
@@ -2684,7 +3082,7 @@ async function analyzeCommitForTesting(args) {
2684
3082
  // Optionally record as deployment
2685
3083
  if (args.record_deployment) {
2686
3084
  await supabase.rpc('record_deployment', {
2687
- p_project_id: PROJECT_ID,
3085
+ p_project_id: currentProjectId,
2688
3086
  p_environment: 'production',
2689
3087
  p_commit_sha: args.commit_sha || null,
2690
3088
  p_commit_message: args.commit_message || null,
@@ -2773,12 +3171,12 @@ async function analyzeChangesForTests(args) {
2773
3171
  const { data: existingTests } = await supabase
2774
3172
  .from('test_cases')
2775
3173
  .select('test_key, title, target_route, description')
2776
- .eq('project_id', PROJECT_ID);
3174
+ .eq('project_id', currentProjectId);
2777
3175
  // Get next test key
2778
3176
  const { data: lastTest } = await supabase
2779
3177
  .from('test_cases')
2780
3178
  .select('test_key')
2781
- .eq('project_id', PROJECT_ID)
3179
+ .eq('project_id', currentProjectId)
2782
3180
  .order('test_key', { ascending: false })
2783
3181
  .limit(1);
2784
3182
  const lastKey = lastTest?.[0]?.test_key || 'TC-000';
@@ -2790,7 +3188,7 @@ async function analyzeChangesForTests(args) {
2790
3188
  const { data: bugs } = await supabase
2791
3189
  .from('reports')
2792
3190
  .select('id, description, severity, app_context')
2793
- .eq('project_id', PROJECT_ID)
3191
+ .eq('project_id', currentProjectId)
2794
3192
  .eq('report_type', 'bug')
2795
3193
  .limit(50);
2796
3194
  relatedBugs = (bugs || []).filter(bug => {
@@ -3135,7 +3533,7 @@ async function createBugReport(args) {
3135
3533
  const { data: project } = await supabase
3136
3534
  .from('projects')
3137
3535
  .select('owner_id')
3138
- .eq('id', PROJECT_ID)
3536
+ .eq('id', currentProjectId)
3139
3537
  .single();
3140
3538
  if (project?.owner_id) {
3141
3539
  reporterId = project.owner_id;
@@ -3145,14 +3543,14 @@ async function createBugReport(args) {
3145
3543
  const { data: testers } = await supabase
3146
3544
  .from('testers')
3147
3545
  .select('id')
3148
- .eq('project_id', PROJECT_ID)
3546
+ .eq('project_id', currentProjectId)
3149
3547
  .limit(1);
3150
3548
  if (testers && testers.length > 0) {
3151
3549
  reporterId = testers[0].id;
3152
3550
  }
3153
3551
  }
3154
3552
  const report = {
3155
- project_id: PROJECT_ID,
3553
+ project_id: currentProjectId,
3156
3554
  report_type: 'bug',
3157
3555
  title: args.title,
3158
3556
  description: args.description,
@@ -3216,7 +3614,7 @@ async function getBugsForFile(args) {
3216
3614
  let query = supabase
3217
3615
  .from('reports')
3218
3616
  .select('id, title, description, severity, status, created_at, code_context')
3219
- .eq('project_id', PROJECT_ID)
3617
+ .eq('project_id', currentProjectId)
3220
3618
  .eq('report_type', 'bug');
3221
3619
  if (!args.include_resolved) {
3222
3620
  query = query.in('status', ['new', 'confirmed', 'in_progress', 'reviewed']);
@@ -3282,7 +3680,7 @@ async function markFixedWithCommit(args) {
3282
3680
  .from('reports')
3283
3681
  .select('code_context')
3284
3682
  .eq('id', args.report_id)
3285
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
3683
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
3286
3684
  .single();
3287
3685
  if (fetchError) {
3288
3686
  return { error: fetchError.message };
@@ -3292,6 +3690,7 @@ async function markFixedWithCommit(args) {
3292
3690
  status: 'resolved',
3293
3691
  resolved_at: new Date().toISOString(),
3294
3692
  resolution_notes: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
3693
+ notify_tester: args.notify_tester === true, // Opt-in: only notify if explicitly requested
3295
3694
  code_context: {
3296
3695
  ...existingContext,
3297
3696
  fix: {
@@ -3307,15 +3706,19 @@ async function markFixedWithCommit(args) {
3307
3706
  .from('reports')
3308
3707
  .update(updates)
3309
3708
  .eq('id', args.report_id)
3310
- .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
3709
+ .eq('project_id', currentProjectId); // Security: ensure report belongs to this project
3311
3710
  if (error) {
3312
3711
  return { error: error.message };
3313
3712
  }
3713
+ const notificationStatus = args.notify_tester
3714
+ ? 'The original tester will be notified and assigned a verification task.'
3715
+ : 'No notification sent (silent resolve). A verification task was created.';
3314
3716
  return {
3315
3717
  success: true,
3316
- message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}`,
3718
+ message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}. ${notificationStatus}`,
3317
3719
  report_id: args.report_id,
3318
3720
  commit: args.commit_sha,
3721
+ tester_notified: args.notify_tester === true,
3319
3722
  next_steps: [
3320
3723
  'Consider running create_regression_test to prevent this bug from recurring',
3321
3724
  'Push your changes to trigger CI/CD',
@@ -3327,7 +3730,7 @@ async function getBugsAffectingCode(args) {
3327
3730
  const { data, error } = await supabase
3328
3731
  .from('reports')
3329
3732
  .select('id, title, description, severity, status, code_context, app_context')
3330
- .eq('project_id', PROJECT_ID)
3733
+ .eq('project_id', currentProjectId)
3331
3734
  .eq('report_type', 'bug')
3332
3735
  .in('status', ['new', 'confirmed', 'in_progress', 'reviewed'])
3333
3736
  .order('severity', { ascending: true });
@@ -3432,7 +3835,7 @@ async function linkBugToCode(args) {
3432
3835
  .from('reports')
3433
3836
  .select('code_context')
3434
3837
  .eq('id', args.report_id)
3435
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
3838
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
3436
3839
  .single();
3437
3840
  if (fetchError) {
3438
3841
  return { error: fetchError.message };
@@ -3453,7 +3856,7 @@ async function linkBugToCode(args) {
3453
3856
  .from('reports')
3454
3857
  .update(updates)
3455
3858
  .eq('id', args.report_id)
3456
- .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
3859
+ .eq('project_id', currentProjectId); // Security: ensure report belongs to this project
3457
3860
  if (error) {
3458
3861
  return { error: error.message };
3459
3862
  }
@@ -3472,7 +3875,7 @@ async function createRegressionTest(args) {
3472
3875
  .from('reports')
3473
3876
  .select('*')
3474
3877
  .eq('id', args.report_id)
3475
- .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
3878
+ .eq('project_id', currentProjectId) // Security: ensure report belongs to this project
3476
3879
  .single();
3477
3880
  if (fetchError) {
3478
3881
  return { error: fetchError.message };
@@ -3489,7 +3892,7 @@ async function createRegressionTest(args) {
3489
3892
  const { data: existingTests } = await supabase
3490
3893
  .from('test_cases')
3491
3894
  .select('test_key')
3492
- .eq('project_id', PROJECT_ID)
3895
+ .eq('project_id', currentProjectId)
3493
3896
  .order('test_key', { ascending: false })
3494
3897
  .limit(1);
3495
3898
  const lastKey = existingTests?.[0]?.test_key || 'TC-000';
@@ -3500,7 +3903,7 @@ async function createRegressionTest(args) {
3500
3903
  const targetRoute = appContext?.currentRoute;
3501
3904
  // Generate test case from bug
3502
3905
  const testCase = {
3503
- project_id: PROJECT_ID,
3906
+ project_id: currentProjectId,
3504
3907
  test_key: newKey,
3505
3908
  title: `Regression: ${report.title}`,
3506
3909
  description: `Regression test to prevent recurrence of bug #${args.report_id.slice(0, 8)}\n\nOriginal bug: ${report.description}`,
@@ -3576,7 +3979,7 @@ async function getPendingFixes(args) {
3576
3979
  created_at,
3577
3980
  report:reports(id, title, severity, description)
3578
3981
  `)
3579
- .eq('project_id', PROJECT_ID)
3982
+ .eq('project_id', currentProjectId)
3580
3983
  .order('created_at', { ascending: true })
3581
3984
  .limit(limit);
3582
3985
  if (!args.include_claimed) {
@@ -3626,7 +4029,7 @@ async function claimFixRequest(args) {
3626
4029
  .from('fix_requests')
3627
4030
  .select('id, status, claimed_by, prompt, title')
3628
4031
  .eq('id', args.fix_request_id)
3629
- .eq('project_id', PROJECT_ID) // Security: ensure fix request belongs to this project
4032
+ .eq('project_id', currentProjectId) // Security: ensure fix request belongs to this project
3630
4033
  .single();
3631
4034
  if (checkError) {
3632
4035
  return { error: checkError.message };
@@ -3653,7 +4056,7 @@ async function claimFixRequest(args) {
3653
4056
  claimed_by: claimedBy,
3654
4057
  })
3655
4058
  .eq('id', args.fix_request_id)
3656
- .eq('project_id', PROJECT_ID) // Security: ensure fix request belongs to this project
4059
+ .eq('project_id', currentProjectId) // Security: ensure fix request belongs to this project
3657
4060
  .eq('status', 'pending'); // Only claim if still pending (race condition protection)
3658
4061
  if (updateError) {
3659
4062
  return { error: updateError.message };
@@ -3688,7 +4091,7 @@ async function completeFixRequest(args) {
3688
4091
  .from('fix_requests')
3689
4092
  .update(updates)
3690
4093
  .eq('id', args.fix_request_id)
3691
- .eq('project_id', PROJECT_ID); // Security: ensure fix request belongs to this project
4094
+ .eq('project_id', currentProjectId); // Security: ensure fix request belongs to this project
3692
4095
  if (error) {
3693
4096
  return { error: error.message };
3694
4097
  }
@@ -3771,7 +4174,7 @@ async function generatePromptContent(name, args) {
3771
4174
  created_at,
3772
4175
  report:reports(id, title, severity)
3773
4176
  `)
3774
- .eq('project_id', PROJECT_ID)
4177
+ .eq('project_id', currentProjectId)
3775
4178
  .eq('status', 'pending')
3776
4179
  .order('created_at', { ascending: true })
3777
4180
  .limit(5);
@@ -3779,7 +4182,7 @@ async function generatePromptContent(name, args) {
3779
4182
  let query = supabase
3780
4183
  .from('reports')
3781
4184
  .select('id, title, description, severity, status, code_context, created_at')
3782
- .eq('project_id', PROJECT_ID)
4185
+ .eq('project_id', currentProjectId)
3783
4186
  .eq('report_type', 'bug')
3784
4187
  .in('status', ['new', 'confirmed', 'in_progress']);
3785
4188
  if (severity !== 'all') {
@@ -3929,7 +4332,7 @@ Would you like me to generate test cases for these files?`;
3929
4332
  const { data: resolvedBugs } = await supabase
3930
4333
  .from('reports')
3931
4334
  .select('id, title, description, severity, resolved_at, code_context')
3932
- .eq('project_id', PROJECT_ID)
4335
+ .eq('project_id', currentProjectId)
3933
4336
  .eq('report_type', 'bug')
3934
4337
  .eq('status', 'resolved')
3935
4338
  .order('resolved_at', { ascending: false })
@@ -4036,28 +4439,809 @@ Which files or areas would you like me to analyze?`;
4036
4439
  return 'Unknown prompt';
4037
4440
  }
4038
4441
  }
4039
- // Main server setup
4040
- async function main() {
4041
- initSupabase();
4042
- const server = new index_js_1.Server({
4043
- name: 'bugbear-mcp',
4044
- version: '0.1.0',
4045
- }, {
4046
- capabilities: {
4047
- tools: {},
4048
- resources: {},
4049
- prompts: {},
4050
- },
4051
- });
4052
- // Handle tool listing
4053
- server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
4054
- tools,
4442
+ // === TESTER & ASSIGNMENT MANAGEMENT HANDLERS ===
4443
+ async function listTesters(args) {
4444
+ let query = supabase
4445
+ .from('testers')
4446
+ .select('id, name, email, status, platforms, tier, assigned_count, completed_count, notes, created_at')
4447
+ .eq('project_id', currentProjectId)
4448
+ .order('name', { ascending: true });
4449
+ if (args.status) {
4450
+ query = query.eq('status', args.status);
4451
+ }
4452
+ const { data, error } = await query;
4453
+ if (error) {
4454
+ return { error: error.message };
4455
+ }
4456
+ let testers = data || [];
4457
+ // Filter by platform if specified (platforms is an array column)
4458
+ if (args.platform) {
4459
+ testers = testers.filter((t) => t.platforms && t.platforms.includes(args.platform));
4460
+ }
4461
+ return {
4462
+ count: testers.length,
4463
+ testers: testers.map((t) => ({
4464
+ id: t.id,
4465
+ name: t.name,
4466
+ email: t.email,
4467
+ status: t.status,
4468
+ platforms: t.platforms,
4469
+ tier: t.tier,
4470
+ assignedCount: t.assigned_count,
4471
+ completedCount: t.completed_count,
4472
+ notes: t.notes,
4473
+ })),
4474
+ };
4475
+ }
4476
+ async function listTestRuns(args) {
4477
+ const limit = Math.min(args.limit || 20, 50);
4478
+ let query = supabase
4479
+ .from('test_runs')
4480
+ .select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
4481
+ .eq('project_id', currentProjectId)
4482
+ .order('created_at', { ascending: false })
4483
+ .limit(limit);
4484
+ if (args.status) {
4485
+ query = query.eq('status', args.status);
4486
+ }
4487
+ const { data, error } = await query;
4488
+ if (error) {
4489
+ return { error: error.message };
4490
+ }
4491
+ return {
4492
+ count: (data || []).length,
4493
+ testRuns: (data || []).map((r) => ({
4494
+ id: r.id,
4495
+ name: r.name,
4496
+ description: r.description,
4497
+ status: r.status,
4498
+ totalTests: r.total_tests,
4499
+ passedTests: r.passed_tests,
4500
+ failedTests: r.failed_tests,
4501
+ passRate: r.total_tests > 0 ? Math.round((r.passed_tests / r.total_tests) * 100) : 0,
4502
+ startedAt: r.started_at,
4503
+ completedAt: r.completed_at,
4504
+ createdAt: r.created_at,
4505
+ })),
4506
+ };
4507
+ }
4508
+ async function createTestRun(args) {
4509
+ if (!args.name || args.name.trim().length === 0) {
4510
+ return { error: 'Test run name is required' };
4511
+ }
4512
+ const { data, error } = await supabase
4513
+ .from('test_runs')
4514
+ .insert({
4515
+ project_id: currentProjectId,
4516
+ name: args.name.trim(),
4517
+ description: args.description?.trim() || null,
4518
+ status: 'draft',
4519
+ })
4520
+ .select('id, name, description, status, created_at')
4521
+ .single();
4522
+ if (error) {
4523
+ return { error: error.message };
4524
+ }
4525
+ return {
4526
+ success: true,
4527
+ testRun: {
4528
+ id: data.id,
4529
+ name: data.name,
4530
+ description: data.description,
4531
+ status: data.status,
4532
+ createdAt: data.created_at,
4533
+ },
4534
+ message: `Test run "${data.name}" created. Use assign_tests to add test assignments.`,
4535
+ };
4536
+ }
4537
+ async function listTestAssignments(args) {
4538
+ const limit = Math.min(args.limit || 50, 200);
4539
+ if (args.tester_id && !isValidUUID(args.tester_id)) {
4540
+ return { error: 'Invalid tester_id format' };
4541
+ }
4542
+ if (args.test_run_id && !isValidUUID(args.test_run_id)) {
4543
+ return { error: 'Invalid test_run_id format' };
4544
+ }
4545
+ let query = supabase
4546
+ .from('test_assignments')
4547
+ .select(`
4548
+ id,
4549
+ status,
4550
+ assigned_at,
4551
+ started_at,
4552
+ completed_at,
4553
+ duration_seconds,
4554
+ is_verification,
4555
+ notes,
4556
+ test_case:test_cases(id, test_key, title, priority, target_route),
4557
+ tester:testers(id, name, email),
4558
+ test_run:test_runs(id, name)
4559
+ `)
4560
+ .eq('project_id', currentProjectId)
4561
+ .order('assigned_at', { ascending: false })
4562
+ .limit(limit);
4563
+ if (args.tester_id) {
4564
+ query = query.eq('tester_id', args.tester_id);
4565
+ }
4566
+ if (args.test_run_id) {
4567
+ query = query.eq('test_run_id', args.test_run_id);
4568
+ }
4569
+ if (args.status) {
4570
+ query = query.eq('status', args.status);
4571
+ }
4572
+ const { data, error } = await query;
4573
+ if (error) {
4574
+ return { error: error.message };
4575
+ }
4576
+ return {
4577
+ count: (data || []).length,
4578
+ assignments: (data || []).map((a) => ({
4579
+ id: a.id,
4580
+ status: a.status,
4581
+ assignedAt: a.assigned_at,
4582
+ startedAt: a.started_at,
4583
+ completedAt: a.completed_at,
4584
+ durationSeconds: a.duration_seconds,
4585
+ isVerification: a.is_verification,
4586
+ notes: a.notes,
4587
+ testCase: a.test_case ? {
4588
+ id: a.test_case.id,
4589
+ testKey: a.test_case.test_key,
4590
+ title: a.test_case.title,
4591
+ priority: a.test_case.priority,
4592
+ targetRoute: a.test_case.target_route,
4593
+ } : null,
4594
+ tester: a.tester ? {
4595
+ id: a.tester.id,
4596
+ name: a.tester.name,
4597
+ email: a.tester.email,
4598
+ } : null,
4599
+ testRun: a.test_run ? {
4600
+ id: a.test_run.id,
4601
+ name: a.test_run.name,
4602
+ } : null,
4603
+ })),
4604
+ };
4605
+ }
4606
+ async function assignTests(args) {
4607
+ // Validate inputs
4608
+ if (!isValidUUID(args.tester_id)) {
4609
+ return { error: 'Invalid tester_id format' };
4610
+ }
4611
+ if (!args.test_case_ids || args.test_case_ids.length === 0) {
4612
+ return { error: 'At least one test_case_id is required' };
4613
+ }
4614
+ if (args.test_case_ids.length > 50) {
4615
+ return { error: 'Maximum 50 test cases per assignment batch' };
4616
+ }
4617
+ for (const id of args.test_case_ids) {
4618
+ if (!isValidUUID(id)) {
4619
+ return { error: `Invalid test_case_id format: ${id}` };
4620
+ }
4621
+ }
4622
+ if (args.test_run_id && !isValidUUID(args.test_run_id)) {
4623
+ return { error: 'Invalid test_run_id format' };
4624
+ }
4625
+ // Verify tester exists and is active
4626
+ const { data: tester, error: testerErr } = await supabase
4627
+ .from('testers')
4628
+ .select('id, name, email, status')
4629
+ .eq('id', args.tester_id)
4630
+ .eq('project_id', currentProjectId)
4631
+ .single();
4632
+ if (testerErr || !tester) {
4633
+ return { error: 'Tester not found in this project' };
4634
+ }
4635
+ if (tester.status !== 'active') {
4636
+ return { error: `Tester "${tester.name}" is ${tester.status}, not active` };
4637
+ }
4638
+ // Verify test cases exist for this project
4639
+ const { data: testCases, error: tcErr } = await supabase
4640
+ .from('test_cases')
4641
+ .select('id, test_key, title')
4642
+ .eq('project_id', currentProjectId)
4643
+ .in('id', args.test_case_ids);
4644
+ if (tcErr) {
4645
+ return { error: tcErr.message };
4646
+ }
4647
+ const foundIds = new Set((testCases || []).map((tc) => tc.id));
4648
+ const missingIds = args.test_case_ids.filter(id => !foundIds.has(id));
4649
+ if (missingIds.length > 0) {
4650
+ return {
4651
+ error: `Test cases not found in this project: ${missingIds.join(', ')}`,
4652
+ };
4653
+ }
4654
+ // Verify test run exists if provided
4655
+ if (args.test_run_id) {
4656
+ const { data: run, error: runErr } = await supabase
4657
+ .from('test_runs')
4658
+ .select('id')
4659
+ .eq('id', args.test_run_id)
4660
+ .eq('project_id', currentProjectId)
4661
+ .single();
4662
+ if (runErr || !run) {
4663
+ return { error: 'Test run not found in this project' };
4664
+ }
4665
+ }
4666
+ // Build assignment rows
4667
+ const rows = args.test_case_ids.map(tcId => ({
4668
+ project_id: currentProjectId,
4669
+ test_case_id: tcId,
4670
+ tester_id: args.tester_id,
4671
+ test_run_id: args.test_run_id || null,
4672
+ status: 'pending',
4673
+ }));
4674
+ // Insert — use upsert-like approach: insert and handle conflicts
4675
+ const { data: inserted, error: insertErr } = await supabase
4676
+ .from('test_assignments')
4677
+ .insert(rows)
4678
+ .select('id, test_case_id');
4679
+ if (insertErr) {
4680
+ // Check if it's a unique constraint violation
4681
+ if (insertErr.message.includes('duplicate') || insertErr.message.includes('unique')) {
4682
+ // Try inserting one by one to find duplicates
4683
+ const created = [];
4684
+ const skipped = [];
4685
+ for (const row of rows) {
4686
+ const { data: single, error: singleErr } = await supabase
4687
+ .from('test_assignments')
4688
+ .insert(row)
4689
+ .select('id, test_case_id')
4690
+ .single();
4691
+ if (singleErr) {
4692
+ const tc = testCases?.find((t) => t.id === row.test_case_id);
4693
+ skipped.push(tc?.test_key || row.test_case_id);
4694
+ }
4695
+ else if (single) {
4696
+ created.push(single);
4697
+ }
4698
+ }
4699
+ return {
4700
+ success: true,
4701
+ created: created.length,
4702
+ skipped: skipped.length,
4703
+ skippedTests: skipped,
4704
+ tester: { id: tester.id, name: tester.name },
4705
+ message: `Assigned ${created.length} test(s) to ${tester.name}. ${skipped.length} skipped (already assigned).`,
4706
+ };
4707
+ }
4708
+ return { error: insertErr.message };
4709
+ }
4710
+ return {
4711
+ success: true,
4712
+ created: (inserted || []).length,
4713
+ skipped: 0,
4714
+ tester: { id: tester.id, name: tester.name },
4715
+ testRun: args.test_run_id || null,
4716
+ message: `Assigned ${(inserted || []).length} test(s) to ${tester.name}.`,
4717
+ };
4718
+ }
4719
+ async function getTesterWorkload(args) {
4720
+ if (!isValidUUID(args.tester_id)) {
4721
+ return { error: 'Invalid tester_id format' };
4722
+ }
4723
+ // Get tester info
4724
+ const { data: tester, error: testerErr } = await supabase
4725
+ .from('testers')
4726
+ .select('id, name, email, status, platforms, tier')
4727
+ .eq('id', args.tester_id)
4728
+ .eq('project_id', currentProjectId)
4729
+ .single();
4730
+ if (testerErr || !tester) {
4731
+ return { error: 'Tester not found in this project' };
4732
+ }
4733
+ // Get all assignments for this tester in this project
4734
+ const { data: assignments, error: assignErr } = await supabase
4735
+ .from('test_assignments')
4736
+ .select(`
4737
+ id,
4738
+ status,
4739
+ assigned_at,
4740
+ completed_at,
4741
+ test_case:test_cases(test_key, title, priority),
4742
+ test_run:test_runs(name)
4743
+ `)
4744
+ .eq('project_id', currentProjectId)
4745
+ .eq('tester_id', args.tester_id)
4746
+ .order('assigned_at', { ascending: false });
4747
+ if (assignErr) {
4748
+ return { error: assignErr.message };
4749
+ }
4750
+ const all = assignments || [];
4751
+ // Count by status
4752
+ const counts = {
4753
+ pending: 0,
4754
+ in_progress: 0,
4755
+ passed: 0,
4756
+ failed: 0,
4757
+ blocked: 0,
4758
+ skipped: 0,
4759
+ };
4760
+ for (const a of all) {
4761
+ counts[a.status] = (counts[a.status] || 0) + 1;
4762
+ }
4763
+ return {
4764
+ tester: {
4765
+ id: tester.id,
4766
+ name: tester.name,
4767
+ email: tester.email,
4768
+ status: tester.status,
4769
+ platforms: tester.platforms,
4770
+ tier: tester.tier,
4771
+ },
4772
+ totalAssignments: all.length,
4773
+ counts,
4774
+ activeLoad: counts.pending + counts.in_progress,
4775
+ recentAssignments: all.slice(0, 10).map((a) => ({
4776
+ id: a.id,
4777
+ status: a.status,
4778
+ assignedAt: a.assigned_at,
4779
+ completedAt: a.completed_at,
4780
+ testCase: a.test_case ? {
4781
+ testKey: a.test_case.test_key,
4782
+ title: a.test_case.title,
4783
+ priority: a.test_case.priority,
4784
+ } : null,
4785
+ testRun: a.test_run?.name || null,
4786
+ })),
4787
+ };
4788
+ }
4789
+ // === NEW TESTER & ANALYTICS HANDLERS ===
4790
+ async function createTester(args) {
4791
+ if (!args.name || args.name.trim().length === 0) {
4792
+ return { error: 'Tester name is required' };
4793
+ }
4794
+ if (!args.email || !args.email.includes('@')) {
4795
+ return { error: 'A valid email address is required' };
4796
+ }
4797
+ if (args.tier !== undefined && (args.tier < 1 || args.tier > 3)) {
4798
+ return { error: 'Tier must be 1, 2, or 3' };
4799
+ }
4800
+ const validPlatforms = ['ios', 'android', 'web'];
4801
+ if (args.platforms) {
4802
+ for (const p of args.platforms) {
4803
+ if (!validPlatforms.includes(p)) {
4804
+ return { error: `Invalid platform "${p}". Must be one of: ${validPlatforms.join(', ')}` };
4805
+ }
4806
+ }
4807
+ }
4808
+ const { data, error } = await supabase
4809
+ .from('testers')
4810
+ .insert({
4811
+ project_id: currentProjectId,
4812
+ name: args.name.trim(),
4813
+ email: args.email.trim().toLowerCase(),
4814
+ platforms: args.platforms || ['ios', 'web'],
4815
+ tier: args.tier ?? 1,
4816
+ notes: args.notes?.trim() || null,
4817
+ status: 'active',
4818
+ })
4819
+ .select('id, name, email, status, platforms, tier, notes, created_at')
4820
+ .single();
4821
+ if (error) {
4822
+ if (error.message.includes('duplicate') || error.message.includes('unique')) {
4823
+ return { error: `A tester with email "${args.email}" already exists in this project` };
4824
+ }
4825
+ return { error: error.message };
4826
+ }
4827
+ return {
4828
+ success: true,
4829
+ tester: {
4830
+ id: data.id,
4831
+ name: data.name,
4832
+ email: data.email,
4833
+ status: data.status,
4834
+ platforms: data.platforms,
4835
+ tier: data.tier,
4836
+ notes: data.notes,
4837
+ createdAt: data.created_at,
4838
+ },
4839
+ message: `Tester "${data.name}" added to the project. Use assign_tests to give them test cases.`,
4840
+ };
4841
+ }
4842
+ async function updateTester(args) {
4843
+ if (!isValidUUID(args.tester_id)) {
4844
+ return { error: 'Invalid tester_id format' };
4845
+ }
4846
+ const updates = {};
4847
+ if (args.status)
4848
+ updates.status = args.status;
4849
+ if (args.platforms)
4850
+ updates.platforms = args.platforms;
4851
+ if (args.tier !== undefined) {
4852
+ if (args.tier < 1 || args.tier > 3) {
4853
+ return { error: 'Tier must be 1, 2, or 3' };
4854
+ }
4855
+ updates.tier = args.tier;
4856
+ }
4857
+ if (args.notes !== undefined)
4858
+ updates.notes = args.notes.trim() || null;
4859
+ if (args.name)
4860
+ updates.name = args.name.trim();
4861
+ if (Object.keys(updates).length === 0) {
4862
+ return { error: 'No fields to update. Provide at least one of: status, platforms, tier, notes, name' };
4863
+ }
4864
+ const { data, error } = await supabase
4865
+ .from('testers')
4866
+ .update(updates)
4867
+ .eq('id', args.tester_id)
4868
+ .eq('project_id', currentProjectId)
4869
+ .select('id, name, email, status, platforms, tier, notes')
4870
+ .single();
4871
+ if (error) {
4872
+ return { error: error.message };
4873
+ }
4874
+ if (!data) {
4875
+ return { error: 'Tester not found in this project' };
4876
+ }
4877
+ return {
4878
+ success: true,
4879
+ tester: {
4880
+ id: data.id,
4881
+ name: data.name,
4882
+ email: data.email,
4883
+ status: data.status,
4884
+ platforms: data.platforms,
4885
+ tier: data.tier,
4886
+ notes: data.notes,
4887
+ },
4888
+ updatedFields: Object.keys(updates),
4889
+ };
4890
+ }
4891
+ async function bulkUpdateReports(args) {
4892
+ if (!args.report_ids || args.report_ids.length === 0) {
4893
+ return { error: 'At least one report_id is required' };
4894
+ }
4895
+ if (args.report_ids.length > 50) {
4896
+ return { error: 'Maximum 50 reports per bulk update' };
4897
+ }
4898
+ for (const id of args.report_ids) {
4899
+ if (!isValidUUID(id)) {
4900
+ return { error: `Invalid report_id format: ${id}` };
4901
+ }
4902
+ }
4903
+ const updates = { status: args.status };
4904
+ if (args.resolution_notes) {
4905
+ updates.resolution_notes = args.resolution_notes;
4906
+ }
4907
+ // Set resolved_at timestamp for terminal statuses
4908
+ if (['fixed', 'resolved', 'verified', 'wont_fix', 'duplicate', 'closed'].includes(args.status)) {
4909
+ updates.resolved_at = new Date().toISOString();
4910
+ }
4911
+ const { data, error } = await supabase
4912
+ .from('reports')
4913
+ .update(updates)
4914
+ .eq('project_id', currentProjectId)
4915
+ .in('id', args.report_ids)
4916
+ .select('id, status, description');
4917
+ if (error) {
4918
+ return { error: error.message };
4919
+ }
4920
+ const updated = data || [];
4921
+ const updatedIds = new Set(updated.map((r) => r.id));
4922
+ const notFound = args.report_ids.filter(id => !updatedIds.has(id));
4923
+ return {
4924
+ success: true,
4925
+ updatedCount: updated.length,
4926
+ requestedCount: args.report_ids.length,
4927
+ notFound: notFound.length > 0 ? notFound : undefined,
4928
+ status: args.status,
4929
+ reports: updated.map((r) => ({
4930
+ id: r.id,
4931
+ status: r.status,
4932
+ description: r.description?.slice(0, 80),
4933
+ })),
4934
+ message: `Updated ${updated.length} report(s) to "${args.status}".${notFound.length > 0 ? ` ${notFound.length} report(s) not found.` : ''}`,
4935
+ };
4936
+ }
4937
+ async function getBugTrends(args) {
4938
+ const days = Math.min(args.days || 30, 180);
4939
+ const groupBy = args.group_by || 'week';
4940
+ const since = new Date(Date.now() - days * 86400000).toISOString();
4941
+ const { data, error } = await supabase
4942
+ .from('reports')
4943
+ .select('id, severity, category, status, report_type, created_at')
4944
+ .eq('project_id', currentProjectId)
4945
+ .gte('created_at', since)
4946
+ .order('created_at', { ascending: true });
4947
+ if (error) {
4948
+ return { error: error.message };
4949
+ }
4950
+ const reports = data || [];
4951
+ if (groupBy === 'week') {
4952
+ const weeks = {};
4953
+ for (const r of reports) {
4954
+ const d = new Date(r.created_at);
4955
+ // Get Monday of that week
4956
+ const day = d.getDay();
4957
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1);
4958
+ const monday = new Date(d.setDate(diff));
4959
+ const weekKey = monday.toISOString().slice(0, 10);
4960
+ if (!weeks[weekKey])
4961
+ weeks[weekKey] = { count: 0, critical: 0, high: 0, medium: 0, low: 0 };
4962
+ weeks[weekKey].count++;
4963
+ const sev = (r.severity || 'low');
4964
+ weeks[weekKey][sev]++;
4965
+ }
4966
+ return {
4967
+ period: `${days} days`,
4968
+ groupBy: 'week',
4969
+ totalReports: reports.length,
4970
+ weeks: Object.entries(weeks).map(([week, data]) => ({ week, ...data })),
4971
+ };
4972
+ }
4973
+ if (groupBy === 'severity') {
4974
+ const groups = { critical: 0, high: 0, medium: 0, low: 0 };
4975
+ for (const r of reports)
4976
+ groups[r.severity || 'low']++;
4977
+ return { period: `${days} days`, groupBy: 'severity', totalReports: reports.length, breakdown: groups };
4978
+ }
4979
+ if (groupBy === 'category') {
4980
+ const groups = {};
4981
+ for (const r of reports) {
4982
+ const cat = r.category || 'uncategorized';
4983
+ groups[cat] = (groups[cat] || 0) + 1;
4984
+ }
4985
+ return { period: `${days} days`, groupBy: 'category', totalReports: reports.length, breakdown: groups };
4986
+ }
4987
+ if (groupBy === 'status') {
4988
+ const groups = {};
4989
+ for (const r of reports) {
4990
+ groups[r.status] = (groups[r.status] || 0) + 1;
4991
+ }
4992
+ return { period: `${days} days`, groupBy: 'status', totalReports: reports.length, breakdown: groups };
4993
+ }
4994
+ return { error: `Invalid group_by: ${groupBy}. Must be one of: week, severity, category, status` };
4995
+ }
4996
+ async function getTesterLeaderboard(args) {
4997
+ const days = Math.min(args.days || 30, 180);
4998
+ const sortBy = args.sort_by || 'tests_completed';
4999
+ const since = new Date(Date.now() - days * 86400000).toISOString();
5000
+ // Get all testers for the project
5001
+ const { data: testers, error: testerErr } = await supabase
5002
+ .from('testers')
5003
+ .select('id, name, email, status, platforms, tier')
5004
+ .eq('project_id', currentProjectId)
5005
+ .eq('status', 'active');
5006
+ if (testerErr)
5007
+ return { error: testerErr.message };
5008
+ // Get completed assignments in the period
5009
+ const { data: assignments, error: assignErr } = await supabase
5010
+ .from('test_assignments')
5011
+ .select('tester_id, status, completed_at, duration_seconds')
5012
+ .eq('project_id', currentProjectId)
5013
+ .gte('completed_at', since)
5014
+ .in('status', ['passed', 'failed']);
5015
+ if (assignErr)
5016
+ return { error: assignErr.message };
5017
+ // Get bugs filed in the period
5018
+ const { data: bugs, error: bugErr } = await supabase
5019
+ .from('reports')
5020
+ .select('tester_id, severity')
5021
+ .eq('project_id', currentProjectId)
5022
+ .gte('created_at', since)
5023
+ .not('tester_id', 'is', null);
5024
+ if (bugErr)
5025
+ return { error: bugErr.message };
5026
+ // Aggregate per tester
5027
+ const testerMap = new Map();
5028
+ for (const t of testers || []) {
5029
+ testerMap.set(t.id, {
5030
+ id: t.id,
5031
+ name: t.name,
5032
+ email: t.email,
5033
+ tier: t.tier,
5034
+ testsCompleted: 0,
5035
+ testsPassed: 0,
5036
+ testsFailed: 0,
5037
+ bugsFound: 0,
5038
+ criticalBugs: 0,
5039
+ avgDurationSeconds: 0,
5040
+ totalDuration: 0,
5041
+ });
5042
+ }
5043
+ for (const a of assignments || []) {
5044
+ const entry = testerMap.get(a.tester_id);
5045
+ if (!entry)
5046
+ continue;
5047
+ entry.testsCompleted++;
5048
+ if (a.status === 'passed')
5049
+ entry.testsPassed++;
5050
+ if (a.status === 'failed')
5051
+ entry.testsFailed++;
5052
+ if (a.duration_seconds)
5053
+ entry.totalDuration += a.duration_seconds;
5054
+ }
5055
+ for (const b of bugs || []) {
5056
+ const entry = testerMap.get(b.tester_id);
5057
+ if (!entry)
5058
+ continue;
5059
+ entry.bugsFound++;
5060
+ if (b.severity === 'critical')
5061
+ entry.criticalBugs++;
5062
+ }
5063
+ let leaderboard = Array.from(testerMap.values()).map(t => ({
5064
+ ...t,
5065
+ passRate: t.testsCompleted > 0 ? Math.round((t.testsPassed / t.testsCompleted) * 100) : 0,
5066
+ avgDurationSeconds: t.testsCompleted > 0 ? Math.round(t.totalDuration / t.testsCompleted) : 0,
5067
+ totalDuration: undefined,
5068
+ }));
5069
+ // Sort
5070
+ if (sortBy === 'bugs_found') {
5071
+ leaderboard.sort((a, b) => b.bugsFound - a.bugsFound);
5072
+ }
5073
+ else if (sortBy === 'pass_rate') {
5074
+ leaderboard.sort((a, b) => b.passRate - a.passRate);
5075
+ }
5076
+ else {
5077
+ leaderboard.sort((a, b) => b.testsCompleted - a.testsCompleted);
5078
+ }
5079
+ return {
5080
+ period: `${days} days`,
5081
+ sortedBy: sortBy,
5082
+ leaderboard,
5083
+ };
5084
+ }
5085
+ async function exportTestResults(args) {
5086
+ if (!isValidUUID(args.test_run_id)) {
5087
+ return { error: 'Invalid test_run_id format' };
5088
+ }
5089
+ // Get the test run
5090
+ const { data: run, error: runErr } = await supabase
5091
+ .from('test_runs')
5092
+ .select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
5093
+ .eq('id', args.test_run_id)
5094
+ .eq('project_id', currentProjectId)
5095
+ .single();
5096
+ if (runErr || !run) {
5097
+ return { error: 'Test run not found in this project' };
5098
+ }
5099
+ // Get all assignments for this run
5100
+ const { data: assignments, error: assignErr } = await supabase
5101
+ .from('test_assignments')
5102
+ .select(`
5103
+ id,
5104
+ status,
5105
+ assigned_at,
5106
+ started_at,
5107
+ completed_at,
5108
+ duration_seconds,
5109
+ is_verification,
5110
+ notes,
5111
+ skip_reason,
5112
+ test_result,
5113
+ feedback_rating,
5114
+ feedback_note,
5115
+ test_case:test_cases(id, test_key, title, priority, description, target_route),
5116
+ tester:testers(id, name, email)
5117
+ `)
5118
+ .eq('test_run_id', args.test_run_id)
5119
+ .eq('project_id', currentProjectId)
5120
+ .order('assigned_at', { ascending: true });
5121
+ if (assignErr) {
5122
+ return { error: assignErr.message };
5123
+ }
5124
+ const all = assignments || [];
5125
+ const passCount = all.filter(a => a.status === 'passed').length;
5126
+ const failCount = all.filter(a => a.status === 'failed').length;
5127
+ return {
5128
+ testRun: {
5129
+ id: run.id,
5130
+ name: run.name,
5131
+ description: run.description,
5132
+ status: run.status,
5133
+ startedAt: run.started_at,
5134
+ completedAt: run.completed_at,
5135
+ createdAt: run.created_at,
5136
+ },
5137
+ summary: {
5138
+ totalAssignments: all.length,
5139
+ passed: passCount,
5140
+ failed: failCount,
5141
+ blocked: all.filter(a => a.status === 'blocked').length,
5142
+ skipped: all.filter(a => a.status === 'skipped').length,
5143
+ pending: all.filter(a => a.status === 'pending').length,
5144
+ inProgress: all.filter(a => a.status === 'in_progress').length,
5145
+ passRate: all.length > 0 ? Math.round((passCount / all.length) * 100) : 0,
5146
+ },
5147
+ assignments: all.map((a) => ({
5148
+ id: a.id,
5149
+ status: a.status,
5150
+ assignedAt: a.assigned_at,
5151
+ startedAt: a.started_at,
5152
+ completedAt: a.completed_at,
5153
+ durationSeconds: a.duration_seconds,
5154
+ isVerification: a.is_verification,
5155
+ notes: a.notes,
5156
+ skipReason: a.skip_reason,
5157
+ testResult: a.test_result,
5158
+ feedbackRating: a.feedback_rating,
5159
+ feedbackNote: a.feedback_note,
5160
+ testCase: a.test_case ? {
5161
+ id: a.test_case.id,
5162
+ testKey: a.test_case.test_key,
5163
+ title: a.test_case.title,
5164
+ priority: a.test_case.priority,
5165
+ description: a.test_case.description,
5166
+ targetRoute: a.test_case.target_route,
5167
+ } : null,
5168
+ tester: a.tester ? {
5169
+ id: a.tester.id,
5170
+ name: a.tester.name,
5171
+ email: a.tester.email,
5172
+ } : null,
5173
+ })),
5174
+ };
5175
+ }
5176
+ async function getTestingVelocity(args) {
5177
+ const days = Math.min(args.days || 14, 90);
5178
+ const since = new Date(Date.now() - days * 86400000).toISOString();
5179
+ const { data, error } = await supabase
5180
+ .from('test_assignments')
5181
+ .select('completed_at, status')
5182
+ .eq('project_id', currentProjectId)
5183
+ .gte('completed_at', since)
5184
+ .in('status', ['passed', 'failed'])
5185
+ .order('completed_at', { ascending: true });
5186
+ if (error) {
5187
+ return { error: error.message };
5188
+ }
5189
+ const completions = data || [];
5190
+ // Group by day
5191
+ const dailyCounts = {};
5192
+ for (let i = 0; i < days; i++) {
5193
+ const d = new Date(Date.now() - (days - 1 - i) * 86400000);
5194
+ dailyCounts[d.toISOString().slice(0, 10)] = 0;
5195
+ }
5196
+ for (const c of completions) {
5197
+ const day = new Date(c.completed_at).toISOString().slice(0, 10);
5198
+ if (dailyCounts[day] !== undefined) {
5199
+ dailyCounts[day]++;
5200
+ }
5201
+ }
5202
+ const dailyArray = Object.entries(dailyCounts).map(([date, count]) => ({ date, count }));
5203
+ const totalCompleted = completions.length;
5204
+ const avgPerDay = days > 0 ? Math.round((totalCompleted / days) * 10) / 10 : 0;
5205
+ // Trend: compare first half to second half
5206
+ const mid = Math.floor(dailyArray.length / 2);
5207
+ const firstHalf = dailyArray.slice(0, mid).reduce((sum, d) => sum + d.count, 0);
5208
+ const secondHalf = dailyArray.slice(mid).reduce((sum, d) => sum + d.count, 0);
5209
+ const trend = secondHalf > firstHalf ? 'increasing' : secondHalf < firstHalf ? 'decreasing' : 'stable';
5210
+ return {
5211
+ period: `${days} days`,
5212
+ totalCompleted,
5213
+ averagePerDay: avgPerDay,
5214
+ trend,
5215
+ daily: dailyArray,
5216
+ };
5217
+ }
5218
+ // Main server setup
5219
+ async function main() {
5220
+ initSupabase();
5221
+ const server = new index_js_1.Server({
5222
+ name: 'bugbear-mcp',
5223
+ version: '0.1.0',
5224
+ }, {
5225
+ capabilities: {
5226
+ tools: {},
5227
+ resources: {},
5228
+ prompts: {},
5229
+ },
5230
+ });
5231
+ // Handle tool listing
5232
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
5233
+ tools,
4055
5234
  }));
4056
5235
  // Handle tool execution
4057
5236
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
4058
5237
  const { name, arguments: args } = request.params;
4059
5238
  try {
4060
5239
  let result;
5240
+ // Project management tools don't require a project to be selected
5241
+ const projectFreeTools = ['list_projects', 'switch_project', 'get_current_project'];
5242
+ if (!projectFreeTools.includes(name)) {
5243
+ requireProject();
5244
+ }
4061
5245
  switch (name) {
4062
5246
  case 'list_reports':
4063
5247
  result = await listReports(args);
@@ -4171,6 +5355,57 @@ async function main() {
4171
5355
  case 'get_testing_patterns':
4172
5356
  result = await getTestingPatterns(args);
4173
5357
  break;
5358
+ // === TESTER & ASSIGNMENT MANAGEMENT ===
5359
+ case 'list_testers':
5360
+ result = await listTesters(args);
5361
+ break;
5362
+ case 'list_test_runs':
5363
+ result = await listTestRuns(args);
5364
+ break;
5365
+ case 'create_test_run':
5366
+ result = await createTestRun(args);
5367
+ break;
5368
+ case 'list_test_assignments':
5369
+ result = await listTestAssignments(args);
5370
+ break;
5371
+ case 'assign_tests':
5372
+ result = await assignTests(args);
5373
+ break;
5374
+ case 'get_tester_workload':
5375
+ result = await getTesterWorkload(args);
5376
+ break;
5377
+ // === NEW TESTER & ANALYTICS TOOLS ===
5378
+ case 'create_tester':
5379
+ result = await createTester(args);
5380
+ break;
5381
+ case 'update_tester':
5382
+ result = await updateTester(args);
5383
+ break;
5384
+ case 'bulk_update_reports':
5385
+ result = await bulkUpdateReports(args);
5386
+ break;
5387
+ case 'get_bug_trends':
5388
+ result = await getBugTrends(args);
5389
+ break;
5390
+ case 'get_tester_leaderboard':
5391
+ result = await getTesterLeaderboard(args);
5392
+ break;
5393
+ case 'export_test_results':
5394
+ result = await exportTestResults(args);
5395
+ break;
5396
+ case 'get_testing_velocity':
5397
+ result = await getTestingVelocity(args);
5398
+ break;
5399
+ // === PROJECT MANAGEMENT ===
5400
+ case 'list_projects':
5401
+ result = await listProjects();
5402
+ break;
5403
+ case 'switch_project':
5404
+ result = await switchProject(args);
5405
+ break;
5406
+ case 'get_current_project':
5407
+ result = getCurrentProject();
5408
+ break;
4174
5409
  default:
4175
5410
  return {
4176
5411
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
@@ -4190,13 +5425,17 @@ async function main() {
4190
5425
  });
4191
5426
  // Handle resource listing (reports as resources)
4192
5427
  server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
4193
- const { data } = await supabase
5428
+ const { data, error } = await supabase
4194
5429
  .from('reports')
4195
5430
  .select('id, description, report_type, severity')
4196
- .eq('project_id', PROJECT_ID)
5431
+ .eq('project_id', currentProjectId)
4197
5432
  .eq('status', 'new')
4198
5433
  .order('created_at', { ascending: false })
4199
5434
  .limit(10);
5435
+ if (error) {
5436
+ console.error('Failed to list resources:', error.message);
5437
+ return { resources: [] };
5438
+ }
4200
5439
  return {
4201
5440
  resources: (data || []).map(r => ({
4202
5441
  uri: `bugbear://reports/${r.id}`,