@gaffer-sh/mcp 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +648 -131
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -341,6 +341,112 @@ var GafferApiClient = class _GafferApiClient {
341
341
  }
342
342
  );
343
343
  }
344
+ /**
345
+ * Get coverage summary for a project
346
+ *
347
+ * @param options - Query options
348
+ * @param options.projectId - The project ID (required)
349
+ * @param options.days - Analysis period in days (default: 30)
350
+ * @returns Coverage summary with trends and lowest coverage files
351
+ */
352
+ async getCoverageSummary(options) {
353
+ if (!this.isUserToken()) {
354
+ throw new Error("getCoverageSummary requires a user API Key (gaf_).");
355
+ }
356
+ if (!options.projectId) {
357
+ throw new Error("projectId is required");
358
+ }
359
+ return this.request(
360
+ `/user/projects/${options.projectId}/coverage-summary`,
361
+ {
362
+ ...options.days && { days: options.days }
363
+ }
364
+ );
365
+ }
366
+ /**
367
+ * Get coverage files for a project with filtering
368
+ *
369
+ * @param options - Query options
370
+ * @param options.projectId - The project ID (required)
371
+ * @param options.filePath - Filter to specific file path
372
+ * @param options.minCoverage - Minimum coverage percentage
373
+ * @param options.maxCoverage - Maximum coverage percentage
374
+ * @param options.limit - Maximum number of results
375
+ * @param options.offset - Pagination offset
376
+ * @param options.sortBy - Sort by 'path' or 'coverage'
377
+ * @param options.sortOrder - Sort order 'asc' or 'desc'
378
+ * @returns List of files with coverage data
379
+ */
380
+ async getCoverageFiles(options) {
381
+ if (!this.isUserToken()) {
382
+ throw new Error("getCoverageFiles requires a user API Key (gaf_).");
383
+ }
384
+ if (!options.projectId) {
385
+ throw new Error("projectId is required");
386
+ }
387
+ return this.request(
388
+ `/user/projects/${options.projectId}/coverage/files`,
389
+ {
390
+ ...options.filePath && { filePath: options.filePath },
391
+ ...options.minCoverage !== void 0 && { minCoverage: options.minCoverage },
392
+ ...options.maxCoverage !== void 0 && { maxCoverage: options.maxCoverage },
393
+ ...options.limit && { limit: options.limit },
394
+ ...options.offset && { offset: options.offset },
395
+ ...options.sortBy && { sortBy: options.sortBy },
396
+ ...options.sortOrder && { sortOrder: options.sortOrder }
397
+ }
398
+ );
399
+ }
400
+ /**
401
+ * Get risk areas (files with low coverage AND test failures)
402
+ *
403
+ * @param options - Query options
404
+ * @param options.projectId - The project ID (required)
405
+ * @param options.days - Analysis period in days (default: 30)
406
+ * @param options.coverageThreshold - Include files below this coverage (default: 80)
407
+ * @returns List of risk areas sorted by risk score
408
+ */
409
+ async getCoverageRiskAreas(options) {
410
+ if (!this.isUserToken()) {
411
+ throw new Error("getCoverageRiskAreas requires a user API Key (gaf_).");
412
+ }
413
+ if (!options.projectId) {
414
+ throw new Error("projectId is required");
415
+ }
416
+ return this.request(
417
+ `/user/projects/${options.projectId}/coverage/risk-areas`,
418
+ {
419
+ ...options.days && { days: options.days },
420
+ ...options.coverageThreshold !== void 0 && { coverageThreshold: options.coverageThreshold }
421
+ }
422
+ );
423
+ }
424
+ /**
425
+ * Get a browser-navigable URL for viewing a test report
426
+ *
427
+ * @param options - Query options
428
+ * @param options.projectId - The project ID (required)
429
+ * @param options.testRunId - The test run ID (required)
430
+ * @param options.filename - Specific file to open (default: index.html)
431
+ * @returns URL with signed token for browser access
432
+ */
433
+ async getReportBrowserUrl(options) {
434
+ if (!this.isUserToken()) {
435
+ throw new Error("getReportBrowserUrl requires a user API Key (gaf_).");
436
+ }
437
+ if (!options.projectId) {
438
+ throw new Error("projectId is required");
439
+ }
440
+ if (!options.testRunId) {
441
+ throw new Error("testRunId is required");
442
+ }
443
+ return this.request(
444
+ `/user/projects/${options.projectId}/reports/${options.testRunId}/browser-url`,
445
+ {
446
+ ...options.filename && { filename: options.filename }
447
+ }
448
+ );
449
+ }
344
450
  };
345
451
 
346
452
  // src/tools/compare-test-metrics.ts
