@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/dist/index.js CHANGED
@@ -435,6 +435,10 @@ const tools = [
435
435
  items: { type: 'string' },
436
436
  description: 'List of files that were modified to fix this bug',
437
437
  },
438
+ notify_tester: {
439
+ type: 'boolean',
440
+ description: 'If true, notify the original tester about the fix with a message and verification task. Default: false (silent resolve).',
441
+ },
438
442
  },
439
443
  required: ['report_id', 'commit_sha'],
440
444
  },
@@ -902,12 +906,131 @@ const tools = [
902
906
  },
903
907
  },
904
908
  },
909
+ // === TESTER & ASSIGNMENT MANAGEMENT TOOLS ===
910
+ {
911
+ name: 'list_testers',
912
+ description: 'List all QA testers for the project with their status, platforms, and workload counts.',
913
+ inputSchema: {
914
+ type: 'object',
915
+ properties: {
916
+ status: {
917
+ type: 'string',
918
+ enum: ['active', 'inactive', 'invited'],
919
+ description: 'Filter by tester status (default: all)',
920
+ },
921
+ platform: {
922
+ type: 'string',
923
+ enum: ['ios', 'android', 'web'],
924
+ description: 'Filter by platform support',
925
+ },
926
+ },
927
+ },
928
+ },
929
+ {
930
+ name: 'list_test_runs',
931
+ description: 'List testing campaigns (test runs) for the project with pass/fail stats.',
932
+ inputSchema: {
933
+ type: 'object',
934
+ properties: {
935
+ status: {
936
+ type: 'string',
937
+ enum: ['draft', 'active', 'paused', 'completed', 'archived'],
938
+ description: 'Filter by test run status',
939
+ },
940
+ limit: {
941
+ type: 'number',
942
+ description: 'Maximum number of runs to return (default: 20)',
943
+ },
944
+ },
945
+ },
946
+ },
947
+ {
948
+ name: 'create_test_run',
949
+ description: 'Create a new testing campaign (test run). Tests can then be assigned to testers within this run.',
950
+ inputSchema: {
951
+ type: 'object',
952
+ properties: {
953
+ name: {
954
+ type: 'string',
955
+ description: 'Name for the test run (e.g. "v2.1 QA Pass", "Sprint 5 Testing")',
956
+ },
957
+ description: {
958
+ type: 'string',
959
+ description: 'Optional description of the test run scope and goals',
960
+ },
961
+ },
962
+ required: ['name'],
963
+ },
964
+ },
965
+ {
966
+ name: 'list_test_assignments',
967
+ description: 'List test assignments showing which tests are assigned to which testers, with status tracking. Joins test case and tester info.',
968
+ inputSchema: {
969
+ type: 'object',
970
+ properties: {
971
+ tester_id: {
972
+ type: 'string',
973
+ description: 'Filter by tester UUID',
974
+ },
975
+ test_run_id: {
976
+ type: 'string',
977
+ description: 'Filter by test run UUID',
978
+ },
979
+ status: {
980
+ type: 'string',
981
+ enum: ['pending', 'in_progress', 'passed', 'failed', 'blocked', 'skipped'],
982
+ description: 'Filter by assignment status',
983
+ },
984
+ limit: {
985
+ type: 'number',
986
+ description: 'Maximum number of assignments to return (default: 50, max: 200)',
987
+ },
988
+ },
989
+ },
990
+ },
991
+ {
992
+ name: 'assign_tests',
993
+ description: 'Assign one or more test cases to a tester. Optionally assign within a test run. Skips duplicates gracefully.',
994
+ inputSchema: {
995
+ type: 'object',
996
+ properties: {
997
+ tester_id: {
998
+ type: 'string',
999
+ description: 'UUID of the tester to assign tests to (required)',
1000
+ },
1001
+ test_case_ids: {
1002
+ type: 'array',
1003
+ items: { type: 'string' },
1004
+ description: 'Array of test case UUIDs to assign (required)',
1005
+ },
1006
+ test_run_id: {
1007
+ type: 'string',
1008
+ description: 'Optional test run UUID to group assignments under',
1009
+ },
1010
+ },
1011
+ required: ['tester_id', 'test_case_ids'],
1012
+ },
1013
+ },
1014
+ {
1015
+ name: 'get_tester_workload',
1016
+ description: 'View a specific tester\'s current workload — assignment counts by status and recent assignments.',
1017
+ inputSchema: {
1018
+ type: 'object',
1019
+ properties: {
1020
+ tester_id: {
1021
+ type: 'string',
1022
+ description: 'UUID of the tester (required)',
1023
+ },
1024
+ },
1025
+ required: ['tester_id'],
1026
+ },
1027
+ },
905
1028
  ];
