@bbearai/mcp-server 0.3.0 → 0.3.3

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 +543 -0
  2. package/package.json +1 -1
  3. package/src/index.ts +627 -4
package/dist/index.js CHANGED
@@ -722,6 +722,159 @@ const tools = [
722
722
  },
723
723
  },
724
724
  },
725
+ // === SPRINT 5: QA INTELLIGENCE EXPANSION ===
726
+ {
727
+ name: 'get_qa_sessions',
728
+ description: 'Get QA session history showing exploratory testing sessions. Sessions capture findings that may or may not become formal bugs. Use this to understand tester exploration patterns and unstructured discoveries.',
729
+ inputSchema: {
730
+ type: 'object',
731
+ properties: {
732
+ status: {
733
+ type: 'string',
734
+ enum: ['active', 'completed', 'all'],
735
+ description: 'Filter by session status (default: all)',
736
+ },
737
+ tester_id: {
738
+ type: 'string',
739
+ description: 'Filter by specific tester UUID',
740
+ },
741
+ limit: {
742
+ type: 'number',
743
+ description: 'Maximum sessions to return (default: 20)',
744
+ },
745
+ include_findings: {
746
+ type: 'boolean',
747
+ description: 'Include session findings in response (default: true)',
748
+ },
749
+ },
750
+ },
751
+ },
752
+ {
753
+ name: 'get_qa_alerts',
754
+ description: 'Get active QA alerts including hot spots (routes with many bugs), coverage gaps (untested areas), and track gaps. Alerts are proactively generated to highlight areas needing attention.',
755
+ inputSchema: {
756
+ type: 'object',
757
+ properties: {
758
+ severity: {
759
+ type: 'string',
760
+ enum: ['critical', 'warning', 'info', 'all'],
761
+ description: 'Filter by alert severity (default: all)',
762
+ },
763
+ type: {
764
+ type: 'string',
765
+ enum: ['hot_spot', 'coverage_gap', 'track_gap', 'regression', 'all'],
766
+ description: 'Filter by alert type (default: all)',
767
+ },
768
+ status: {
769
+ type: 'string',
770
+ enum: ['active', 'acknowledged', 'all'],
771
+ description: 'Filter by status (default: active)',
772
+ },
773
+ refresh: {
774
+ type: 'boolean',
775
+ description: 'Refresh alerts by running detection (default: false)',
776
+ },
777
+ },
778
+ },
779
+ },
780
+ {
781
+ name: 'get_deployment_analysis',
782
+ description: 'Get deployment history with risk analysis. Shows what files/routes were affected by each deploy and the calculated risk score. Use this to understand deployment impact on QA.',
783
+ inputSchema: {
784
+ type: 'object',
785
+ properties: {
786
+ deployment_id: {
787
+ type: 'string',
788
+ description: 'Get specific deployment by ID',
789
+ },
790
+ environment: {
791
+ type: 'string',
792
+ enum: ['production', 'preview', 'staging', 'all'],
793
+ description: 'Filter by environment (default: all)',
794
+ },
795
+ limit: {
796
+ type: 'number',
797
+ description: 'Maximum deployments to return (default: 10)',
798
+ },
799
+ include_testing_priority: {
800
+ type: 'boolean',
801
+ description: 'Include testing priority recommendations (default: true)',
802
+ },
803
+ },
804
+ },
805
+ },
806
+ {
807
+ name: 'get_tester_recommendations',
808
+ description: 'Get recommended testers for a test case based on their expertise, track strengths, and past performance. Use this for smart test assignment.',
809
+ inputSchema: {
810
+ type: 'object',
811
+ properties: {
812
+ test_case_id: {
813
+ type: 'string',
814
+ description: 'Test case UUID to get recommendations for (required)',
815
+ },
816
+ limit: {
817
+ type: 'number',
818
+ description: 'Maximum recommendations to return (default: 5)',
819
+ },
820
+ },
821
+ required: ['test_case_id'],
822
+ },
823
+ },
824
+ {
825
+ name: 'analyze_commit_for_testing',
826
+ description: 'Analyze a commit to suggest what should be tested. Maps changed files to routes/features and checks bug history to provide prioritized testing recommendations.',
827
+ inputSchema: {
828
+ type: 'object',
829
+ properties: {
830
+ commit_sha: {
831
+ type: 'string',
832
+ description: 'Git commit SHA to analyze',
833
+ },
834
+ files_changed: {
835
+ type: 'array',
836
+ items: { type: 'string' },
837
+ description: 'List of files changed in the commit',
838
+ },
839
+ commit_message: {
840
+ type: 'string',
841
+ description: 'Commit message for context',
842
+ },
843
+ record_deployment: {
844
+ type: 'boolean',
845
+ description: 'Record this as a deployment for tracking (default: false)',
846
+ },
847
+ },
848
+ required: ['files_changed'],
849
+ },
850
+ },
851
+ {
852
+ name: 'get_testing_patterns',
853
+ description: 'Get common testing patterns from the curated pattern library. Patterns are based on public knowledge (OWASP, WCAG, framework docs) NOT customer data. Use this to suggest tests for specific feature types.',
854
+ inputSchema: {
855
+ type: 'object',
856
+ properties: {
857
+ feature_type: {
858
+ type: 'string',
859
+ enum: ['form', 'auth', 'payment', 'file_upload', 'search', 'navigation', 'settings', 'dashboard', 'checkout', 'media', 'async', 'error', 'rendering'],
860
+ description: 'Type of feature to get patterns for',
861
+ },
862
+ framework: {
863
+ type: 'string',
864
+ description: 'Framework to filter patterns (react, nextjs, vue, etc.)',
865
+ },
866
+ tracks: {
867
+ type: 'array',
868
+ items: { type: 'string' },
869
+ description: 'QA tracks to focus on (functional, security, accessibility, performance)',
870
+ },
871
+ search: {
872
+ type: 'string',
873
+ description: 'Search patterns by keyword instead of category',
874
+ },
875
+ },
876
+ },
877
+ },
725
878
  ];
726
879
  // Tool handlers
727
880
  async function listReports(args) {
@@ -2134,6 +2287,353 @@ async function getQAHealth(args) {
2134
2287
  },
2135
2288
  };
2136
2289
  }
