@bbearai/mcp-server 0.5.0 → 0.5.1

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 +560 -25
  2. package/package.json +1 -1
  3. package/src/index.ts +632 -25
package/src/index.ts CHANGED
@@ -452,6 +452,10 @@ const tools = [
452
452
  items: { type: 'string' },
453
453
  description: 'List of files that were modified to fix this bug',
454
454
  },
455
+ notify_tester: {
456
+ type: 'boolean',
457
+ description: 'If true, notify the original tester about the fix with a message and verification task. Default: false (silent resolve).',
458
+ },
455
459
  },
456
460
  required: ['report_id', 'commit_sha'],
457
461
  },
@@ -919,6 +923,125 @@ const tools = [
919
923
  },
920
924
  },
921
925
  },
926
+ // === TESTER & ASSIGNMENT MANAGEMENT TOOLS ===
927
+ {
928
+ name: 'list_testers',
929
+ description: 'List all QA testers for the project with their status, platforms, and workload counts.',
930
+ inputSchema: {
931
+ type: 'object' as const,
932
+ properties: {
933
+ status: {
934
+ type: 'string',
935
+ enum: ['active', 'inactive', 'invited'],
936
+ description: 'Filter by tester status (default: all)',
937
+ },
938
+ platform: {
939
+ type: 'string',
940
+ enum: ['ios', 'android', 'web'],
941
+ description: 'Filter by platform support',
942
+ },
943
+ },
944
+ },
945
+ },
946
+ {
947
+ name: 'list_test_runs',
948
+ description: 'List testing campaigns (test runs) for the project with pass/fail stats.',
949
+ inputSchema: {
950
+ type: 'object' as const,
951
+ properties: {
952
+ status: {
953
+ type: 'string',
954
+ enum: ['draft', 'active', 'paused', 'completed', 'archived'],
955
+ description: 'Filter by test run status',
956
+ },
957
+ limit: {
958
+ type: 'number',
959
+ description: 'Maximum number of runs to return (default: 20)',
960
+ },
961
+ },
962
+ },
963
+ },
964
+ {
965
+ name: 'create_test_run',
966
+ description: 'Create a new testing campaign (test run). Tests can then be assigned to testers within this run.',
967
+ inputSchema: {
968
+ type: 'object' as const,
969
+ properties: {
970
+ name: {
971
+ type: 'string',
972
+ description: 'Name for the test run (e.g. "v2.1 QA Pass", "Sprint 5 Testing")',
973
+ },
974
+ description: {
975
+ type: 'string',
976
+ description: 'Optional description of the test run scope and goals',
977
+ },
978
+ },
979
+ required: ['name'],
980
+ },
981
+ },
982
+ {
983
+ name: 'list_test_assignments',
984
+ description: 'List test assignments showing which tests are assigned to which testers, with status tracking. Joins test case and tester info.',
985
+ inputSchema: {
986
+ type: 'object' as const,
987
+ properties: {
988
+ tester_id: {
989
+ type: 'string',
990
+ description: 'Filter by tester UUID',
991
+ },
992
+ test_run_id: {
993
+ type: 'string',
994
+ description: 'Filter by test run UUID',
995
+ },
996
+ status: {
997
+ type: 'string',
998
+ enum: ['pending', 'in_progress', 'passed', 'failed', 'blocked', 'skipped'],
999
+ description: 'Filter by assignment status',
1000
+ },
1001
+ limit: {
1002
+ type: 'number',
1003
+ description: 'Maximum number of assignments to return (default: 50, max: 200)',
1004
+ },
1005
+ },
1006
+ },
1007
+ },
1008
+ {
1009
+ name: 'assign_tests',
1010
+ description: 'Assign one or more test cases to a tester. Optionally assign within a test run. Skips duplicates gracefully.',
1011
+ inputSchema: {
1012
+ type: 'object' as const,
1013
+ properties: {
1014
+ tester_id: {
1015
+ type: 'string',
1016
+ description: 'UUID of the tester to assign tests to (required)',
1017
+ },
1018
+ test_case_ids: {
1019
+ type: 'array',
1020
+ items: { type: 'string' },
1021
+ description: 'Array of test case UUIDs to assign (required)',
1022
+ },
1023
+ test_run_id: {
1024
+ type: 'string',
1025
+ description: 'Optional test run UUID to group assignments under',
1026
+ },
1027
+ },
1028
+ required: ['tester_id', 'test_case_ids'],
1029
+ },
1030
+ },
1031
+ {
1032
+ name: 'get_tester_workload',
1033
+ description: 'View a specific tester\'s current workload — assignment counts by status and recent assignments.',
1034
+ inputSchema: {
1035
+ type: 'object' as const,
1036
+ properties: {
1037
+ tester_id: {
1038
+ type: 'string',
1039
+ description: 'UUID of the tester (required)',
1040
+ },
1041
+ },
1042
+ required: ['tester_id'],
1043
+ },
1044
+ },
922
1045
  ];