906
1029
  // Tool handlers
907
1030
  async function listReports(args) {
908
1031
  let query = supabase
909
1032
  .from('reports')
910
- .select('id, report_type, severity, status, description, app_context, created_at, reporter_name, reporter_email, tester:testers(name, email)')
1033
+ .select('id, report_type, severity, status, description, app_context, created_at, reporter_name, tester:testers(name)')
911
1034
  .eq('project_id', PROJECT_ID)
912
1035
  .order('created_at', { ascending: false })
913
1036
  .limit(Math.min(args.limit || 10, 50));
@@ -963,10 +1086,8 @@ async function getReport(args) {
963
1086
  created_at: data.created_at,
964
1087
  reporter: data.tester ? {
965
1088
  name: data.tester.name,
966
- email: data.tester.email,
967
1089
  } : (data.reporter_name ? {
968
1090
  name: data.reporter_name,
969
- email: data.reporter_email,
970
1091
  } : null),
971
1092
  track: data.track ? {
972
1093
  name: data.track.name,
@@ -1126,13 +1247,16 @@ async function createTestCase(args) {
1126
1247
  // Find track ID if track name provided
1127
1248
  let trackId = null;
1128
1249
  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;
1250
+ const sanitizedTrack = sanitizeSearchQuery(args.track);
1251
+ if (sanitizedTrack) {
1252
+ const { data: trackData } = await supabase
1253
+ .from('qa_tracks')
1254
+ .select('id')
1255
+ .eq('project_id', PROJECT_ID)
1256
+ .ilike('name', `%${sanitizedTrack}%`)
1257
+ .single();
1258
+ trackId = trackData?.id || null;
1259
+ }
1136
1260
  }
1137
1261
  const testCase = {
1138
1262
  project_id: PROJECT_ID,
@@ -1511,7 +1635,11 @@ async function getTestPriorities(args) {
1511
1635
  const minScore = args.min_score || 0;
1512
1636
  const includeFactors = args.include_factors !== false;
1513
1637
  // First, refresh the route stats
1514
- await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
1638
+ const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
1639
+ if (refreshError) {
1640
+ // Non-fatal: proceed with potentially stale data but warn
1641
+ console.warn('Failed to refresh route stats:', refreshError.message);
1642
+ }
1515
1643
  // Get prioritized routes
1516
1644
  const { data: routes, error } = await supabase
1517
1645
  .from('route_test_stats')
@@ -1890,7 +2018,9 @@ async function getCoverageMatrix(args) {
1890
2018
  .from('test_assignments')
1891
2019
  .select('test_case_id, status, completed_at')
1892
2020
  .eq('project_id', PROJECT_ID)
1893
- .in('status', ['passed', 'failed']);
2021
+ .in('status', ['passed', 'failed'])
2022
+ .order('completed_at', { ascending: false })
2023
+ .limit(2000);
1894
2024
  assignments = data || [];
1895
2025
  }
1896
2026
  // Get route stats for bug counts
@@ -2038,7 +2168,11 @@ async function getStaleCoverage(args) {
2038
2168
  const daysThreshold = args.days_threshold || 14;
2039
2169
  const limit = args.limit || 20;
2040
2170
  // Refresh stats first
2041
- await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
2171
+ const { error: refreshError } = await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
2172
+ if (refreshError) {
2173
+ // Non-fatal: proceed with potentially stale data but warn
2174
+ console.warn('Failed to refresh route stats:', refreshError.message);
2175
+ }
2042
2176
  // Get routes ordered by staleness and risk
2043
2177
  const { data: routes, error } = await supabase
2044
2178
  .from('route_test_stats')
@@ -2123,12 +2257,29 @@ async function generateDeployChecklist(args) {
2123
2257
  }
2124
2258
  });
2125
2259
  }
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(','));
2260
+ // Limit routes array to prevent query explosion
2261
+ const safeRoutes = routes.slice(0, 100);
2262
+ // Get test cases for these routes (use separate queries to avoid filter injection)
2263
+ const [{ data: byRoute }, { data: byCategory }] = await Promise.all([
2264
+ supabase
2265
+ .from('test_cases')
2266
+ .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2267
+ .eq('project_id', PROJECT_ID)
2268
+ .in('target_route', safeRoutes),
2269
+ supabase
2270
+ .from('test_cases')
2271
+ .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2272
+ .eq('project_id', PROJECT_ID)
2273
+ .in('category', safeRoutes),
2274
+ ]);
2275
+ // Deduplicate by id
2276
+ const seenIds = new Set();
2277
+ const testCases = [...(byRoute || []), ...(byCategory || [])].filter(tc => {
2278
+ if (seenIds.has(tc.id))
2279
+ return false;
2280
+ seenIds.add(tc.id);
2281
+ return true;
2282
+ });
2132
2283
  // Get route stats for risk assessment
2133
2284
  const { data: routeStats } = await supabase
2134
2285
  .from('route_test_stats')
@@ -2631,10 +2782,19 @@ async function analyzeCommitForTesting(args) {
2631
2782
  const affectedRoutes = [];
2632
2783
  for (const mapping of mappings || []) {
2633
2784
  const matchedFiles = filesChanged.filter(file => {
2634
- const pattern = mapping.file_pattern
2635
- .replace(/\*\*/g, '.*')
2636
- .replace(/\*/g, '[^/]*');
2637
- return new RegExp(pattern).test(file);
2785
+ try {
2786
+ // Validate pattern complexity to prevent ReDoS
2787
+ if (mapping.file_pattern.length > 200)
2788
+ return false;
2789
+ const pattern = mapping.file_pattern
2790
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars first
2791
+ .replace(/\\\*\\\*/g, '.*') // Then convert glob ** to .*
2792
+ .replace(/\\\*/g, '[^/]*'); // And glob * to [^/]*
2793
+ return new RegExp(`^${pattern}$`).test(file);
2794
+ }
2795
+ catch {
2796
+ return false; // Skip malformed patterns
2797
+ }
2638
2798
  });
2639
2799
  if (matchedFiles.length > 0) {
2640
2800
  affectedRoutes.push({
@@ -3292,6 +3452,7 @@ async function markFixedWithCommit(args) {
3292
3452
  status: 'resolved',
3293
3453
  resolved_at: new Date().toISOString(),
3294
3454
  resolution_notes: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
3455
+ notify_tester: args.notify_tester === true, // Opt-in: only notify if explicitly requested
3295
3456
  code_context: {
3296
3457
  ...existingContext,
3297
3458
  fix: {
@@ -3311,11 +3472,15 @@ async function markFixedWithCommit(args) {
3311
3472
  if (error) {
3312
3473
  return { error: error.message };
3313
3474
  }
3475
+ const notificationStatus = args.notify_tester
3476
+ ? 'The original tester will be notified and assigned a verification task.'
3477
+ : 'No notification sent (silent resolve). A verification task was created.';
3314
3478
  return {
3315
3479
  success: true,
3316
- message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}`,
3480
+ message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}. ${notificationStatus}`,
3317
3481
  report_id: args.report_id,
3318
3482
  commit: args.commit_sha,
3483
+ tester_notified: args.notify_tester === true,
3319
3484
  next_steps: [
3320
3485
  'Consider running create_regression_test to prevent this bug from recurring',
3321
3486
  'Push your changes to trigger CI/CD',
@@ -4036,6 +4201,353 @@ Which files or areas would you like me to analyze?`;
4036
4201
  return 'Unknown prompt';
4037
4202
  }
4038
4203
  }
4204
+ // === TESTER & ASSIGNMENT MANAGEMENT HANDLERS ===
4205
+ async function listTesters(args) {
4206
+ let query = supabase
4207
+ .from('testers')
4208
+ .select('id, name, email, status, platforms, tier, assigned_count, completed_count, notes, created_at')
4209
+ .eq('project_id', PROJECT_ID)
4210
+ .order('name', { ascending: true });
4211
+ if (args.status) {
4212
+ query = query.eq('status', args.status);
4213
+ }
4214
+ const { data, error } = await query;
4215
+ if (error) {
4216
+ return { error: error.message };
4217
+ }
4218
+ let testers = data || [];
4219
+ // Filter by platform if specified (platforms is an array column)
4220
+ if (args.platform) {
4221
+ testers = testers.filter((t) => t.platforms && t.platforms.includes(args.platform));
4222
+ }
4223
+ return {
4224
+ count: testers.length,
4225
+ testers: testers.map((t) => ({
4226
+ id: t.id,
4227
+ name: t.name,
4228
+ email: t.email,
4229
+ status: t.status,
4230
+ platforms: t.platforms,
4231
+ tier: t.tier,
4232
+ assignedCount: t.assigned_count,
4233
+ completedCount: t.completed_count,
4234
+ notes: t.notes,
4235
+ })),
4236
+ };
4237
+ }
4238
+ async function listTestRuns(args) {
4239
+ const limit = Math.min(args.limit || 20, 50);
4240
+ let query = supabase
4241
+ .from('test_runs')
4242
+ .select('id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at')
4243
+ .eq('project_id', PROJECT_ID)
4244
+ .order('created_at', { ascending: false })
4245
+ .limit(limit);
4246
+ if (args.status) {
4247
+ query = query.eq('status', args.status);
4248
+ }
4249
+ const { data, error } = await query;
4250
+ if (error) {
4251
+ return { error: error.message };
4252
+ }
4253
+ return {
4254
+ count: (data || []).length,
4255
+ testRuns: (data || []).map((r) => ({
4256
+ id: r.id,
4257
+ name: r.name,
4258
+ description: r.description,
4259
+ status: r.status,
4260
+ totalTests: r.total_tests,
4261
+ passedTests: r.passed_tests,
4262
+ failedTests: r.failed_tests,
4263
+ passRate: r.total_tests > 0 ? Math.round((r.passed_tests / r.total_tests) * 100) : 0,
4264
+ startedAt: r.started_at,
4265
+ completedAt: r.completed_at,
4266
+ createdAt: r.created_at,
4267
+ })),
4268
+ };
4269
+ }
4270
+ async function createTestRun(args) {
4271
+ if (!args.name || args.name.trim().length === 0) {
4272
+ return { error: 'Test run name is required' };
4273
+ }
4274
+ const { data, error } = await supabase
4275
+ .from('test_runs')
4276
+ .insert({
4277
+ project_id: PROJECT_ID,
4278
+ name: args.name.trim(),
4279
+ description: args.description?.trim() || null,
4280
+ status: 'draft',
4281
+ })
4282
+ .select('id, name, description, status, created_at')
4283
+ .single();
4284
+ if (error) {
4285
+ return { error: error.message };
4286
+ }
4287
+ return {
4288
+ success: true,
4289
+ testRun: {
4290
+ id: data.id,
4291
+ name: data.name,
4292
+ description: data.description,
4293
+ status: data.status,
4294
+ createdAt: data.created_at,
4295
+ },
4296
+ message: `Test run "${data.name}" created. Use assign_tests to add test assignments.`,
4297
+ };
4298
+ }
4299
+ async function listTestAssignments(args) {
4300
+ const limit = Math.min(args.limit || 50, 200);
4301
+ if (args.tester_id && !isValidUUID(args.tester_id)) {
4302
+ return { error: 'Invalid tester_id format' };
4303
+ }
4304
+ if (args.test_run_id && !isValidUUID(args.test_run_id)) {
4305
+ return { error: 'Invalid test_run_id format' };
4306
+ }
4307
+ let query = supabase
4308
+ .from('test_assignments')
4309
+ .select(`
4310
+ id,
4311
+ status,
4312
+ assigned_at,
4313
+ started_at,
4314
+ completed_at,
4315
+ duration_seconds,
4316
+ is_verification,
4317
+ notes,
4318
+ test_case:test_cases(id, test_key, title, priority, target_route),
4319
+ tester:testers(id, name, email),
4320
+ test_run:test_runs(id, name)
4321
+ `)
4322
+ .eq('project_id', PROJECT_ID)
4323
+ .order('assigned_at', { ascending: false })
4324
+ .limit(limit);
4325
+ if (args.tester_id) {
4326
+ query = query.eq('tester_id', args.tester_id);
4327
+ }
4328
+ if (args.test_run_id) {
4329
+ query = query.eq('test_run_id', args.test_run_id);
4330
+ }
4331
+ if (args.status) {
4332
+ query = query.eq('status', args.status);
4333
+ }
4334
+ const { data, error } = await query;
4335
+ if (error) {
4336
+ return { error: error.message };
4337
+ }
4338
+ return {
4339
+ count: (data || []).length,
4340
+ assignments: (data || []).map((a) => ({
4341
+ id: a.id,
4342
+ status: a.status,
4343
+ assignedAt: a.assigned_at,
4344
+ startedAt: a.started_at,
4345
+ completedAt: a.completed_at,
4346
+ durationSeconds: a.duration_seconds,
4347
+ isVerification: a.is_verification,
4348
+ notes: a.notes,
4349
+ testCase: a.test_case ? {
4350
+ id: a.test_case.id,
4351
+ testKey: a.test_case.test_key,
4352
+ title: a.test_case.title,
4353
+ priority: a.test_case.priority,
4354
+ targetRoute: a.test_case.target_route,
4355
+ } : null,
4356
+ tester: a.tester ? {
4357
+ id: a.tester.id,
4358
+ name: a.tester.name,
4359
+ email: a.tester.email,
4360
+ } : null,
4361
+ testRun: a.test_run ? {
4362
+ id: a.test_run.id,
4363
+ name: a.test_run.name,
4364
+ } : null,
4365
+ })),
4366
+ };
4367
+ }
4368
+ async function assignTests(args) {
4369
+ // Validate inputs
4370
+ if (!isValidUUID(args.tester_id)) {
4371
+ return { error: 'Invalid tester_id format' };
4372
+ }
4373
+ if (!args.test_case_ids || args.test_case_ids.length === 0) {
4374
+ return { error: 'At least one test_case_id is required' };
4375
+ }
4376
+ if (args.test_case_ids.length > 50) {
4377
+ return { error: 'Maximum 50 test cases per assignment batch' };
4378
+ }
4379
+ for (const id of args.test_case_ids) {
4380
+ if (!isValidUUID(id)) {
4381
+ return { error: `Invalid test_case_id format: ${id}` };
4382
+ }
4383
+ }
4384
+ if (args.test_run_id && !isValidUUID(args.test_run_id)) {
4385
+ return { error: 'Invalid test_run_id format' };
4386
+ }
4387
+ // Verify tester exists and is active
4388
+ const { data: tester, error: testerErr } = await supabase
4389
+ .from('testers')
4390
+ .select('id, name, email, status')
4391
+ .eq('id', args.tester_id)
4392
+ .eq('project_id', PROJECT_ID)
4393
+ .single();
4394
+ if (testerErr || !tester) {
4395
+ return { error: 'Tester not found in this project' };
4396
+ }
4397
+ if (tester.status !== 'active') {
4398
+ return { error: `Tester "${tester.name}" is ${tester.status}, not active` };
4399
+ }
4400
+ // Verify test cases exist for this project
4401
+ const { data: testCases, error: tcErr } = await supabase
4402
+ .from('test_cases')
4403
+ .select('id, test_key, title')
4404
+ .eq('project_id', PROJECT_ID)
4405
+ .in('id', args.test_case_ids);
4406
+ if (tcErr) {
4407
+ return { error: tcErr.message };
4408
+ }
4409
+ const foundIds = new Set((testCases || []).map((tc) => tc.id));
4410
+ const missingIds = args.test_case_ids.filter(id => !foundIds.has(id));
4411
+ if (missingIds.length > 0) {
4412
+ return {
4413
+ error: `Test cases not found in this project: ${missingIds.join(', ')}`,
4414
+ };
4415
+ }
4416
+ // Verify test run exists if provided
4417
+ if (args.test_run_id) {
4418
+ const { data: run, error: runErr } = await supabase
4419
+ .from('test_runs')
4420
+ .select('id')
4421
+ .eq('id', args.test_run_id)
4422
+ .eq('project_id', PROJECT_ID)
4423
+ .single();
4424
+ if (runErr || !run) {
4425
+ return { error: 'Test run not found in this project' };
4426
+ }
4427
+ }
4428
+ // Build assignment rows
4429
+ const rows = args.test_case_ids.map(tcId => ({
4430
+ project_id: PROJECT_ID,
4431
+ test_case_id: tcId,
4432
+ tester_id: args.tester_id,
4433
+ test_run_id: args.test_run_id || null,
4434
+ status: 'pending',
4435
+ }));
4436
+ // Insert — use upsert-like approach: insert and handle conflicts
4437
+ const { data: inserted, error: insertErr } = await supabase
4438
+ .from('test_assignments')
4439
+ .insert(rows)
4440
+ .select('id, test_case_id');
4441
+ if (insertErr) {
4442
+ // Check if it's a unique constraint violation
4443
+ if (insertErr.message.includes('duplicate') || insertErr.message.includes('unique')) {
4444
+ // Try inserting one by one to find duplicates
4445
+ const created = [];
4446
+ const skipped = [];
4447
+ for (const row of rows) {
4448
+ const { data: single, error: singleErr } = await supabase
4449
+ .from('test_assignments')
4450
+ .insert(row)
4451
+ .select('id, test_case_id')
4452
+ .single();
4453
+ if (singleErr) {
4454
+ const tc = testCases?.find((t) => t.id === row.test_case_id);
4455
+ skipped.push(tc?.test_key || row.test_case_id);
4456
+ }
4457
+ else if (single) {
4458
+ created.push(single);
4459
+ }
4460
+ }
4461
+ return {
4462
+ success: true,
4463
+ created: created.length,
4464
+ skipped: skipped.length,
4465
+ skippedTests: skipped,
4466
+ tester: { id: tester.id, name: tester.name },
4467
+ message: `Assigned ${created.length} test(s) to ${tester.name}. ${skipped.length} skipped (already assigned).`,
4468
+ };
4469
+ }
4470
+ return { error: insertErr.message };
4471
+ }
4472
+ return {
4473
+ success: true,
4474
+ created: (inserted || []).length,
4475
+ skipped: 0,
4476
+ tester: { id: tester.id, name: tester.name },
4477
+ testRun: args.test_run_id || null,
4478
+ message: `Assigned ${(inserted || []).length} test(s) to ${tester.name}.`,
4479
+ };
4480
+ }
4481
+ async function getTesterWorkload(args) {
4482
+ if (!isValidUUID(args.tester_id)) {
4483
+ return { error: 'Invalid tester_id format' };
4484
+ }
4485
+ // Get tester info
4486
+ const { data: tester, error: testerErr } = await supabase
4487
+ .from('testers')
4488
+ .select('id, name, email, status, platforms, tier')
4489
+ .eq('id', args.tester_id)
4490
+ .eq('project_id', PROJECT_ID)
4491
+ .single();
4492
+ if (testerErr || !tester) {
4493
+ return { error: 'Tester not found in this project' };
4494
+ }
4495
+ // Get all assignments for this tester in this project
4496
+ const { data: assignments, error: assignErr } = await supabase
4497
+ .from('test_assignments')
4498
+ .select(`
4499
+ id,
4500
+ status,
4501
+ assigned_at,
4502
+ completed_at,
4503
+ test_case:test_cases(test_key, title, priority),
4504
+ test_run:test_runs(name)
4505
+ `)
4506
+ .eq('project_id', PROJECT_ID)
4507
+ .eq('tester_id', args.tester_id)
4508
+ .order('assigned_at', { ascending: false });
4509
+ if (assignErr) {
4510
+ return { error: assignErr.message };
4511
+ }
4512
+ const all = assignments || [];
4513
+ // Count by status
4514
+ const counts = {
4515
+ pending: 0,
4516
+ in_progress: 0,
4517
+ passed: 0,
4518
+ failed: 0,
4519
+ blocked: 0,
4520
+ skipped: 0,
4521
+ };
4522
+ for (const a of all) {
4523
+ counts[a.status] = (counts[a.status] || 0) + 1;
4524
+ }
4525
+ return {
4526
+ tester: {
4527
+ id: tester.id,
4528
+ name: tester.name,
4529
+ email: tester.email,
4530
+ status: tester.status,
4531
+ platforms: tester.platforms,
4532
+ tier: tester.tier,
4533
+ },
4534
+ totalAssignments: all.length,
4535
+ counts,
4536
+ activeLoad: counts.pending + counts.in_progress,
4537
+ recentAssignments: all.slice(0, 10).map((a) => ({
4538
+ id: a.id,
4539
+ status: a.status,
4540
+ assignedAt: a.assigned_at,
4541
+ completedAt: a.completed_at,
4542
+ testCase: a.test_case ? {
4543
+ testKey: a.test_case.test_key,
4544
+ title: a.test_case.title,
4545
+ priority: a.test_case.priority,
4546
+ } : null,
4547
+ testRun: a.test_run?.name || null,
4548
+ })),
4549
+ };
4550
+ }
4039
4551
  // Main server setup
4040
4552
  async function main() {
4041
4553
  initSupabase();
@@ -4171,6 +4683,25 @@ async function main() {
4171
4683
  case 'get_testing_patterns':
4172
4684
  result = await getTestingPatterns(args);
4173
4685
  break;
4686
+ // === TESTER & ASSIGNMENT MANAGEMENT ===
4687
+ case 'list_testers':
4688
+ result = await listTesters(args);
4689
+ break;
4690
+ case 'list_test_runs':
4691
+ result = await listTestRuns(args);
4692
+ break;
4693
+ case 'create_test_run':
4694
+ result = await createTestRun(args);
4695
+ break;
4696
+ case 'list_test_assignments':
4697
+ result = await listTestAssignments(args);
4698
+ break;
4699
+ case 'assign_tests':
4700
+ result = await assignTests(args);
4701
+ break;
4702
+ case 'get_tester_workload':
4703
+ result = await getTesterWorkload(args);
4704
+ break;
4174
4705
  default:
4175
4706
  return {
4176
4707
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
@@ -4190,13 +4721,17 @@ async function main() {
4190
4721
  });
4191
4722
  // Handle resource listing (reports as resources)
4192
4723
  server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
4193
- const { data } = await supabase
4724
+ const { data, error } = await supabase
4194
4725
  .from('reports')
4195
4726
  .select('id, description, report_type, severity')
4196
4727
  .eq('project_id', PROJECT_ID)
4197
4728
  .eq('status', 'new')
4198
4729
  .order('created_at', { ascending: false })
4199
4730
  .limit(10);
4731
+ if (error) {
4732
+ console.error('Failed to list resources:', error.message);
4733
+ return { resources: [] };
4734
+ }
4200
4735
  return {
4201
4736
  resources: (data || []).map(r => ({
4202
4737
  uri: `bugbear://reports/${r.id}`,