@@ -443,26 +549,206 @@ Use cases:
443
549
  Tip: Use get_test_history first to find the commit SHAs or test run IDs you want to compare.`
444
550
  };
445
551
 
446
- // src/tools/get-flaky-tests.ts
552
+ // src/tools/find-uncovered-failure-areas.ts
447
553
  import { z as z2 } from "zod";
554
+ var findUncoveredFailureAreasInputSchema = {
555
+ projectId: z2.string().describe("Project ID to analyze. Required. Use list_projects to find project IDs."),
556
+ days: z2.number().int().min(1).max(365).optional().describe("Number of days to analyze for test failures (default: 30)"),
557
+ coverageThreshold: z2.number().min(0).max(100).optional().describe("Include files with coverage below this percentage (default: 80)")
558
+ };
559
+ var findUncoveredFailureAreasOutputSchema = {
560
+ hasCoverage: z2.boolean(),
561
+ hasTestResults: z2.boolean(),
562
+ riskAreas: z2.array(z2.object({
563
+ filePath: z2.string(),
564
+ coverage: z2.number(),
565
+ failureCount: z2.number(),
566
+ riskScore: z2.number(),
567
+ testNames: z2.array(z2.string())
568
+ })),
569
+ message: z2.string().optional()
570
+ };
571
+ async function executeFindUncoveredFailureAreas(client, input) {
572
+ const response = await client.getCoverageRiskAreas({
573
+ projectId: input.projectId,
574
+ days: input.days,
575
+ coverageThreshold: input.coverageThreshold
576
+ });
577
+ return {
578
+ hasCoverage: response.hasCoverage,
579
+ hasTestResults: response.hasTestResults,
580
+ riskAreas: response.riskAreas,
581
+ message: response.message
582
+ };
583
+ }
584
+ var findUncoveredFailureAreasMetadata = {
585
+ name: "find_uncovered_failure_areas",
586
+ title: "Find Uncovered Failure Areas",
587
+ description: `Find areas of code that have both low coverage AND test failures.
588
+
589
+ This cross-references test failures with coverage data to identify high-risk
590
+ areas in your codebase that need attention. Files are ranked by a "risk score"
591
+ calculated as: (100 - coverage%) \xD7 failureCount.
592
+
593
+ When using a user API Key (gaf_), you must provide a projectId.
594
+ Use list_projects first to find available project IDs.
595
+
596
+ Parameters:
597
+ - projectId: The project to analyze (required)
598
+ - days: Analysis period for test failures (default: 30)
599
+ - coverageThreshold: Include files below this coverage % (default: 80)
600
+
601
+ Returns:
602
+ - List of risk areas sorted by risk score (highest risk first)
603
+ - Each area includes: file path, coverage %, failure count, risk score, test names
604
+
605
+ Use this to prioritize which parts of your codebase need better test coverage.`
606
+ };
607
+
608
+ // src/tools/get-coverage-for-file.ts
609
+ import { z as z3 } from "zod";
610
+ var getCoverageForFileInputSchema = {
611
+ projectId: z3.string().describe("Project ID to get coverage for. Required. Use list_projects to find project IDs."),
612
+ filePath: z3.string().describe("File path to get coverage for. Can be exact path or partial match.")
613
+ };
614
+ var getCoverageForFileOutputSchema = {
615
+ hasCoverage: z3.boolean(),
616
+ files: z3.array(z3.object({
617
+ path: z3.string(),
618
+ lines: z3.object({
619
+ covered: z3.number(),
620
+ total: z3.number(),
621
+ percentage: z3.number()
622
+ }),
623
+ branches: z3.object({
624
+ covered: z3.number(),
625
+ total: z3.number(),
626
+ percentage: z3.number()
627
+ }),
628
+ functions: z3.object({
629
+ covered: z3.number(),
630
+ total: z3.number(),
631
+ percentage: z3.number()
632
+ })
633
+ })),
634
+ message: z3.string().optional()
635
+ };
636
+ async function executeGetCoverageForFile(client, input) {
637
+ const response = await client.getCoverageFiles({
638
+ projectId: input.projectId,
639
+ filePath: input.filePath,
640
+ limit: 10
641
+ // Return up to 10 matching files
642
+ });
643
+ return {
644
+ hasCoverage: response.hasCoverage,
645
+ files: response.files.map((f) => ({
646
+ path: f.path,
647
+ lines: f.lines,
648
+ branches: f.branches,
649
+ functions: f.functions
650
+ })),
651
+ message: response.message
652
+ };
653
+ }
654
+ var getCoverageForFileMetadata = {
655
+ name: "get_coverage_for_file",
656
+ title: "Get Coverage for File",
657
+ description: `Get coverage metrics for a specific file or files matching a path pattern.
658
+
659
+ When using a user API Key (gaf_), you must provide a projectId.
660
+ Use list_projects first to find available project IDs.
661
+
662
+ Parameters:
663
+ - projectId: The project to query (required)
664
+ - filePath: File path to search for (exact or partial match)
665
+
666
+ Returns:
667
+ - Line coverage (covered/total/percentage)
668
+ - Branch coverage (covered/total/percentage)
669
+ - Function coverage (covered/total/percentage)
670
+
671
+ Use this to check coverage for specific files or find files by path pattern.`
672
+ };
673
+
674
+ // src/tools/get-coverage-summary.ts
675
+ import { z as z4 } from "zod";
676
+ var getCoverageSummaryInputSchema = {
677
+ projectId: z4.string().describe("Project ID to get coverage for. Required. Use list_projects to find project IDs."),
678
+ days: z4.number().int().min(1).max(365).optional().describe("Number of days to analyze for trends (default: 30)")
679
+ };
680
+ var getCoverageSummaryOutputSchema = {
681
+ hasCoverage: z4.boolean(),
682
+ current: z4.object({
683
+ lines: z4.number(),
684
+ branches: z4.number(),
685
+ functions: z4.number()
686
+ }).optional(),
687
+ trend: z4.object({
688
+ direction: z4.enum(["up", "down", "stable"]),
689
+ change: z4.number()
690
+ }).optional(),
691
+ totalReports: z4.number(),
692
+ latestReportDate: z4.string().nullable().optional(),
693
+ lowestCoverageFiles: z4.array(z4.object({
694
+ path: z4.string(),
695
+ coverage: z4.number()
696
+ })).optional(),
697
+ message: z4.string().optional()
698
+ };
699
+ async function executeGetCoverageSummary(client, input) {
700
+ const response = await client.getCoverageSummary({
701
+ projectId: input.projectId,
702
+ days: input.days
703
+ });
704
+ return {
705
+ hasCoverage: response.hasCoverage,
706
+ current: response.current,
707
+ trend: response.trend,
708
+ totalReports: response.totalReports,
709
+ latestReportDate: response.latestReportDate,
710
+ lowestCoverageFiles: response.lowestCoverageFiles,
711
+ message: response.message
712
+ };
713
+ }
714
+ var getCoverageSummaryMetadata = {
715
+ name: "get_coverage_summary",
716
+ title: "Get Coverage Summary",
717
+ description: `Get the coverage metrics summary for a project.
718
+
719
+ When using a user API Key (gaf_), you must provide a projectId.
720
+ Use list_projects first to find available project IDs.
721
+
722
+ Returns:
723
+ - Current coverage percentages (lines, branches, functions)
724
+ - Trend direction (up, down, stable) and change amount
725
+ - Total number of coverage reports
726
+ - Latest report date
727
+ - Top 5 files with lowest coverage
728
+
729
+ Use this to understand your project's overall test coverage health.`
730
+ };
731
+
732
+ // src/tools/get-flaky-tests.ts
733
+ import { z as z5 } from "zod";
448
734
  var getFlakyTestsInputSchema = {
449
- projectId: z2.string().optional().describe("Project ID to get flaky tests for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
450
- threshold: z2.number().min(0).max(1).optional().describe("Minimum flip rate to be considered flaky (0-1, default: 0.1 = 10%)"),
451
- limit: z2.number().int().min(1).max(100).optional().describe("Maximum number of flaky tests to return (default: 50)"),
452
- days: z2.number().int().min(1).max(365).optional().describe("Analysis period in days (default: 30)")
735
+ projectId: z5.string().optional().describe("Project ID to get flaky tests for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
736
+ threshold: z5.number().min(0).max(1).optional().describe("Minimum flip rate to be considered flaky (0-1, default: 0.1 = 10%)"),
737
+ limit: z5.number().int().min(1).max(100).optional().describe("Maximum number of flaky tests to return (default: 50)"),
738
+ days: z5.number().int().min(1).max(365).optional().describe("Analysis period in days (default: 30)")
453
739
  };
454
740
  var getFlakyTestsOutputSchema = {
455
- flakyTests: z2.array(z2.object({
456
- name: z2.string(),
457
- flipRate: z2.number(),
458
- flipCount: z2.number(),
459
- totalRuns: z2.number(),
460
- lastSeen: z2.string()
741
+ flakyTests: z5.array(z5.object({
742
+ name: z5.string(),
743
+ flipRate: z5.number(),
744
+ flipCount: z5.number(),
745
+ totalRuns: z5.number(),
746
+ lastSeen: z5.string()
461
747
  })),
462
- summary: z2.object({
463
- threshold: z2.number(),
464
- totalFlaky: z2.number(),
465
- period: z2.number()
748
+ summary: z5.object({
749
+ threshold: z5.number(),
750
+ totalFlaky: z5.number(),
751
+ period: z5.number()
466
752
  })
467
753
  };
468
754
  async function executeGetFlakyTests(client, input) {
@@ -502,22 +788,22 @@ specific tests are flaky and need investigation.`
502
788
  };
503
789
 
504
790
  // src/tools/get-project-health.ts
505
- import { z as z3 } from "zod";
791
+ import { z as z6 } from "zod";
506
792
  var getProjectHealthInputSchema = {
507
- projectId: z3.string().optional().describe("Project ID to get health for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
508
- days: z3.number().int().min(1).max(365).optional().describe("Number of days to analyze (default: 30)")
793
+ projectId: z6.string().optional().describe("Project ID to get health for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
794
+ days: z6.number().int().min(1).max(365).optional().describe("Number of days to analyze (default: 30)")
509
795
  };
510
796
  var getProjectHealthOutputSchema = {
511
- projectName: z3.string(),
512
- healthScore: z3.number(),
513
- passRate: z3.number().nullable(),
514
- testRunCount: z3.number(),
515
- flakyTestCount: z3.number(),
516
- trend: z3.enum(["up", "down", "stable"]),
517
- period: z3.object({
518
- days: z3.number(),
519
- start: z3.string(),
520
- end: z3.string()
797
+ projectName: z6.string(),
798
+ healthScore: z6.number(),
799
+ passRate: z6.number().nullable(),
800
+ testRunCount: z6.number(),
801
+ flakyTestCount: z6.number(),
802
+ trend: z6.enum(["up", "down", "stable"]),
803
+ period: z6.object({
804
+ days: z6.number(),
805
+ start: z6.string(),
806
+ end: z6.string()
521
807
  })
522
808
  };
523
809
  async function executeGetProjectHealth(client, input) {
@@ -553,23 +839,77 @@ Returns:
553
839
  Use this to understand the current state of your test suite.`
554
840
  };
555
841
 
842
+ // src/tools/get-report-browser-url.ts
843
+ import { z as z7 } from "zod";
844
+ var getReportBrowserUrlInputSchema = {
845
+ projectId: z7.string().describe("Project ID the test run belongs to. Required. Use list_projects to find project IDs."),
846
+ testRunId: z7.string().describe("The test run ID to get the report URL for. Use list_test_runs to find test run IDs."),
847
+ filename: z7.string().optional().describe("Specific file to open (default: index.html or first HTML file)")
848
+ };
849
+ var getReportBrowserUrlOutputSchema = {
850
+ url: z7.string(),
851
+ filename: z7.string(),
852
+ testRunId: z7.string(),
853
+ expiresAt: z7.string(),
854
+ expiresInSeconds: z7.number()
855
+ };
856
+ async function executeGetReportBrowserUrl(client, input) {
857
+ const response = await client.getReportBrowserUrl({
858
+ projectId: input.projectId,
859
+ testRunId: input.testRunId,
860
+ filename: input.filename
861
+ });
862
+ return {
863
+ url: response.url,
864
+ filename: response.filename,
865
+ testRunId: response.testRunId,
866
+ expiresAt: response.expiresAt,
867
+ expiresInSeconds: response.expiresInSeconds
868
+ };
869
+ }
870
+ var getReportBrowserUrlMetadata = {
871
+ name: "get_report_browser_url",
872
+ title: "Get Report Browser URL",
873
+ description: `Get a browser-navigable URL for viewing a test report (Playwright, Vitest, etc.).
874
+
875
+ Returns a signed URL that can be opened directly in a browser without requiring
876
+ the user to log in. The URL expires after 30 minutes for security.
877
+
878
+ When using a user API Key (gaf_), you must provide both projectId and testRunId.
879
+ Use list_projects and list_test_runs to find available IDs.
880
+
881
+ Parameters:
882
+ - projectId: The project the test run belongs to (required)
883
+ - testRunId: The test run to view (required)
884
+ - filename: Specific file to open (optional, defaults to index.html)
885
+
886
+ Returns:
887
+ - url: Browser-navigable URL with signed token
888
+ - filename: The file being accessed
889
+ - expiresAt: ISO timestamp when the URL expires
890
+ - expiresInSeconds: Time until expiration
891
+
892
+ The returned URL can be shared with users who need to view the report.
893
+ Note: URLs expire after 30 minutes for security.`
894
+ };
895
+
556
896
  // src/tools/get-report.ts
557
- import { z as z4 } from "zod";
897
+ import { z as z8 } from "zod";
558
898
  var getReportInputSchema = {
559
- testRunId: z4.string().describe("The test run ID to get report files for. Use list_test_runs to find test run IDs.")
899
+ testRunId: z8.string().describe("The test run ID to get report files for. Use list_test_runs to find test run IDs.")
560
900
  };
561
901
  var getReportOutputSchema = {
562
- testRunId: z4.string(),
563
- projectId: z4.string(),
564
- projectName: z4.string(),
565
- resultSchema: z4.string().optional(),
566
- files: z4.array(z4.object({
567
- filename: z4.string(),
568
- size: z4.number(),
569
- contentType: z4.string(),
570
- downloadUrl: z4.string()
902
+ testRunId: z8.string(),
903
+ projectId: z8.string(),
904
+ projectName: z8.string(),
905
+ resultSchema: z8.string().optional(),
906
+ files: z8.array(z8.object({
907
+ filename: z8.string(),
908
+ size: z8.number(),
909
+ contentType: z8.string(),
910
+ downloadUrl: z8.string()
571
911
  })),
572
- urlExpiresInSeconds: z4.number().optional()
912
+ urlExpiresInSeconds: z8.number().optional()
573
913
  };
574
914
  async function executeGetReport(client, input) {
575
915
  const response = await client.getReport(input.testRunId);
@@ -627,29 +967,29 @@ Use cases:
627
967
  };
628
968
 
629
969
  // src/tools/get-slowest-tests.ts
630
- import { z as z5 } from "zod";
970
+ import { z as z9 } from "zod";
631
971
  var getSlowestTestsInputSchema = {
632
- projectId: z5.string().describe("Project ID to get slowest tests for. Required. Use list_projects to find project IDs."),
633
- days: z5.number().int().min(1).max(365).optional().describe("Analysis period in days (default: 30)"),
634
- limit: z5.number().int().min(1).max(100).optional().describe("Maximum number of tests to return (default: 20)"),
635
- framework: z5.string().optional().describe('Filter by test framework (e.g., "playwright", "vitest", "jest")'),
636
- branch: z5.string().optional().describe('Filter by git branch name (e.g., "main", "develop")')
972
+ projectId: z9.string().describe("Project ID to get slowest tests for. Required. Use list_projects to find project IDs."),
973
+ days: z9.number().int().min(1).max(365).optional().describe("Analysis period in days (default: 30)"),
974
+ limit: z9.number().int().min(1).max(100).optional().describe("Maximum number of tests to return (default: 20)"),
975
+ framework: z9.string().optional().describe('Filter by test framework (e.g., "playwright", "vitest", "jest")'),
976
+ branch: z9.string().optional().describe('Filter by git branch name (e.g., "main", "develop")')
637
977
  };
638
978
  var getSlowestTestsOutputSchema = {
639
- slowestTests: z5.array(z5.object({
640
- name: z5.string(),
641
- fullName: z5.string(),
642
- filePath: z5.string().optional(),
643
- framework: z5.string().optional(),
644
- avgDurationMs: z5.number(),
645
- p95DurationMs: z5.number(),
646
- runCount: z5.number()
979
+ slowestTests: z9.array(z9.object({
980
+ name: z9.string(),
981
+ fullName: z9.string(),
982
+ filePath: z9.string().optional(),
983
+ framework: z9.string().optional(),
984
+ avgDurationMs: z9.number(),
985
+ p95DurationMs: z9.number(),
986
+ runCount: z9.number()
647
987
  })),
648
- summary: z5.object({
649
- projectId: z5.string(),
650
- projectName: z5.string(),
651
- period: z5.number(),
652
- totalReturned: z5.number()
988
+ summary: z9.object({
989
+ projectId: z9.string(),
990
+ projectName: z9.string(),
991
+ period: z9.number(),
992
+ totalReturned: z9.number()
653
993
  })
654
994
  };
655
995
  async function executeGetSlowestTests(client, input) {
@@ -707,28 +1047,28 @@ Use cases:
707
1047
  };
708
1048
 
709
1049
  // src/tools/get-test-history.ts
710
- import { z as z6 } from "zod";
1050
+ import { z as z10 } from "zod";
711
1051
  var getTestHistoryInputSchema = {
712
- projectId: z6.string().optional().describe("Project ID to get test history for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
713
- testName: z6.string().optional().describe("Exact test name to search for"),
714
- filePath: z6.string().optional().describe("File path containing the test"),
715
- limit: z6.number().int().min(1).max(100).optional().describe("Maximum number of results (default: 20)")
1052
+ projectId: z10.string().optional().describe("Project ID to get test history for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
1053
+ testName: z10.string().optional().describe("Exact test name to search for"),
1054
+ filePath: z10.string().optional().describe("File path containing the test"),
1055
+ limit: z10.number().int().min(1).max(100).optional().describe("Maximum number of results (default: 20)")
716
1056
  };
717
1057
  var getTestHistoryOutputSchema = {
718
- history: z6.array(z6.object({
719
- testRunId: z6.string(),
720
- createdAt: z6.string(),
721
- branch: z6.string().optional(),
722
- commitSha: z6.string().optional(),
723
- status: z6.enum(["passed", "failed", "skipped", "pending"]),
724
- durationMs: z6.number(),
725
- message: z6.string().optional()
1058
+ history: z10.array(z10.object({
1059
+ testRunId: z10.string(),
1060
+ createdAt: z10.string(),
1061
+ branch: z10.string().optional(),
1062
+ commitSha: z10.string().optional(),
1063
+ status: z10.enum(["passed", "failed", "skipped", "pending"]),
1064
+ durationMs: z10.number(),
1065
+ message: z10.string().optional()
726
1066
  })),
727
- summary: z6.object({
728
- totalRuns: z6.number(),
729
- passedRuns: z6.number(),
730
- failedRuns: z6.number(),
731
- passRate: z6.number().nullable()
1067
+ summary: z10.object({
1068
+ totalRuns: z10.number(),
1069
+ passedRuns: z10.number(),
1070
+ failedRuns: z10.number(),
1071
+ passRate: z10.number().nullable()
732
1072
  })
733
1073
  };
734
1074
  async function executeGetTestHistory(client, input) {
@@ -783,35 +1123,35 @@ Use this to investigate flaky tests or understand test stability.`
783
1123
  };
784
1124
 
785
1125
  // src/tools/get-test-run-details.ts
786
- import { z as z7 } from "zod";
1126
+ import { z as z11 } from "zod";
787
1127
  var getTestRunDetailsInputSchema = {
788
- testRunId: z7.string().describe("The test run ID to get details for. Use list_test_runs to find test run IDs."),
789
- projectId: z7.string().describe("Project ID the test run belongs to. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
790
- status: z7.enum(["passed", "failed", "skipped"]).optional().describe("Filter tests by status. Returns only tests matching this status."),
791
- limit: z7.number().int().min(1).max(500).optional().describe("Maximum number of tests to return (default: 100, max: 500)"),
792
- offset: z7.number().int().min(0).optional().describe("Number of tests to skip for pagination (default: 0)")
1128
+ testRunId: z11.string().describe("The test run ID to get details for. Use list_test_runs to find test run IDs."),
1129
+ projectId: z11.string().describe("Project ID the test run belongs to. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
1130
+ status: z11.enum(["passed", "failed", "skipped"]).optional().describe("Filter tests by status. Returns only tests matching this status."),
1131
+ limit: z11.number().int().min(1).max(500).optional().describe("Maximum number of tests to return (default: 100, max: 500)"),
1132
+ offset: z11.number().int().min(0).optional().describe("Number of tests to skip for pagination (default: 0)")
793
1133
  };
794
1134
  var getTestRunDetailsOutputSchema = {
795
- testRunId: z7.string(),
796
- summary: z7.object({
797
- passed: z7.number(),
798
- failed: z7.number(),
799
- skipped: z7.number(),
800
- total: z7.number()
1135
+ testRunId: z11.string(),
1136
+ summary: z11.object({
1137
+ passed: z11.number(),
1138
+ failed: z11.number(),
1139
+ skipped: z11.number(),
1140
+ total: z11.number()
801
1141
  }),
802
- tests: z7.array(z7.object({
803
- name: z7.string(),
804
- fullName: z7.string(),
805
- status: z7.enum(["passed", "failed", "skipped"]),
806
- durationMs: z7.number().nullable(),
807
- filePath: z7.string().nullable(),
808
- error: z7.string().nullable()
1142
+ tests: z11.array(z11.object({
1143
+ name: z11.string(),
1144
+ fullName: z11.string(),
1145
+ status: z11.enum(["passed", "failed", "skipped"]),
1146
+ durationMs: z11.number().nullable(),
1147
+ filePath: z11.string().nullable(),
1148
+ error: z11.string().nullable()
809
1149
  })),
810
- pagination: z7.object({
811
- total: z7.number(),
812
- limit: z7.number(),
813
- offset: z7.number(),
814
- hasMore: z7.boolean()
1150
+ pagination: z11.object({
1151
+ total: z11.number(),
1152
+ limit: z11.number(),
1153
+ offset: z11.number(),
1154
+ hasMore: z11.boolean()
815
1155
  })
816
1156
  };
817
1157
  async function executeGetTestRunDetails(client, input) {
@@ -866,24 +1206,101 @@ Note: For aggregate analytics like flaky test detection or duration trends,
866
1206
  use get_test_history, get_flaky_tests, or get_slowest_tests instead.`
867
1207
  };
868
1208
 
1209
+ // src/tools/get-untested-files.ts
1210
+ import { z as z12 } from "zod";
1211
+ var getUntestedFilesInputSchema = {
1212
+ projectId: z12.string().describe("Project ID to analyze. Required. Use list_projects to find project IDs."),
1213
+ maxCoverage: z12.number().min(0).max(100).optional().describe('Maximum coverage percentage to include (default: 10 for "untested")'),
1214
+ limit: z12.number().int().min(1).max(100).optional().describe("Maximum number of files to return (default: 20)")
1215
+ };
1216
+ var getUntestedFilesOutputSchema = {
1217
+ hasCoverage: z12.boolean(),
1218
+ files: z12.array(z12.object({
1219
+ path: z12.string(),
1220
+ lines: z12.object({
1221
+ covered: z12.number(),
1222
+ total: z12.number(),
1223
+ percentage: z12.number()
1224
+ }),
1225
+ branches: z12.object({
1226
+ covered: z12.number(),
1227
+ total: z12.number(),
1228
+ percentage: z12.number()
1229
+ }),
1230
+ functions: z12.object({
1231
+ covered: z12.number(),
1232
+ total: z12.number(),
1233
+ percentage: z12.number()
1234
+ })
1235
+ })),
1236
+ totalCount: z12.number(),
1237
+ message: z12.string().optional()
1238
+ };
1239
+ async function executeGetUntestedFiles(client, input) {
1240
+ const maxCoverage = input.maxCoverage ?? 10;
1241
+ const limit = input.limit ?? 20;
1242
+ const response = await client.getCoverageFiles({
1243
+ projectId: input.projectId,
1244
+ maxCoverage,
1245
+ limit,
1246
+ sortBy: "coverage",
1247
+ sortOrder: "asc"
1248
+ // Lowest coverage first
1249
+ });
1250
+ return {
1251
+ hasCoverage: response.hasCoverage,
1252
+ files: response.files.map((f) => ({
1253
+ path: f.path,
1254
+ lines: f.lines,
1255
+ branches: f.branches,
1256
+ functions: f.functions
1257
+ })),
1258
+ totalCount: response.pagination.total,
1259
+ message: response.message
1260
+ };
1261
+ }
1262
+ var getUntestedFilesMetadata = {
1263
+ name: "get_untested_files",
1264
+ title: "Get Untested Files",
1265
+ description: `Get files with little or no test coverage.
1266
+
1267
+ Returns files sorted by coverage percentage (lowest first), filtered
1268
+ to only include files below a coverage threshold.
1269
+
1270
+ When using a user API Key (gaf_), you must provide a projectId.
1271
+ Use list_projects first to find available project IDs.
1272
+
1273
+ Parameters:
1274
+ - projectId: The project to analyze (required)
1275
+ - maxCoverage: Include files with coverage at or below this % (default: 10)
1276
+ - limit: Maximum number of files to return (default: 20, max: 100)
1277
+
1278
+ Returns:
1279
+ - List of files sorted by coverage (lowest first)
1280
+ - Each file includes line/branch/function coverage metrics
1281
+ - Total count of files matching the criteria
1282
+
1283
+ Use this to find files that need tests added.`
1284
+ };
1285
+
869
1286
  // src/tools/list-projects.ts
870
- import { z as z8 } from "zod";
1287
+ import { z as z13 } from "zod";
871
1288
  var listProjectsInputSchema = {
872
- organizationId: z8.string().optional().describe("Filter by organization ID (optional)"),
873
- limit: z8.number().int().min(1).max(100).optional().describe("Maximum number of projects to return (default: 50)")
1289
+ organizationId: z13.string().optional().describe("Filter by organization ID (optional)"),
1290
+ limit: z13.number().int().min(1).max(100).optional().describe("Maximum number of projects to return (default: 50)")
874
1291
  };
875
1292
  var listProjectsOutputSchema = {
876
- projects: z8.array(z8.object({
877
- id: z8.string(),
878
- name: z8.string(),
879
- description: z8.string().nullable().optional(),
880
- organization: z8.object({
881
- id: z8.string(),
882
- name: z8.string(),
883
- slug: z8.string()
1293
+ projects: z13.array(z13.object({
1294
+ id: z13.string(),
1295
+ name: z13.string(),
1296
+ description: z13.string().nullable().optional(),
1297
+ organization: z13.object({
1298
+ id: z13.string(),
1299
+ name: z13.string(),
1300
+ slug: z13.string()
884
1301
  })
885
1302
  })),
886
- total: z8.number()
1303
+ total: z13.number()
887
1304
  };
888
1305
  async function executeListProjects(client, input) {
889
1306
  const response = await client.listProjects({
@@ -912,28 +1329,28 @@ Requires a user API Key (gaf_). Get one from Account Settings in the Gaffer dash
912
1329
  };
913
1330
 
914
1331
  // src/tools/list-test-runs.ts
915
- import { z as z9 } from "zod";
1332
+ import { z as z14 } from "zod";
916
1333
  var listTestRunsInputSchema = {
917
- projectId: z9.string().optional().describe("Project ID to list test runs for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
918
- commitSha: z9.string().optional().describe("Filter by commit SHA (exact or prefix match)"),
919
- branch: z9.string().optional().describe("Filter by branch name"),
920
- status: z9.enum(["passed", "failed"]).optional().describe('Filter by status: "passed" (no failures) or "failed" (has failures)'),
921
- limit: z9.number().int().min(1).max(100).optional().describe("Maximum number of test runs to return (default: 20)")
1334
+ projectId: z14.string().optional().describe("Project ID to list test runs for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
1335
+ commitSha: z14.string().optional().describe("Filter by commit SHA (exact or prefix match)"),
1336
+ branch: z14.string().optional().describe("Filter by branch name"),
1337
+ status: z14.enum(["passed", "failed"]).optional().describe('Filter by status: "passed" (no failures) or "failed" (has failures)'),
1338
+ limit: z14.number().int().min(1).max(100).optional().describe("Maximum number of test runs to return (default: 20)")
922
1339
  };
923
1340
  var listTestRunsOutputSchema = {
924
- testRuns: z9.array(z9.object({
925
- id: z9.string(),
926
- commitSha: z9.string().optional(),
927
- branch: z9.string().optional(),
928
- passedCount: z9.number(),
929
- failedCount: z9.number(),
930
- skippedCount: z9.number(),
931
- totalCount: z9.number(),
932
- createdAt: z9.string()
1341
+ testRuns: z14.array(z14.object({
1342
+ id: z14.string(),
1343
+ commitSha: z14.string().optional(),
1344
+ branch: z14.string().optional(),
1345
+ passedCount: z14.number(),
1346
+ failedCount: z14.number(),
1347
+ skippedCount: z14.number(),
1348
+ totalCount: z14.number(),
1349
+ createdAt: z14.string()
933
1350
  })),
934
- pagination: z9.object({
935
- total: z9.number(),
936
- hasMore: z9.boolean()
1351
+ pagination: z14.object({
1352
+ total: z14.number(),
1353
+ hasMore: z14.boolean()
937
1354
  })
938
1355
  };
939
1356
  async function executeListTestRuns(client, input) {
@@ -1213,6 +1630,106 @@ async function main() {
1213
1630
  }
1214
1631
  }
1215
1632
  );
1633
+ server.registerTool(
1634
+ getCoverageSummaryMetadata.name,
1635
+ {
1636
+ title: getCoverageSummaryMetadata.title,
1637
+ description: getCoverageSummaryMetadata.description,
1638
+ inputSchema: getCoverageSummaryInputSchema,
1639
+ outputSchema: getCoverageSummaryOutputSchema
1640
+ },
1641
+ async (input) => {
1642
+ try {
1643
+ const output = await executeGetCoverageSummary(client, input);
1644
+ return {
1645
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
1646
+ structuredContent: output
1647
+ };
1648
+ } catch (error) {
1649
+ return handleToolError(getCoverageSummaryMetadata.name, error);
1650
+ }
1651
+ }
1652
+ );
1653
+ server.registerTool(
1654
+ getCoverageForFileMetadata.name,
1655
+ {
1656
+ title: getCoverageForFileMetadata.title,
1657
+ description: getCoverageForFileMetadata.description,
1658
+ inputSchema: getCoverageForFileInputSchema,
1659
+ outputSchema: getCoverageForFileOutputSchema
1660
+ },
1661
+ async (input) => {
1662
+ try {
1663
+ const output = await executeGetCoverageForFile(client, input);
1664
+ return {
1665
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
1666
+ structuredContent: output
1667
+ };
1668
+ } catch (error) {
1669
+ return handleToolError(getCoverageForFileMetadata.name, error);
1670
+ }
1671
+ }
1672
+ );
1673
+ server.registerTool(
1674
+ findUncoveredFailureAreasMetadata.name,
1675
+ {
1676
+ title: findUncoveredFailureAreasMetadata.title,
1677
+ description: findUncoveredFailureAreasMetadata.description,
1678
+ inputSchema: findUncoveredFailureAreasInputSchema,
1679
+ outputSchema: findUncoveredFailureAreasOutputSchema
1680
+ },
1681
+ async (input) => {
1682
+ try {
1683
+ const output = await executeFindUncoveredFailureAreas(client, input);
1684
+ return {
1685
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
1686
+ structuredContent: output
1687
+ };
1688
+ } catch (error) {
1689
+ return handleToolError(findUncoveredFailureAreasMetadata.name, error);
1690
+ }
1691
+ }
1692
+ );
1693
+ server.registerTool(
1694
+ getUntestedFilesMetadata.name,
1695
+ {
1696
+ title: getUntestedFilesMetadata.title,
1697
+ description: getUntestedFilesMetadata.description,
1698
+ inputSchema: getUntestedFilesInputSchema,
1699
+ outputSchema: getUntestedFilesOutputSchema
1700
+ },
1701
+ async (input) => {
1702
+ try {
1703
+ const output = await executeGetUntestedFiles(client, input);
1704
+ return {
1705
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
1706
+ structuredContent: output
1707
+ };
1708
+ } catch (error) {
1709
+ return handleToolError(getUntestedFilesMetadata.name, error);
1710
+ }
1711
+ }
1712
+ );
1713
+ server.registerTool(
1714
+ getReportBrowserUrlMetadata.name,
1715
+ {
1716
+ title: getReportBrowserUrlMetadata.title,
1717
+ description: getReportBrowserUrlMetadata.description,
1718
+ inputSchema: getReportBrowserUrlInputSchema,
1719
+ outputSchema: getReportBrowserUrlOutputSchema
1720
+ },
1721
+ async (input) => {
1722
+ try {
1723
+ const output = await executeGetReportBrowserUrl(client, input);
1724
+ return {
1725
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
1726
+ structuredContent: output
1727
+ };
1728
+ } catch (error) {
1729
+ return handleToolError(getReportBrowserUrlMetadata.name, error);
1730
+ }
1731
+ }
1732
+ );
1216
1733
  const transport = new StdioServerTransport();
1217
1734
  await server.connect(transport);
1218
1735
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gaffer-sh/mcp",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "description": "MCP server for Gaffer test history - give your AI assistant memory of your tests",
6
6
  "license": "MIT",
7
7
  "author": "Gaffer <hello@gaffer.sh>",