2290
+ // === SPRINT 5: NEW HANDLER FUNCTIONS ===
2291
+ async function getQASessions(args) {
2292
+ const status = args.status || 'all';
2293
+ const limit = Math.min(args.limit || 20, 50);
2294
+ const includeFindings = args.include_findings !== false;
2295
+ let query = supabase
2296
+ .from('qa_sessions')
2297
+ .select(`
2298
+ id, focus_area, track, platform, started_at, ended_at,
2299
+ notes, routes_covered, status, duration_minutes,
2300
+ findings_count, bugs_filed, created_at,
2301
+ tester:testers(id, name, email)
2302
+ `)
2303
+ .eq('project_id', PROJECT_ID)
2304
+ .order('started_at', { ascending: false })
2305
+ .limit(limit);
2306
+ if (status !== 'all') {
2307
+ query = query.eq('status', status);
2308
+ }
2309
+ if (args.tester_id && isValidUUID(args.tester_id)) {
2310
+ query = query.eq('tester_id', args.tester_id);
2311
+ }
2312
+ const { data: sessions, error } = await query;
2313
+ if (error) {
2314
+ return { error: error.message };
2315
+ }
2316
+ // Optionally load findings for each session
2317
+ let sessionsWithFindings = sessions || [];
2318
+ if (includeFindings && sessions && sessions.length > 0) {
2319
+ const sessionIds = sessions.map(s => s.id);
2320
+ const { data: findings } = await supabase
2321
+ .from('qa_findings')
2322
+ .select('id, session_id, type, severity, title, description, route, converted_to_bug_id, dismissed')
2323
+ .in('session_id', sessionIds);
2324
+ const findingsList = findings || [];
2325
+ const findingsBySession = findingsList.reduce((acc, f) => {
2326
+ acc[f.session_id] = acc[f.session_id] || [];
2327
+ acc[f.session_id].push(f);
2328
+ return acc;
2329
+ }, {});
2330
+ sessionsWithFindings = sessions.map(s => ({
2331
+ ...s,
2332
+ findings: findingsBySession[s.id] || [],
2333
+ }));
2334
+ }
2335
+ const summary = {
2336
+ total: sessionsWithFindings.length,
2337
+ active: sessionsWithFindings.filter(s => s.status === 'active').length,
2338
+ completed: sessionsWithFindings.filter(s => s.status === 'completed').length,
2339
+ totalFindings: sessionsWithFindings.reduce((sum, s) => sum + (s.findings_count || 0), 0),
2340
+ totalBugsFiled: sessionsWithFindings.reduce((sum, s) => sum + (s.bugs_filed || 0), 0),
2341
+ };
2342
+ return {
2343
+ sessions: sessionsWithFindings,
2344
+ summary,
2345
+ };
2346
+ }
2347
+ async function getQAAlerts(args) {
2348
+ const severity = args.severity || 'all';
2349
+ const type = args.type || 'all';
2350
+ const status = args.status || 'active';
2351
+ // Optionally refresh alerts
2352
+ if (args.refresh) {
2353
+ await supabase.rpc('detect_all_alerts', { p_project_id: PROJECT_ID });
2354
+ }
2355
+ let query = supabase
2356
+ .from('qa_alerts')
2357
+ .select('*')
2358
+ .eq('project_id', PROJECT_ID)
2359
+ .order('severity', { ascending: true }) // critical first
2360
+ .order('created_at', { ascending: false });
2361
+ if (severity !== 'all') {
2362
+ query = query.eq('severity', severity);
2363
+ }
2364
+ if (type !== 'all') {
2365
+ query = query.eq('type', type);
2366
+ }
2367
+ if (status !== 'all') {
2368
+ query = query.eq('status', status);
2369
+ }
2370
+ const { data: alerts, error } = await query;
2371
+ if (error) {
2372
+ return { error: error.message };
2373
+ }
2374
+ const summary = {
2375
+ total: alerts?.length || 0,
2376
+ critical: alerts?.filter(a => a.severity === 'critical').length || 0,
2377
+ warning: alerts?.filter(a => a.severity === 'warning').length || 0,
2378
+ info: alerts?.filter(a => a.severity === 'info').length || 0,
2379
+ byType: {
2380
+ hot_spot: alerts?.filter(a => a.type === 'hot_spot').length || 0,
2381
+ coverage_gap: alerts?.filter(a => a.type === 'coverage_gap').length || 0,
2382
+ track_gap: alerts?.filter(a => a.type === 'track_gap').length || 0,
2383
+ },
2384
+ };
2385
+ return {
2386
+ alerts: alerts?.map(a => ({
2387
+ id: a.id,
2388
+ type: a.type,
2389
+ severity: a.severity,
2390
+ title: a.title,
2391
+ description: a.description,
2392
+ route: a.trigger_route,
2393
+ track: a.trigger_track,
2394
+ recommendation: a.recommendation,
2395
+ action_type: a.action_type,
2396
+ status: a.status,
2397
+ created_at: a.created_at,
2398
+ })),
2399
+ summary,
2400
+ };
2401
+ }
2402
+ async function getDeploymentAnalysis(args) {
2403
+ const limit = Math.min(args.limit || 10, 50);
2404
+ const includeTestingPriority = args.include_testing_priority !== false;
2405
+ if (args.deployment_id && isValidUUID(args.deployment_id)) {
2406
+ // Get specific deployment
2407
+ const { data: deployment, error } = await supabase
2408
+ .from('deployments')
2409
+ .select('*')
2410
+ .eq('id', args.deployment_id)
2411
+ .eq('project_id', PROJECT_ID)
2412
+ .single();
2413
+ if (error) {
2414
+ return { error: error.message };
2415
+ }
2416
+ return { deployment };
2417
+ }
2418
+ // Get deployment history
2419
+ let query = supabase
2420
+ .from('deployments')
2421
+ .select('*')
2422
+ .eq('project_id', PROJECT_ID)
2423
+ .order('deployed_at', { ascending: false })
2424
+ .limit(limit);
2425
+ if (args.environment && args.environment !== 'all') {
2426
+ query = query.eq('environment', args.environment);
2427
+ }
2428
+ const { data: deployments, error } = await query;
2429
+ if (error) {
2430
+ return { error: error.message };
2431
+ }
2432
+ const summary = {
2433
+ total: deployments?.length || 0,
2434
+ avgRiskScore: deployments?.length
2435
+ ? Math.round(deployments.reduce((sum, d) => sum + (d.risk_score || 0), 0) / deployments.length)
2436
+ : 0,
2437
+ highRisk: deployments?.filter(d => (d.risk_score || 0) >= 70).length || 0,
2438
+ verified: deployments?.filter(d => d.verified_at).length || 0,
2439
+ };
2440
+ return {
2441
+ deployments: deployments?.map(d => ({
2442
+ id: d.id,
2443
+ environment: d.environment,
2444
+ commit_sha: d.commit_sha,
2445
+ commit_message: d.commit_message,
2446
+ branch: d.branch,
2447
+ deployed_at: d.deployed_at,
2448
+ risk_score: d.risk_score,
2449
+ routes_affected: d.routes_affected,
2450
+ testing_priority: includeTestingPriority ? d.testing_priority : undefined,
2451
+ verified: !!d.verified_at,
2452
+ })),
2453
+ summary,
2454
+ };
2455
+ }
2456
+ async function getTesterRecommendations(args) {
2457
+ if (!isValidUUID(args.test_case_id)) {
2458
+ return { error: 'Invalid test_case_id format' };
2459
+ }
2460
+ const limit = Math.min(args.limit || 5, 10);
2461
+ const { data: recommendations, error } = await supabase.rpc('get_tester_recommendations', {
2462
+ p_test_case_id: args.test_case_id,
2463
+ p_limit: limit,
2464
+ });
2465
+ if (error) {
2466
+ return { error: error.message };
2467
+ }
2468
+ // Get test case info for context
2469
+ const { data: testCase } = await supabase
2470
+ .from('test_cases')
2471
+ .select('test_key, title, track:qa_tracks(name)')
2472
+ .eq('id', args.test_case_id)
2473
+ .single();
2474
+ return {
2475
+ test_case: testCase ? {
2476
+ id: args.test_case_id,
2477
+ test_key: testCase.test_key,
2478
+ title: testCase.title,
2479
+ track: testCase.track?.name,
2480
+ } : null,
2481
+ recommendations: recommendations?.map((r) => ({
2482
+ tester_id: r.tester_id,
2483
+ name: r.tester_name,
2484
+ email: r.tester_email,
2485
+ match_score: r.match_score,
2486
+ reasons: r.match_reasons,
2487
+ })) || [],
2488
+ };
2489
+ }
2490
+ async function analyzeCommitForTesting(args) {
2491
+ const filesChanged = args.files_changed || [];
2492
+ // Map files to routes using file_route_mapping
2493
+ const { data: mappings } = await supabase
2494
+ .from('file_route_mapping')
2495
+ .select('file_pattern, route, feature, confidence')
2496
+ .eq('project_id', PROJECT_ID);
2497
+ const affectedRoutes = [];
2498
+ for (const mapping of mappings || []) {
2499
+ const matchedFiles = filesChanged.filter(file => {
2500
+ const pattern = mapping.file_pattern
2501
+ .replace(/\*\*/g, '.*')
2502
+ .replace(/\*/g, '[^/]*');
2503
+ return new RegExp(pattern).test(file);
2504
+ });
2505
+ if (matchedFiles.length > 0) {
2506
+ affectedRoutes.push({
2507
+ route: mapping.route,
2508
+ feature: mapping.feature || undefined,
2509
+ confidence: mapping.confidence,
2510
+ matched_files: matchedFiles,
2511
+ });
2512
+ }
2513
+ }
2514
+ // Get bug history for affected routes
2515
+ const routes = affectedRoutes.map(r => r.route);
2516
+ let bugHistory = [];
2517
+ if (routes.length > 0) {
2518
+ const { data: bugs } = await supabase
2519
+ .from('reports')
2520
+ .select('id, severity, description, route, created_at')
2521
+ .eq('project_id', PROJECT_ID)
2522
+ .eq('report_type', 'bug')
2523
+ .in('route', routes)
2524
+ .gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
2525
+ bugHistory = bugs || [];
2526
+ }
2527
+ // Calculate risk score
2528
+ let riskScore = 0;
2529
+ riskScore += Math.min(filesChanged.length * 2, 20); // File count
2530
+ riskScore += Math.min(affectedRoutes.length * 5, 30); // Route count
2531
+ riskScore += Math.min(bugHistory.length * 10, 50); // Bug history
2532
+ // Generate testing recommendations
2533
+ const recommendations = affectedRoutes.map(r => {
2534
+ const routeBugs = bugHistory.filter(b => b.route === r.route);
2535
+ const priority = routeBugs.length >= 3 ? 'critical' :
2536
+ routeBugs.length >= 1 ? 'high' : 'medium';
2537
+ return {
2538
+ route: r.route,
2539
+ feature: r.feature,
2540
+ priority,
2541
+ reason: routeBugs.length > 0
2542
+ ? `${routeBugs.length} bug(s) in last 30 days`
2543
+ : 'Code changed in this area',
2544
+ recent_bugs: routeBugs.length,
2545
+ };
2546
+ }).sort((a, b) => {
2547
+ const order = { critical: 0, high: 1, medium: 2 };
2548
+ return order[a.priority] - order[b.priority];
2549
+ });
2550
+ // Optionally record as deployment
2551
+ if (args.record_deployment) {
2552
+ await supabase.rpc('record_deployment', {
2553
+ p_project_id: PROJECT_ID,
2554
+ p_environment: 'production',
2555
+ p_commit_sha: args.commit_sha || null,
2556
+ p_commit_message: args.commit_message || null,
2557
+ p_files_changed: filesChanged,
2558
+ p_webhook_source: 'mcp',
2559
+ });
2560
+ }
2561
+ return {
2562
+ commit_sha: args.commit_sha,
2563
+ files_analyzed: filesChanged.length,
2564
+ risk_score: Math.min(riskScore, 100),
2565
+ affected_routes: affectedRoutes,
2566
+ bug_history_summary: {
2567
+ total: bugHistory.length,
2568
+ critical: bugHistory.filter(b => b.severity === 'critical').length,
2569
+ high: bugHistory.filter(b => b.severity === 'high').length,
2570
+ },
2571
+ testing_recommendations: recommendations,
2572
+ deployment_recorded: args.record_deployment || false,
2573
+ };
2574
+ }
2575
+ async function getTestingPatterns(args) {
2576
+ let patterns = [];
2577
+ if (args.search) {
2578
+ // Search patterns by keyword
2579
+ const { data, error } = await supabase.rpc('search_patterns', {
2580
+ p_query: args.search,
2581
+ p_limit: 20,
2582
+ });
2583
+ if (!error)
2584
+ patterns = data || [];
2585
+ }
2586
+ else if (args.feature_type) {
2587
+ // Get patterns for feature type
2588
+ const { data, error } = await supabase.rpc('get_patterns_for_feature', {
2589
+ p_category: args.feature_type,
2590
+ p_framework: args.framework || null,
2591
+ p_tracks: args.tracks || null,
2592
+ });
2593
+ if (!error)
2594
+ patterns = data || [];
2595
+ }
2596
+ else {
2597
+ // Get all patterns (limited)
2598
+ const { data, error } = await supabase
2599
+ .from('qa_patterns')
2600
+ .select('*')
2601
+ .eq('is_active', true)
2602
+ .order('severity')
2603
+ .limit(30);
2604
+ if (!error)
2605
+ patterns = data || [];
2606
+ }
2607
+ const summary = {
2608
+ total: patterns.length,
2609
+ by_severity: {
2610
+ critical: patterns.filter(p => p.severity === 'critical').length,
2611
+ high: patterns.filter(p => p.severity === 'high').length,
2612
+ medium: patterns.filter(p => p.severity === 'medium').length,
2613
+ low: patterns.filter(p => p.severity === 'low').length,
2614
+ },
2615
+ by_track: patterns.reduce((acc, p) => {
2616
+ acc[p.track] = (acc[p.track] || 0) + 1;
2617
+ return acc;
2618
+ }, {}),
2619
+ };
2620
+ return {
2621
+ patterns: patterns.map(p => ({
2622
+ title: p.title,
2623
+ category: p.category,
2624
+ track: p.track,
2625
+ severity: p.severity,
2626
+ description: p.description,
2627
+ why_it_happens: p.why_it_happens,
2628
+ suggested_tests: p.suggested_tests,
2629
+ common_fix: p.common_fix,
2630
+ source: p.source,
2631
+ frameworks: p.frameworks,
2632
+ })),
2633
+ summary,
2634
+ note: 'Patterns are from public knowledge (OWASP, WCAG, framework docs), not customer data.',
2635
+ };
2636
+ }
2137
2637
  async function analyzeChangesForTests(args) {
2138
2638
  // Get existing tests to check coverage
2139
2639
  const { data: existingTests } = await supabase
@@ -2496,6 +2996,27 @@ async function createBugReport(args) {
2496
2996
  if (args.suggested_fix) {
2497
2997
  codeContext.suggested_fix = args.suggested_fix;
2498
2998
  }
2999
+ // Find a reporter_id: try to get the project owner or first tester
3000
+ let reporterId = null;
3001
+ const { data: project } = await supabase
3002
+ .from('projects')
3003
+ .select('owner_id')
3004
+ .eq('id', PROJECT_ID)
3005
+ .single();
3006
+ if (project?.owner_id) {
3007
+ reporterId = project.owner_id;
3008
+ }
3009
+ else {
3010
+ // Fallback: use the first tester for this project
3011
+ const { data: testers } = await supabase
3012
+ .from('testers')
3013
+ .select('id')
3014
+ .eq('project_id', PROJECT_ID)
3015
+ .limit(1);
3016
+ if (testers && testers.length > 0) {
3017
+ reporterId = testers[0].id;
3018
+ }
3019
+ }
2499
3020
  const report = {
2500
3021
  project_id: PROJECT_ID,
2501
3022
  report_type: 'bug',
@@ -2514,6 +3035,9 @@ async function createBugReport(args) {
2514
3035
  },
2515
3036
  code_context: codeContext,
2516
3037
  };
3038
+ if (reporterId) {
3039
+ report.reporter_id = reporterId;
3040
+ }
2517
3041
  const { data, error } = await supabase
2518
3042
  .from('reports')
2519
3043
  .insert(report)
@@ -3491,6 +4015,25 @@ async function main() {
3491
4015
  case 'complete_fix_request':
3492
4016
  result = await completeFixRequest(args);
3493
4017
  break;
4018
+ // === SPRINT 5: NEW MCP TOOLS ===
4019
+ case 'get_qa_sessions':
4020
+ result = await getQASessions(args);
4021
+ break;
4022
+ case 'get_qa_alerts':
4023
+ result = await getQAAlerts(args);
4024
+ break;
4025
+ case 'get_deployment_analysis':
4026
+ result = await getDeploymentAnalysis(args);
4027
+ break;
4028
+ case 'get_tester_recommendations':
4029
+ result = await getTesterRecommendations(args);
4030
+ break;
4031
+ case 'analyze_commit_for_testing':
4032
+ result = await analyzeCommitForTesting(args);
4033
+ break;
4034
+ case 'get_testing_patterns':
4035
+ result = await getTestingPatterns(args);
4036
+ break;
3494
4037
  default:
3495
4038
  return {
3496
4039
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/mcp-server",
3
- "version": "0.3.0",
3
+ "version": "0.3.3",
4
4
  "description": "MCP server for BugBear - allows Claude Code to query bug reports",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -739,6 +739,159 @@ const tools = [
739
739
  },
740
740
  },
741
741
  },
742
+ // === SPRINT 5: QA INTELLIGENCE EXPANSION ===
743
+ {
744
+ name: 'get_qa_sessions',
745
+ description: 'Get QA session history showing exploratory testing sessions. Sessions capture findings that may or may not become formal bugs. Use this to understand tester exploration patterns and unstructured discoveries.',
746
+ inputSchema: {
747
+ type: 'object' as const,
748
+ properties: {
749
+ status: {
750
+ type: 'string',
751
+ enum: ['active', 'completed', 'all'],
752
+ description: 'Filter by session status (default: all)',
753
+ },
754
+ tester_id: {
755
+ type: 'string',
756
+ description: 'Filter by specific tester UUID',
757
+ },
758
+ limit: {
759
+ type: 'number',
760
+ description: 'Maximum sessions to return (default: 20)',
761
+ },
762
+ include_findings: {
763
+ type: 'boolean',
764
+ description: 'Include session findings in response (default: true)',
765
+ },
766
+ },
767
+ },
768
+ },
769
+ {
770
+ name: 'get_qa_alerts',
771
+ description: 'Get active QA alerts including hot spots (routes with many bugs), coverage gaps (untested areas), and track gaps. Alerts are proactively generated to highlight areas needing attention.',
772
+ inputSchema: {
773
+ type: 'object' as const,
774
+ properties: {
775
+ severity: {
776
+ type: 'string',
777
+ enum: ['critical', 'warning', 'info', 'all'],
778
+ description: 'Filter by alert severity (default: all)',
779
+ },
780
+ type: {
781
+ type: 'string',
782
+ enum: ['hot_spot', 'coverage_gap', 'track_gap', 'regression', 'all'],
783
+ description: 'Filter by alert type (default: all)',
784
+ },
785
+ status: {
786
+ type: 'string',
787
+ enum: ['active', 'acknowledged', 'all'],
788
+ description: 'Filter by status (default: active)',
789
+ },
790
+ refresh: {
791
+ type: 'boolean',
792
+ description: 'Refresh alerts by running detection (default: false)',
793
+ },
794
+ },
795
+ },
796
+ },
797
+ {
798
+ name: 'get_deployment_analysis',
799
+ description: 'Get deployment history with risk analysis. Shows what files/routes were affected by each deploy and the calculated risk score. Use this to understand deployment impact on QA.',
800
+ inputSchema: {
801
+ type: 'object' as const,
802
+ properties: {
803
+ deployment_id: {
804
+ type: 'string',
805
+ description: 'Get specific deployment by ID',
806
+ },
807
+ environment: {
808
+ type: 'string',
809
+ enum: ['production', 'preview', 'staging', 'all'],
810
+ description: 'Filter by environment (default: all)',
811
+ },
812
+ limit: {
813
+ type: 'number',
814
+ description: 'Maximum deployments to return (default: 10)',
815
+ },
816
+ include_testing_priority: {
817
+ type: 'boolean',
818
+ description: 'Include testing priority recommendations (default: true)',
819
+ },
820
+ },
821
+ },
822
+ },
823
+ {
824
+ name: 'get_tester_recommendations',
825
+ description: 'Get recommended testers for a test case based on their expertise, track strengths, and past performance. Use this for smart test assignment.',
826
+ inputSchema: {
827
+ type: 'object' as const,
828
+ properties: {
829
+ test_case_id: {
830
+ type: 'string',
831
+ description: 'Test case UUID to get recommendations for (required)',
832
+ },
833
+ limit: {
834
+ type: 'number',
835
+ description: 'Maximum recommendations to return (default: 5)',
836
+ },
837
+ },
838
+ required: ['test_case_id'],
839
+ },
840
+ },
841
+ {
842
+ name: 'analyze_commit_for_testing',
843
+ description: 'Analyze a commit to suggest what should be tested. Maps changed files to routes/features and checks bug history to provide prioritized testing recommendations.',
844
+ inputSchema: {
845
+ type: 'object' as const,
846
+ properties: {
847
+ commit_sha: {
848
+ type: 'string',
849
+ description: 'Git commit SHA to analyze',
850
+ },
851
+ files_changed: {
852
+ type: 'array',
853
+ items: { type: 'string' },
854
+ description: 'List of files changed in the commit',
855
+ },
856
+ commit_message: {
857
+ type: 'string',
858
+ description: 'Commit message for context',
859
+ },
860
+ record_deployment: {
861
+ type: 'boolean',
862
+ description: 'Record this as a deployment for tracking (default: false)',
863
+ },
864
+ },
865
+ required: ['files_changed'],
866
+ },
867
+ },
868
+ {
869
+ name: 'get_testing_patterns',
870
+ description: 'Get common testing patterns from the curated pattern library. Patterns are based on public knowledge (OWASP, WCAG, framework docs) NOT customer data. Use this to suggest tests for specific feature types.',
871
+ inputSchema: {
872
+ type: 'object' as const,
873
+ properties: {
874
+ feature_type: {
875
+ type: 'string',
876
+ enum: ['form', 'auth', 'payment', 'file_upload', 'search', 'navigation', 'settings', 'dashboard', 'checkout', 'media', 'async', 'error', 'rendering'],
877
+ description: 'Type of feature to get patterns for',
878
+ },
879
+ framework: {
880
+ type: 'string',
881
+ description: 'Framework to filter patterns (react, nextjs, vue, etc.)',
882
+ },
883
+ tracks: {
884
+ type: 'array',
885
+ items: { type: 'string' },
886
+ description: 'QA tracks to focus on (functional, security, accessibility, performance)',
887
+ },
888
+ search: {
889
+ type: 'string',
890
+ description: 'Search patterns by keyword instead of category',
891
+ },
892
+ },
893
+ },
894
+ },
742
895
  ];
743
896
 
744
897
  // Tool handlers
@@ -750,7 +903,7 @@ async function listReports(args: {
750
903
  }) {
751
904
  let query = supabase
752
905
  .from('reports')
753
- .select('id, report_type, severity, status, description, app_context, created_at, tester:testers(name, email)')
906
+ .select('id, report_type, severity, status, description, app_context, created_at, reporter_name, reporter_email, tester:testers(name, email)')
754
907
  .eq('project_id', PROJECT_ID)
755
908
  .order('created_at', { ascending: false })
756
909
  .limit(Math.min(args.limit || 10, 50));
@@ -773,7 +926,7 @@ async function listReports(args: {
773
926
  status: r.status,
774
927
  description: r.description,
775
928
  route: (r.app_context as any)?.currentRoute,
776
- reporter: (r.tester as any)?.name || 'Anonymous',
929
+ reporter: (r.tester as any)?.name || (r as any).reporter_name || 'Anonymous',
777
930
  created_at: r.created_at,
778
931
  })),
779
932
  total: data?.length || 0,
@@ -812,7 +965,10 @@ async function getReport(args: { report_id: string }) {
812
965
  reporter: data.tester ? {
813
966
  name: data.tester.name,
814
967
  email: data.tester.email,
815
- } : null,
968
+ } : (data.reporter_name ? {
969
+ name: data.reporter_name,
970
+ email: data.reporter_email,
971
+ } : null),
816
972
  track: data.track ? {
817
973
  name: data.track.name,
818
974
  icon: data.track.icon,
@@ -2409,6 +2565,429 @@ async function getQAHealth(args: {
2409
2565
  };
2410
2566
  }
2411
2567
 
2568
+ // === SPRINT 5: NEW HANDLER FUNCTIONS ===
2569
+
2570
+ async function getQASessions(args: {
2571
+ status?: 'active' | 'completed' | 'all';
2572
+ tester_id?: string;
2573
+ limit?: number;
2574
+ include_findings?: boolean;
2575
+ }) {
2576
+ const status = args.status || 'all';
2577
+ const limit = Math.min(args.limit || 20, 50);
2578
+ const includeFindings = args.include_findings !== false;
2579
+
2580
+ let query = supabase
2581
+ .from('qa_sessions')
2582
+ .select(`
2583
+ id, focus_area, track, platform, started_at, ended_at,
2584
+ notes, routes_covered, status, duration_minutes,
2585
+ findings_count, bugs_filed, created_at,
2586
+ tester:testers(id, name, email)
2587
+ `)
2588
+ .eq('project_id', PROJECT_ID)
2589
+ .order('started_at', { ascending: false })
2590
+ .limit(limit);
2591
+
2592
+ if (status !== 'all') {
2593
+ query = query.eq('status', status);
2594
+ }
2595
+
2596
+ if (args.tester_id && isValidUUID(args.tester_id)) {
2597
+ query = query.eq('tester_id', args.tester_id);
2598
+ }
2599
+
2600
+ const { data: sessions, error } = await query;
2601
+
2602
+ if (error) {
2603
+ return { error: error.message };
2604
+ }
2605
+
2606
+ // Optionally load findings for each session
2607
+ let sessionsWithFindings = sessions || [];
2608
+ if (includeFindings && sessions && sessions.length > 0) {
2609
+ const sessionIds = sessions.map(s => s.id);
2610
+ const { data: findings } = await supabase
2611
+ .from('qa_findings')
2612
+ .select('id, session_id, type, severity, title, description, route, converted_to_bug_id, dismissed')
2613
+ .in('session_id', sessionIds);
2614
+
2615
+ const findingsList = findings || [];
2616
+ const findingsBySession = findingsList.reduce((acc, f) => {
2617
+ acc[f.session_id] = acc[f.session_id] || [];
2618
+ acc[f.session_id].push(f);
2619
+ return acc;
2620
+ }, {} as Record<string, typeof findingsList>);
2621
+
2622
+ sessionsWithFindings = sessions.map(s => ({
2623
+ ...s,
2624
+ findings: findingsBySession[s.id] || [],
2625
+ }));
2626
+ }
2627
+
2628
+ const summary = {
2629
+ total: sessionsWithFindings.length,
2630
+ active: sessionsWithFindings.filter(s => s.status === 'active').length,
2631
+ completed: sessionsWithFindings.filter(s => s.status === 'completed').length,
2632
+ totalFindings: sessionsWithFindings.reduce((sum, s) => sum + (s.findings_count || 0), 0),
2633
+ totalBugsFiled: sessionsWithFindings.reduce((sum, s) => sum + (s.bugs_filed || 0), 0),
2634
+ };
2635
+
2636
+ return {
2637
+ sessions: sessionsWithFindings,
2638
+ summary,
2639
+ };
2640
+ }
2641
+
2642
+ async function getQAAlerts(args: {
2643
+ severity?: 'critical' | 'warning' | 'info' | 'all';
2644
+ type?: 'hot_spot' | 'coverage_gap' | 'track_gap' | 'regression' | 'all';
2645
+ status?: 'active' | 'acknowledged' | 'all';
2646
+ refresh?: boolean;
2647
+ }) {
2648
+ const severity = args.severity || 'all';
2649
+ const type = args.type || 'all';
2650
+ const status = args.status || 'active';
2651
+
2652
+ // Optionally refresh alerts
2653
+ if (args.refresh) {
2654
+ await supabase.rpc('detect_all_alerts', { p_project_id: PROJECT_ID });
2655
+ }
2656
+
2657
+ let query = supabase
2658
+ .from('qa_alerts')
2659
+ .select('*')
2660
+ .eq('project_id', PROJECT_ID)
2661
+ .order('severity', { ascending: true }) // critical first
2662
+ .order('created_at', { ascending: false });
2663
+
2664
+ if (severity !== 'all') {
2665
+ query = query.eq('severity', severity);
2666
+ }
2667
+ if (type !== 'all') {
2668
+ query = query.eq('type', type);
2669
+ }
2670
+ if (status !== 'all') {
2671
+ query = query.eq('status', status);
2672
+ }
2673
+
2674
+ const { data: alerts, error } = await query;
2675
+
2676
+ if (error) {
2677
+ return { error: error.message };
2678
+ }
2679
+
2680
+ const summary = {
2681
+ total: alerts?.length || 0,
2682
+ critical: alerts?.filter(a => a.severity === 'critical').length || 0,
2683
+ warning: alerts?.filter(a => a.severity === 'warning').length || 0,
2684
+ info: alerts?.filter(a => a.severity === 'info').length || 0,
2685
+ byType: {
2686
+ hot_spot: alerts?.filter(a => a.type === 'hot_spot').length || 0,
2687
+ coverage_gap: alerts?.filter(a => a.type === 'coverage_gap').length || 0,
2688
+ track_gap: alerts?.filter(a => a.type === 'track_gap').length || 0,
2689
+ },
2690
+ };
2691
+
2692
+ return {
2693
+ alerts: alerts?.map(a => ({
2694
+ id: a.id,
2695
+ type: a.type,
2696
+ severity: a.severity,
2697
+ title: a.title,
2698
+ description: a.description,
2699
+ route: a.trigger_route,
2700
+ track: a.trigger_track,
2701
+ recommendation: a.recommendation,
2702
+ action_type: a.action_type,
2703
+ status: a.status,
2704
+ created_at: a.created_at,
2705
+ })),
2706
+ summary,
2707
+ };
2708
+ }
2709
+
2710
+ async function getDeploymentAnalysis(args: {
2711
+ deployment_id?: string;
2712
+ environment?: 'production' | 'preview' | 'staging' | 'all';
2713
+ limit?: number;
2714
+ include_testing_priority?: boolean;
2715
+ }) {
2716
+ const limit = Math.min(args.limit || 10, 50);
2717
+ const includeTestingPriority = args.include_testing_priority !== false;
2718
+
2719
+ if (args.deployment_id && isValidUUID(args.deployment_id)) {
2720
+ // Get specific deployment
2721
+ const { data: deployment, error } = await supabase
2722
+ .from('deployments')
2723
+ .select('*')
2724
+ .eq('id', args.deployment_id)
2725
+ .eq('project_id', PROJECT_ID)
2726
+ .single();
2727
+
2728
+ if (error) {
2729
+ return { error: error.message };
2730
+ }
2731
+
2732
+ return { deployment };
2733
+ }
2734
+
2735
+ // Get deployment history
2736
+ let query = supabase
2737
+ .from('deployments')
2738
+ .select('*')
2739
+ .eq('project_id', PROJECT_ID)
2740
+ .order('deployed_at', { ascending: false })
2741
+ .limit(limit);
2742
+
2743
+ if (args.environment && args.environment !== 'all') {
2744
+ query = query.eq('environment', args.environment);
2745
+ }
2746
+
2747
+ const { data: deployments, error } = await query;
2748
+
2749
+ if (error) {
2750
+ return { error: error.message };
2751
+ }
2752
+
2753
+ const summary = {
2754
+ total: deployments?.length || 0,
2755
+ avgRiskScore: deployments?.length
2756
+ ? Math.round(deployments.reduce((sum, d) => sum + (d.risk_score || 0), 0) / deployments.length)
2757
+ : 0,
2758
+ highRisk: deployments?.filter(d => (d.risk_score || 0) >= 70).length || 0,
2759
+ verified: deployments?.filter(d => d.verified_at).length || 0,
2760
+ };
2761
+
2762
+ return {
2763
+ deployments: deployments?.map(d => ({
2764
+ id: d.id,
2765
+ environment: d.environment,
2766
+ commit_sha: d.commit_sha,
2767
+ commit_message: d.commit_message,
2768
+ branch: d.branch,
2769
+ deployed_at: d.deployed_at,
2770
+ risk_score: d.risk_score,
2771
+ routes_affected: d.routes_affected,
2772
+ testing_priority: includeTestingPriority ? d.testing_priority : undefined,
2773
+ verified: !!d.verified_at,
2774
+ })),
2775
+ summary,
2776
+ };
2777
+ }
2778
+
2779
+ async function getTesterRecommendations(args: {
2780
+ test_case_id: string;
2781
+ limit?: number;
2782
+ }) {
2783
+ if (!isValidUUID(args.test_case_id)) {
2784
+ return { error: 'Invalid test_case_id format' };
2785
+ }
2786
+
2787
+ const limit = Math.min(args.limit || 5, 10);
2788
+
2789
+ const { data: recommendations, error } = await supabase.rpc('get_tester_recommendations', {
2790
+ p_test_case_id: args.test_case_id,
2791
+ p_limit: limit,
2792
+ });
2793
+
2794
+ if (error) {
2795
+ return { error: error.message };
2796
+ }
2797
+
2798
+ // Get test case info for context
2799
+ const { data: testCase } = await supabase
2800
+ .from('test_cases')
2801
+ .select('test_key, title, track:qa_tracks(name)')
2802
+ .eq('id', args.test_case_id)
2803
+ .single();
2804
+
2805
+ return {
2806
+ test_case: testCase ? {
2807
+ id: args.test_case_id,
2808
+ test_key: testCase.test_key,
2809
+ title: testCase.title,
2810
+ track: (testCase.track as any)?.name,
2811
+ } : null,
2812
+ recommendations: recommendations?.map((r: any) => ({
2813
+ tester_id: r.tester_id,
2814
+ name: r.tester_name,
2815
+ email: r.tester_email,
2816
+ match_score: r.match_score,
2817
+ reasons: r.match_reasons,
2818
+ })) || [],
2819
+ };
2820
+ }
2821
+
2822
+ async function analyzeCommitForTesting(args: {
2823
+ commit_sha?: string;
2824
+ files_changed: string[];
2825
+ commit_message?: string;
2826
+ record_deployment?: boolean;
2827
+ }) {
2828
+ const filesChanged = args.files_changed || [];
2829
+
2830
+ // Map files to routes using file_route_mapping
2831
+ const { data: mappings } = await supabase
2832
+ .from('file_route_mapping')
2833
+ .select('file_pattern, route, feature, confidence')
2834
+ .eq('project_id', PROJECT_ID);
2835
+
2836
+ const affectedRoutes: Array<{ route: string; feature?: string; confidence: number; matched_files: string[] }> = [];
2837
+
2838
+ for (const mapping of mappings || []) {
2839
+ const matchedFiles = filesChanged.filter(file => {
2840
+ const pattern = mapping.file_pattern
2841
+ .replace(/\*\*/g, '.*')
2842
+ .replace(/\*/g, '[^/]*');
2843
+ return new RegExp(pattern).test(file);
2844
+ });
2845
+
2846
+ if (matchedFiles.length > 0) {
2847
+ affectedRoutes.push({
2848
+ route: mapping.route,
2849
+ feature: mapping.feature || undefined,
2850
+ confidence: mapping.confidence,
2851
+ matched_files: matchedFiles,
2852
+ });
2853
+ }
2854
+ }
2855
+
2856
+ // Get bug history for affected routes
2857
+ const routes = affectedRoutes.map(r => r.route);
2858
+ let bugHistory: any[] = [];
2859
+
2860
+ if (routes.length > 0) {
2861
+ const { data: bugs } = await supabase
2862
+ .from('reports')
2863
+ .select('id, severity, description, route, created_at')
2864
+ .eq('project_id', PROJECT_ID)
2865
+ .eq('report_type', 'bug')
2866
+ .in('route', routes)
2867
+ .gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
2868
+
2869
+ bugHistory = bugs || [];
2870
+ }
2871
+
2872
+ // Calculate risk score
2873
+ let riskScore = 0;
2874
+ riskScore += Math.min(filesChanged.length * 2, 20); // File count
2875
+ riskScore += Math.min(affectedRoutes.length * 5, 30); // Route count
2876
+ riskScore += Math.min(bugHistory.length * 10, 50); // Bug history
2877
+
2878
+ // Generate testing recommendations
2879
+ const recommendations = affectedRoutes.map(r => {
2880
+ const routeBugs = bugHistory.filter(b => b.route === r.route);
2881
+ const priority = routeBugs.length >= 3 ? 'critical' :
2882
+ routeBugs.length >= 1 ? 'high' : 'medium';
2883
+
2884
+ return {
2885
+ route: r.route,
2886
+ feature: r.feature,
2887
+ priority,
2888
+ reason: routeBugs.length > 0
2889
+ ? `${routeBugs.length} bug(s) in last 30 days`
2890
+ : 'Code changed in this area',
2891
+ recent_bugs: routeBugs.length,
2892
+ };
2893
+ }).sort((a, b) => {
2894
+ const order = { critical: 0, high: 1, medium: 2 };
2895
+ return order[a.priority as keyof typeof order] - order[b.priority as keyof typeof order];
2896
+ });
2897
+
2898
+ // Optionally record as deployment
2899
+ if (args.record_deployment) {
2900
+ await supabase.rpc('record_deployment', {
2901
+ p_project_id: PROJECT_ID,
2902
+ p_environment: 'production',
2903
+ p_commit_sha: args.commit_sha || null,
2904
+ p_commit_message: args.commit_message || null,
2905
+ p_files_changed: filesChanged,
2906
+ p_webhook_source: 'mcp',
2907
+ });
2908
+ }
2909
+
2910
+ return {
2911
+ commit_sha: args.commit_sha,
2912
+ files_analyzed: filesChanged.length,
2913
+ risk_score: Math.min(riskScore, 100),
2914
+ affected_routes: affectedRoutes,
2915
+ bug_history_summary: {
2916
+ total: bugHistory.length,
2917
+ critical: bugHistory.filter(b => b.severity === 'critical').length,
2918
+ high: bugHistory.filter(b => b.severity === 'high').length,
2919
+ },
2920
+ testing_recommendations: recommendations,
2921
+ deployment_recorded: args.record_deployment || false,
2922
+ };
2923
+ }
2924
+
2925
+ async function getTestingPatterns(args: {
2926
+ feature_type?: string;
2927
+ framework?: string;
2928
+ tracks?: string[];
2929
+ search?: string;
2930
+ }) {
2931
+ let patterns: any[] = [];
2932
+
2933
+ if (args.search) {
2934
+ // Search patterns by keyword
2935
+ const { data, error } = await supabase.rpc('search_patterns', {
2936
+ p_query: args.search,
2937
+ p_limit: 20,
2938
+ });
2939
+ if (!error) patterns = data || [];
2940
+ } else if (args.feature_type) {
2941
+ // Get patterns for feature type
2942
+ const { data, error } = await supabase.rpc('get_patterns_for_feature', {
2943
+ p_category: args.feature_type,
2944
+ p_framework: args.framework || null,
2945
+ p_tracks: args.tracks || null,
2946
+ });
2947
+ if (!error) patterns = data || [];
2948
+ } else {
2949
+ // Get all patterns (limited)
2950
+ const { data, error } = await supabase
2951
+ .from('qa_patterns')
2952
+ .select('*')
2953
+ .eq('is_active', true)
2954
+ .order('severity')
2955
+ .limit(30);
2956
+ if (!error) patterns = data || [];
2957
+ }
2958
+
2959
+ const summary = {
2960
+ total: patterns.length,
2961
+ by_severity: {
2962
+ critical: patterns.filter(p => p.severity === 'critical').length,
2963
+ high: patterns.filter(p => p.severity === 'high').length,
2964
+ medium: patterns.filter(p => p.severity === 'medium').length,
2965
+ low: patterns.filter(p => p.severity === 'low').length,
2966
+ },
2967
+ by_track: patterns.reduce((acc, p) => {
2968
+ acc[p.track] = (acc[p.track] || 0) + 1;
2969
+ return acc;
2970
+ }, {} as Record<string, number>),
2971
+ };
2972
+
2973
+ return {
2974
+ patterns: patterns.map(p => ({
2975
+ title: p.title,
2976
+ category: p.category,
2977
+ track: p.track,
2978
+ severity: p.severity,
2979
+ description: p.description,
2980
+ why_it_happens: p.why_it_happens,
2981
+ suggested_tests: p.suggested_tests,
2982
+ common_fix: p.common_fix,
2983
+ source: p.source,
2984
+ frameworks: p.frameworks,
2985
+ })),
2986
+ summary,
2987
+ note: 'Patterns are from public knowledge (OWASP, WCAG, framework docs), not customer data.',
2988
+ };
2989
+ }
2990
+
2412
2991
  async function analyzeChangesForTests(args: {
2413
2992
  changed_files: string[];
2414
2993
  change_type: 'feature' | 'bugfix' | 'refactor' | 'ui_change' | 'api_change' | 'config';
@@ -2837,7 +3416,28 @@ async function createBugReport(args: {
2837
3416
  codeContext.suggested_fix = args.suggested_fix;
2838
3417
  }
2839
3418
 
2840
- const report = {
3419
+ // Find a reporter_id: try to get the project owner or first tester
3420
+ let reporterId: string | null = null;
3421
+ const { data: project } = await supabase
3422
+ .from('projects')
3423
+ .select('owner_id')
3424
+ .eq('id', PROJECT_ID)
3425
+ .single();
3426
+ if (project?.owner_id) {
3427
+ reporterId = project.owner_id;
3428
+ } else {
3429
+ // Fallback: use the first tester for this project
3430
+ const { data: testers } = await supabase
3431
+ .from('testers')
3432
+ .select('id')
3433
+ .eq('project_id', PROJECT_ID)
3434
+ .limit(1);
3435
+ if (testers && testers.length > 0) {
3436
+ reporterId = testers[0].id;
3437
+ }
3438
+ }
3439
+
3440
+ const report: Record<string, unknown> = {
2841
3441
  project_id: PROJECT_ID,
2842
3442
  report_type: 'bug',
2843
3443
  title: args.title,
@@ -2856,6 +3456,10 @@ async function createBugReport(args: {
2856
3456
  code_context: codeContext,
2857
3457
  };
2858
3458
 
3459
+ if (reporterId) {
3460
+ report.reporter_id = reporterId;
3461
+ }
3462
+
2859
3463
  const { data, error } = await supabase
2860
3464
  .from('reports')
2861
3465
  .insert(report)
@@ -3986,6 +4590,25 @@ async function main() {
3986
4590
  case 'complete_fix_request':
3987
4591
  result = await completeFixRequest(args as any);
3988
4592
  break;
4593
+ // === SPRINT 5: NEW MCP TOOLS ===
4594
+ case 'get_qa_sessions':
4595
+ result = await getQASessions(args as any);
4596
+ break;
4597
+ case 'get_qa_alerts':
4598
+ result = await getQAAlerts(args as any);
4599
+ break;
4600
+ case 'get_deployment_analysis':
4601
+ result = await getDeploymentAnalysis(args as any);
4602
+ break;
4603
+ case 'get_tester_recommendations':
4604
+ result = await getTesterRecommendations(args as any);
4605
+ break;
4606
+ case 'analyze_commit_for_testing':
4607
+ result = await analyzeCommitForTesting(args as any);
4608
+ break;
4609
+ case 'get_testing_patterns':
4610
+ result = await getTestingPatterns(args as any);
4611
+ break;
3989
4612
  default:
3990
4613
  return {
3991
4614
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],