923
1046
 
924
1047
  // Tool handlers
@@ -930,7 +1053,7 @@ async function listReports(args: {
930
1053
  }) {
931
1054
  let query = supabase
932
1055
  .from('reports')
933
- .select('id, report_type, severity, status, description, app_context, created_at, reporter_name, reporter_email, tester:testers(name, email)')
1056
+ .select('id, report_type, severity, status, description, app_context, created_at, reporter_name, tester:testers(name)')
934
1057
  .eq('project_id', PROJECT_ID)
935
1058
  .order('created_at', { ascending: false })
936
1059
  .limit(Math.min(args.limit || 10, 50));
@@ -991,10 +1114,8 @@ async function getReport(args: { report_id: string }) {
991
1114
  created_at: data.created_at,
992
1115
  reporter: data.tester ? {
993
1116
  name: data.tester.name,
994
- email: data.tester.email,
995
1117
  } : (data.reporter_name ? {
996
1118
  name: data.reporter_name,
997
- email: data.reporter_email,
998
1119
  } : null),
999
1120
  track: data.track ? {
1000
1121
  name: data.track.name,
@@ -1194,13 +1315,16 @@ async function createTestCase(args: {
1194
1315
  // Find track ID if track name provided
1195
1316
  let trackId: string | null = null;
1196
1317
  if (args.track) {
1197
- const { data: trackData } = await supabase
1198
- .from('qa_tracks')
1199
- .select('id')
1200
- .eq('project_id', PROJECT_ID)
1201
- .ilike('name', `%${args.track}%`)
1202
- .single();
1203
- trackId = trackData?.id || null;
1318
+ const sanitizedTrack = sanitizeSearchQuery(args.track);
1319
+ if (sanitizedTrack) {
1320
+ const { data: trackData } = await supabase
1321
+ .from('qa_tracks')
1322
+ .select('id')
1323
+ .eq('project_id', PROJECT_ID)
1324
+ .ilike('name', `%${sanitizedTrack}%`)
1325
+ .single();
1326
+ trackId = trackData?.id || null;
1327
+ }
1204
1328
  }
1205
1329
 
1206
1330
  const testCase = {
@@ -1674,7 +1798,11 @@ async function getTestPriorities(args: {
1674
1798
  const includeFactors = args.include_factors !== false;
1675
1799
 
1676
1800
  // First, refresh the route stats
1677
- await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
1801
+ const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
1802
+ if (refreshError) {
1803
+ // Non-fatal: proceed with potentially stale data but warn
1804
+ console.warn('Failed to refresh route stats:', refreshError.message);
1805
+ }
1678
1806
 
1679
1807
  // Get prioritized routes
1680
1808
  const { data: routes, error } = await supabase
@@ -2114,7 +2242,9 @@ async function getCoverageMatrix(args: {
2114
2242
  .from('test_assignments')
2115
2243
  .select('test_case_id, status, completed_at')
2116
2244
  .eq('project_id', PROJECT_ID)
2117
- .in('status', ['passed', 'failed']);
2245
+ .in('status', ['passed', 'failed'])
2246
+ .order('completed_at', { ascending: false })
2247
+ .limit(2000);
2118
2248
  assignments = data || [];
2119
2249
  }
2120
2250
 
@@ -2286,7 +2416,11 @@ async function getStaleCoverage(args: {
2286
2416
  const limit = args.limit || 20;
2287
2417
 
2288
2418
  // Refresh stats first
2289
- await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
2419
+ const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
2420
+ if (refreshError) {
2421
+ // Non-fatal: proceed with potentially stale data but warn
2422
+ console.warn('Failed to refresh route stats:', refreshError.message);
2423
+ }
2290
2424
 
2291
2425
  // Get routes ordered by staleness and risk
2292
2426
  const { data: routes, error } = await supabase
@@ -2382,12 +2516,30 @@ async function generateDeployChecklist(args: {
2382
2516
  });
2383
2517
  }
2384
2518
 
2385
- // Get test cases for these routes
2386
- const { data: testCases } = await supabase
2387
- .from('test_cases')
2388
- .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2389
- .eq('project_id', PROJECT_ID)
2390
- .or(routes.map(r => `target_route.eq.${r}`).join(',') + ',' + routes.map(r => `category.eq.${r}`).join(','));
2519
+ // Limit routes array to prevent query explosion
2520
+ const safeRoutes = routes.slice(0, 100);
2521
+
2522
+ // Get test cases for these routes (use separate queries to avoid filter injection)
2523
+ const [{ data: byRoute }, { data: byCategory }] = await Promise.all([
2524
+ supabase
2525
+ .from('test_cases')
2526
+ .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2527
+ .eq('project_id', PROJECT_ID)
2528
+ .in('target_route', safeRoutes),
2529
+ supabase
2530
+ .from('test_cases')
2531
+ .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2532
+ .eq('project_id', PROJECT_ID)
2533
+ .in('category', safeRoutes),
2534
+ ]);
2535
+
2536
+ // Deduplicate by id
2537
+ const seenIds = new Set<string>();
2538
+ const testCases = [...(byRoute || []), ...(byCategory || [])].filter(tc => {
2539
+ if (seenIds.has(tc.id)) return false;
2540
+ seenIds.add(tc.id);
2541
+ return true;
2542
+ });
2391
2543
 
2392
2544
  // Get route stats for risk assessment
2393
2545
  const { data: routeStats } = await supabase
@@ -2988,10 +3140,17 @@ async function analyzeCommitForTesting(args: {
2988
3140
 
2989
3141
  for (const mapping of mappings || []) {
2990
3142
  const matchedFiles = filesChanged.filter(file => {
2991
- const pattern = mapping.file_pattern
2992
- .replace(/\*\*/g, '.*')
2993
- .replace(/\*/g, '[^/]*');
2994
- return new RegExp(pattern).test(file);
3143
+ try {
3144
+ // Validate pattern complexity to prevent ReDoS
3145
+ if (mapping.file_pattern.length > 200) return false;
3146
+ const pattern = mapping.file_pattern
3147
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars first
3148
+ .replace(/\\\*\\\*/g, '.*') // Then convert glob ** to .*
3149
+ .replace(/\\\*/g, '[^/]*'); // And glob * to [^/]*
3150
+ return new RegExp(`^${pattern}$`).test(file);
3151
+ } catch {
3152
+ return false; // Skip malformed patterns
3153
+ }
2995
3154
  });
2996
3155
 
2997
3156
  if (matchedFiles.length > 0) {
@@ -3736,6 +3895,7 @@ async function markFixedWithCommit(args: {
3736
3895
  commit_message?: string;
3737
3896
  resolution_notes?: string;
3738
3897
  files_changed?: string[];
3898
+ notify_tester?: boolean;
3739
3899
  }) {
3740
3900
  if (!isValidUUID(args.report_id)) {
3741
3901
  return { error: 'Invalid report_id format' };
@@ -3759,6 +3919,7 @@ async function markFixedWithCommit(args: {
3759
3919
  status: 'resolved',
3760
3920
  resolved_at: new Date().toISOString(),
3761
3921
  resolution_notes: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
3922
+ notify_tester: args.notify_tester === true, // Opt-in: only notify if explicitly requested
3762
3923
  code_context: {
3763
3924
  ...existingContext,
3764
3925
  fix: {
@@ -3781,11 +3942,16 @@ async function markFixedWithCommit(args: {
3781
3942
  return { error: error.message };
3782
3943
  }
3783
3944
 
3945
+ const notificationStatus = args.notify_tester
3946
+ ? 'The original tester will be notified and assigned a verification task.'
3947
+ : 'No notification sent (silent resolve). A verification task was created.';
3948
+
3784
3949
  return {
3785
3950
  success: true,
3786
- message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}`,
3951
+ message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}. ${notificationStatus}`,
3787
3952
  report_id: args.report_id,
3788
3953
  commit: args.commit_sha,
3954
+ tester_notified: args.notify_tester === true,
3789
3955
  next_steps: [
3790
3956
  'Consider running create_regression_test to prevent this bug from recurring',
3791
3957
  'Push your changes to trigger CI/CD',
@@ -4620,6 +4786,423 @@ Which files or areas would you like me to analyze?`;
4620
4786
  }
4621
4787
  }
4622
4788
 
4789
+ // === TESTER & ASSIGNMENT MANAGEMENT HANDLERS ===
4790
+
4791
+ async function listTesters(args: {
4792
+ status?: string;
4793
+ platform?: string;
4794
+ }) {
4795
+ let query = supabase
4796
+ .from('testers')
4797
+ .select('id, name, email, status, platforms, tier, assigned_count, completed_count, notes, created_at')
4798
+ .eq('project_id', PROJECT_ID)
4799
+ .order('name', { ascending: true });
4800
+
4801
+ if (args.status) {
4802
+ query = query.eq('status', args.status);
4803
+ }
4804
+
4805
+ const { data, error } = await query;
4806
+
4807
+ if (error) {
4808
+ return { error: error.message };
4809
+ }
4810
+
4811
+ let testers = data || [];
4812
+
4813
+ // Filter by platform if specified (platforms is an array column)
4814
+ if (args.platform) {
4815
+ testers = testers.filter((t: any) =>
4816
+ t.platforms && t.platforms.includes(args.platform)
4817
+ );
4818
+ }
4819
+
4820
+ return {
4821
+ count: testers.length,
4822
+ testers: testers.map((t: any) => ({
4823
+ id: t.id,
4824
+ name: t.name,
4825
+ email: t.email,
4826
+ status: t.status,
4827
+ platforms: t.platforms,
4828
+ tier: t.tier,
4829
+ assignedCount: t.assigned_count,
4830
+ completedCount: t.completed_count,
4831
+ notes: t.notes,
4832
+ })),
4833
+ };
4834
+ }
4835
+
4836
+ async function listTestRuns(args: {
4837
+ status?: string;
4838
+ limit?: number;
4839
+ }) {
4840
+ const limit = Math.min(args.limit || 20, 50);
4841
+
4842
+ let query = supabase
4843
+ .from('test_runs')
4844
+ .select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
4845
+ .eq('project_id', PROJECT_ID)
4846
+ .order('created_at', { ascending: false })
4847
+ .limit(limit);
4848
+
4849
+ if (args.status) {
4850
+ query = query.eq('status', args.status);
4851
+ }
4852
+
4853
+ const { data, error } = await query;
4854
+
4855
+ if (error) {
4856
+ return { error: error.message };
4857
+ }
4858
+
4859
+ return {
4860
+ count: (data || []).length,
4861
+ testRuns: (data || []).map((r: any) => ({
4862
+ id: r.id,
4863
+ name: r.name,
4864
+ description: r.description,
4865
+ status: r.status,
4866
+ totalTests: r.total_tests,
4867
+ passedTests: r.passed_tests,
4868
+ failedTests: r.failed_tests,
4869
+ passRate: r.total_tests > 0 ? Math.round((r.passed_tests / r.total_tests) * 100) : 0,
4870
+ startedAt: r.started_at,
4871
+ completedAt: r.completed_at,
4872
+ createdAt: r.created_at,
4873
+ })),
4874
+ };
4875
+ }
4876
+
4877
+ async function createTestRun(args: {
4878
+ name: string;
4879
+ description?: string;
4880
+ }) {
4881
+ if (!args.name || args.name.trim().length === 0) {
4882
+ return { error: 'Test run name is required' };
4883
+ }
4884
+
4885
+ const { data, error } = await supabase
4886
+ .from('test_runs')
4887
+ .insert({
4888
+ project_id: PROJECT_ID,
4889
+ name: args.name.trim(),
4890
+ description: args.description?.trim() || null,
4891
+ status: 'draft',
4892
+ })
4893
+ .select('id, name, description, status, created_at')
4894
+ .single();
4895
+
4896
+ if (error) {
4897
+ return { error: error.message };
4898
+ }
4899
+
4900
+ return {
4901
+ success: true,
4902
+ testRun: {
4903
+ id: data.id,
4904
+ name: data.name,
4905
+ description: data.description,
4906
+ status: data.status,
4907
+ createdAt: data.created_at,
4908
+ },
4909
+ message: `Test run "${data.name}" created. Use assign_tests to add test assignments.`,
4910
+ };
4911
+ }
4912
+
4913
+ async function listTestAssignments(args: {
4914
+ tester_id?: string;
4915
+ test_run_id?: string;
4916
+ status?: string;
4917
+ limit?: number;
4918
+ }) {
4919
+ const limit = Math.min(args.limit || 50, 200);
4920
+
4921
+ if (args.tester_id && !isValidUUID(args.tester_id)) {
4922
+ return { error: 'Invalid tester_id format' };
4923
+ }
4924
+ if (args.test_run_id && !isValidUUID(args.test_run_id)) {
4925
+ return { error: 'Invalid test_run_id format' };
4926
+ }
4927
+
4928
+ let query = supabase
4929
+ .from('test_assignments')
4930
+ .select(`
4931
+ id,
4932
+ status,
4933
+ assigned_at,
4934
+ started_at,
4935
+ completed_at,
4936
+ duration_seconds,
4937
+ is_verification,
4938
+ notes,
4939
+ test_case:test_cases(id, test_key, title, priority, target_route),
4940
+ tester:testers(id, name, email),
4941
+ test_run:test_runs(id, name)
4942
+ `)
4943
+ .eq('project_id', PROJECT_ID)
4944
+ .order('assigned_at', { ascending: false })
4945
+ .limit(limit);
4946
+
4947
+ if (args.tester_id) {
4948
+ query = query.eq('tester_id', args.tester_id);
4949
+ }
4950
+ if (args.test_run_id) {
4951
+ query = query.eq('test_run_id', args.test_run_id);
4952
+ }
4953
+ if (args.status) {
4954
+ query = query.eq('status', args.status);
4955
+ }
4956
+
4957
+ const { data, error } = await query;
4958
+
4959
+ if (error) {
4960
+ return { error: error.message };
4961
+ }
4962
+
4963
+ return {
4964
+ count: (data || []).length,
4965
+ assignments: (data || []).map((a: any) => ({
4966
+ id: a.id,
4967
+ status: a.status,
4968
+ assignedAt: a.assigned_at,
4969
+ startedAt: a.started_at,
4970
+ completedAt: a.completed_at,
4971
+ durationSeconds: a.duration_seconds,
4972
+ isVerification: a.is_verification,
4973
+ notes: a.notes,
4974
+ testCase: a.test_case ? {
4975
+ id: a.test_case.id,
4976
+ testKey: a.test_case.test_key,
4977
+ title: a.test_case.title,
4978
+ priority: a.test_case.priority,
4979
+ targetRoute: a.test_case.target_route,
4980
+ } : null,
4981
+ tester: a.tester ? {
4982
+ id: a.tester.id,
4983
+ name: a.tester.name,
4984
+ email: a.tester.email,
4985
+ } : null,
4986
+ testRun: a.test_run ? {
4987
+ id: a.test_run.id,
4988
+ name: a.test_run.name,
4989
+ } : null,
4990
+ })),
4991
+ };
4992
+ }
4993
+
4994
+ async function assignTests(args: {
4995
+ tester_id: string;
4996
+ test_case_ids: string[];
4997
+ test_run_id?: string;
4998
+ }) {
4999
+ // Validate inputs
5000
+ if (!isValidUUID(args.tester_id)) {
5001
+ return { error: 'Invalid tester_id format' };
5002
+ }
5003
+ if (!args.test_case_ids || args.test_case_ids.length === 0) {
5004
+ return { error: 'At least one test_case_id is required' };
5005
+ }
5006
+ if (args.test_case_ids.length > 50) {
5007
+ return { error: 'Maximum 50 test cases per assignment batch' };
5008
+ }
5009
+ for (const id of args.test_case_ids) {
5010
+ if (!isValidUUID(id)) {
5011
+ return { error: `Invalid test_case_id format: ${id}` };
5012
+ }
5013
+ }
5014
+ if (args.test_run_id && !isValidUUID(args.test_run_id)) {
5015
+ return { error: 'Invalid test_run_id format' };
5016
+ }
5017
+
5018
+ // Verify tester exists and is active
5019
+ const { data: tester, error: testerErr } = await supabase
5020
+ .from('testers')
5021
+ .select('id, name, email, status')
5022
+ .eq('id', args.tester_id)
5023
+ .eq('project_id', PROJECT_ID)
5024
+ .single();
5025
+
5026
+ if (testerErr || !tester) {
5027
+ return { error: 'Tester not found in this project' };
5028
+ }
5029
+ if (tester.status !== 'active') {
5030
+ return { error: `Tester "${tester.name}" is ${tester.status}, not active` };
5031
+ }
5032
+
5033
+ // Verify test cases exist for this project
5034
+ const { data: testCases, error: tcErr } = await supabase
5035
+ .from('test_cases')
5036
+ .select('id, test_key, title')
5037
+ .eq('project_id', PROJECT_ID)
5038
+ .in('id', args.test_case_ids);
5039
+
5040
+ if (tcErr) {
5041
+ return { error: tcErr.message };
5042
+ }
5043
+
5044
+ const foundIds = new Set((testCases || []).map((tc: any) => tc.id));
5045
+ const missingIds = args.test_case_ids.filter(id => !foundIds.has(id));
5046
+
5047
+ if (missingIds.length > 0) {
5048
+ return {
5049
+ error: `Test cases not found in this project: ${missingIds.join(', ')}`,
5050
+ };
5051
+ }
5052
+
5053
+ // Verify test run exists if provided
5054
+ if (args.test_run_id) {
5055
+ const { data: run, error: runErr } = await supabase
5056
+ .from('test_runs')
5057
+ .select('id')
5058
+ .eq('id', args.test_run_id)
5059
+ .eq('project_id', PROJECT_ID)
5060
+ .single();
5061
+
5062
+ if (runErr || !run) {
5063
+ return { error: 'Test run not found in this project' };
5064
+ }
5065
+ }
5066
+
5067
+ // Build assignment rows
5068
+ const rows = args.test_case_ids.map(tcId => ({
5069
+ project_id: PROJECT_ID,
5070
+ test_case_id: tcId,
5071
+ tester_id: args.tester_id,
5072
+ test_run_id: args.test_run_id || null,
5073
+ status: 'pending',
5074
+ }));
5075
+
5076
+ // Insert — use upsert-like approach: insert and handle conflicts
5077
+ const { data: inserted, error: insertErr } = await supabase
5078
+ .from('test_assignments')
5079
+ .insert(rows)
5080
+ .select('id, test_case_id');
5081
+
5082
+ if (insertErr) {
5083
+ // Check if it's a unique constraint violation
5084
+ if (insertErr.message.includes('duplicate') || insertErr.message.includes('unique')) {
5085
+ // Try inserting one by one to find duplicates
5086
+ const created: any[] = [];
5087
+ const skipped: string[] = [];
5088
+
5089
+ for (const row of rows) {
5090
+ const { data: single, error: singleErr } = await supabase
5091
+ .from('test_assignments')
5092
+ .insert(row)
5093
+ .select('id, test_case_id')
5094
+ .single();
5095
+
5096
+ if (singleErr) {
5097
+ const tc = testCases?.find((t: any) => t.id === row.test_case_id);
5098
+ skipped.push(tc?.test_key || row.test_case_id);
5099
+ } else if (single) {
5100
+ created.push(single);
5101
+ }
5102
+ }
5103
+
5104
+ return {
5105
+ success: true,
5106
+ created: created.length,
5107
+ skipped: skipped.length,
5108
+ skippedTests: skipped,
5109
+ tester: { id: tester.id, name: tester.name },
5110
+ message: `Assigned ${created.length} test(s) to ${tester.name}. ${skipped.length} skipped (already assigned).`,
5111
+ };
5112
+ }
5113
+ return { error: insertErr.message };
5114
+ }
5115
+
5116
+ return {
5117
+ success: true,
5118
+ created: (inserted || []).length,
5119
+ skipped: 0,
5120
+ tester: { id: tester.id, name: tester.name },
5121
+ testRun: args.test_run_id || null,
5122
+ message: `Assigned ${(inserted || []).length} test(s) to ${tester.name}.`,
5123
+ };
5124
+ }
5125
+
5126
+ async function getTesterWorkload(args: {
5127
+ tester_id: string;
5128
+ }) {
5129
+ if (!isValidUUID(args.tester_id)) {
5130
+ return { error: 'Invalid tester_id format' };
5131
+ }
5132
+
5133
+ // Get tester info
5134
+ const { data: tester, error: testerErr } = await supabase
5135
+ .from('testers')
5136
+ .select('id, name, email, status, platforms, tier')
5137
+ .eq('id', args.tester_id)
5138
+ .eq('project_id', PROJECT_ID)
5139
+ .single();
5140
+
5141
+ if (testerErr || !tester) {
5142
+ return { error: 'Tester not found in this project' };
5143
+ }
5144
+
5145
+ // Get all assignments for this tester in this project
5146
+ const { data: assignments, error: assignErr } = await supabase
5147
+ .from('test_assignments')
5148
+ .select(`
5149
+ id,
5150
+ status,
5151
+ assigned_at,
5152
+ completed_at,
5153
+ test_case:test_cases(test_key, title, priority),
5154
+ test_run:test_runs(name)
5155
+ `)
5156
+ .eq('project_id', PROJECT_ID)
5157
+ .eq('tester_id', args.tester_id)
5158
+ .order('assigned_at', { ascending: false });
5159
+
5160
+ if (assignErr) {
5161
+ return { error: assignErr.message };
5162
+ }
5163
+
5164
+ const all = assignments || [];
5165
+
5166
+ // Count by status
5167
+ const counts: Record<string, number> = {
5168
+ pending: 0,
5169
+ in_progress: 0,
5170
+ passed: 0,
5171
+ failed: 0,
5172
+ blocked: 0,
5173
+ skipped: 0,
5174
+ };
5175
+ for (const a of all) {
5176
+ counts[a.status] = (counts[a.status] || 0) + 1;
5177
+ }
5178
+
5179
+ return {
5180
+ tester: {
5181
+ id: tester.id,
5182
+ name: tester.name,
5183
+ email: tester.email,
5184
+ status: tester.status,
5185
+ platforms: tester.platforms,
5186
+ tier: tester.tier,
5187
+ },
5188
+ totalAssignments: all.length,
5189
+ counts,
5190
+ activeLoad: counts.pending + counts.in_progress,
5191
+ recentAssignments: all.slice(0, 10).map((a: any) => ({
5192
+ id: a.id,
5193
+ status: a.status,
5194
+ assignedAt: a.assigned_at,
5195
+ completedAt: a.completed_at,
5196
+ testCase: a.test_case ? {
5197
+ testKey: a.test_case.test_key,
5198
+ title: a.test_case.title,
5199
+ priority: a.test_case.priority,
5200
+ } : null,
5201
+ testRun: a.test_run?.name || null,
5202
+ })),
5203
+ };
5204
+ }
5205
+
4623
5206
  // Main server setup
4624
5207
  async function main() {
4625
5208
  initSupabase();
@@ -4763,6 +5346,25 @@ async function main() {
4763
5346
  case 'get_testing_patterns':
4764
5347
  result = await getTestingPatterns(args as any);
4765
5348
  break;
5349
+ // === TESTER & ASSIGNMENT MANAGEMENT ===
5350
+ case 'list_testers':
5351
+ result = await listTesters(args as any);
5352
+ break;
5353
+ case 'list_test_runs':
5354
+ result = await listTestRuns(args as any);
5355
+ break;
5356
+ case 'create_test_run':
5357
+ result = await createTestRun(args as any);
5358
+ break;
5359
+ case 'list_test_assignments':
5360
+ result = await listTestAssignments(args as any);
5361
+ break;
5362
+ case 'assign_tests':
5363
+ result = await assignTests(args as any);
5364
+ break;
5365
+ case 'get_tester_workload':
5366
+ result = await getTesterWorkload(args as any);
5367
+ break;
4766
5368
  default:
4767
5369
  return {
4768
5370
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
@@ -4783,7 +5385,7 @@ async function main() {
4783
5385
 
4784
5386
  // Handle resource listing (reports as resources)
4785
5387
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
4786
- const { data } = await supabase
5388
+ const { data, error } = await supabase
4787
5389
  .from('reports')
4788
5390
  .select('id, description, report_type, severity')
4789
5391
  .eq('project_id', PROJECT_ID)
@@ -4791,6 +5393,11 @@ async function main() {
4791
5393
  .order('created_at', { ascending: false })
4792
5394
  .limit(10);
4793
5395
 
5396
+ if (error) {
5397
+ console.error('Failed to list resources:', error.message);
5398
+ return { resources: [] };
5399
+ }
5400
+
4794
5401
  return {
4795
5402
  resources: (data || []).map(r => ({
4796
5403
  uri: `bugbear://reports/${r.id}`,