@gaffer-sh/mcp 0.6.2 → 0.7.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.
package/dist/index.js CHANGED
@@ -10,7 +10,6 @@ const REQUEST_TIMEOUT_MS = 3e4;
10
10
  const MAX_RETRIES = 3;
11
11
  const INITIAL_RETRY_DELAY_MS = 1e3;
12
12
  const RETRYABLE_STATUS_CODES = [
13
- 401,
14
13
  429,
15
14
  500,
16
15
  502,
@@ -30,7 +29,8 @@ function sleep(ms) {
30
29
  */
31
30
  function detectTokenType(token) {
32
31
  if (token.startsWith("gaf_")) return "user";
33
- return "project";
32
+ if (token.startsWith("gfr_")) return "project";
33
+ throw new Error(`Unrecognized API key format. Expected a user API key (gaf_...) or project token (gfr_...). Got: "${token.substring(0, 4)}...". Check your GAFFER_API_KEY environment variable.`);
34
34
  }
35
35
  /**
36
36
  * Gaffer API v1 client for MCP server
@@ -46,7 +46,7 @@ var GafferApiClient = class GafferApiClient {
46
46
  apiKey;
47
47
  baseUrl;
48
48
  tokenType;
49
- resolvedProjectId = null;
49
+ resolveProjectIdPromise = null;
50
50
  constructor(config) {
51
51
  this.apiKey = config.apiKey;
52
52
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
@@ -74,15 +74,21 @@ var GafferApiClient = class GafferApiClient {
74
74
  }
75
75
  /**
76
76
  * Resolve the project ID for the current token.
77
- * For project tokens, fetches from /project on first call and caches.
77
+ * For project tokens, fetches from /project on first call and caches the Promise
78
+ * to deduplicate concurrent calls.
78
79
  * For user tokens, requires explicit projectId.
79
80
  */
80
81
  async resolveProjectId(projectId) {
81
82
  if (projectId) return projectId;
82
83
  if (this.isUserToken()) throw new Error("projectId is required when using a user API Key");
83
- if (this.resolvedProjectId) return this.resolvedProjectId;
84
- this.resolvedProjectId = (await this.request("/project")).project.id;
85
- return this.resolvedProjectId;
84
+ if (!this.resolveProjectIdPromise) this.resolveProjectIdPromise = this.request("/project").then((response) => {
85
+ if (!response?.project?.id) throw new Error("Failed to resolve project ID from token: unexpected response from /project endpoint. Ensure your project token (gfr_) is valid and the project still exists.");
86
+ return response.project.id;
87
+ }).catch((error) => {
88
+ this.resolveProjectIdPromise = null;
89
+ throw error;
90
+ });
91
+ return this.resolveProjectIdPromise;
86
92
  }
87
93
  /**
88
94
  * Make authenticated request to Gaffer API with retry logic
@@ -201,7 +207,9 @@ var GafferApiClient = class GafferApiClient {
201
207
  });
202
208
  }
203
209
  /**
204
- * Get report files for a test run
210
+ * Get report files for a test run.
211
+ * User-only: the /user/test-runs/:id/report route has no project-scoped equivalent,
212
+ * so project tokens cannot access raw report downloads.
205
213
  */
206
214
  async getReport(testRunId) {
207
215
  if (!this.isUserToken()) throw new Error("getReport requires a user API Key (gaf_). Project tokens (gfr_) cannot access reports via API.");
@@ -314,8 +322,120 @@ var GafferApiClient = class GafferApiClient {
314
322
  const projectId = await this.resolveProjectId(options.projectId);
315
323
  return this.request(`/user/projects/${projectId}/upload-sessions/${options.sessionId}`);
316
324
  }
325
+ /**
326
+ * Search across test failures by error message, stack trace, or test name
327
+ */
328
+ async searchFailures(options) {
329
+ if (!options.query) throw new Error("query is required");
330
+ const projectId = await this.resolveProjectId(options.projectId);
331
+ return this.request(`/user/projects/${projectId}/search-failures`, {
332
+ query: options.query,
333
+ ...options.searchIn && { searchIn: options.searchIn },
334
+ ...options.days && { days: options.days },
335
+ ...options.branch && { branch: options.branch },
336
+ ...options.limit && { limit: options.limit }
337
+ });
338
+ }
317
339
  };
318
340
 
341
+ //#endregion
342
+ //#region src/codemode/executor.ts
343
+ /**
344
+ * Patterns blocked from user code as a basic guard.
345
+ * This is NOT a sandbox — determined users can bypass these checks via
346
+ * string concatenation, bracket notation, or constructor access.
347
+ * The real security boundary is the API layer (read-only, user's own token).
348
+ */
349
+ const BLOCKED_PATTERNS = [
350
+ "globalThis",
351
+ "process",
352
+ "require(",
353
+ "import ",
354
+ "import(",
355
+ "eval(",
356
+ "new Function",
357
+ "Function(",
358
+ "Buffer",
359
+ "__dirname",
360
+ "__filename",
361
+ ".constructor",
362
+ "Reflect"
363
+ ];
364
+ /** Maximum API calls per execution */
365
+ const MAX_API_CALLS = 20;
366
+ /** Execution timeout in milliseconds */
367
+ const EXECUTION_TIMEOUT_MS = 3e4;
368
+ /**
369
+ * Validate code doesn't contain blocked patterns.
370
+ * Returns the first blocked pattern found, or null if safe.
371
+ */
372
+ function validateCode(code) {
373
+ for (const pattern of BLOCKED_PATTERNS) if (code.includes(pattern)) return pattern;
374
+ return null;
375
+ }
376
+ /**
377
+ * Execute user-provided JavaScript code with access to the codemode namespace.
378
+ *
379
+ * Uses AsyncFunction constructor to run code in an async context.
380
+ * The namespace object is injected as `codemode` — all API calls go through it.
381
+ *
382
+ * Security notes:
383
+ * - Not a true sandbox (no vm2/isolated-vm) — same pattern as Cloudflare code mode
384
+ * - Blocked patterns prevent obvious escape hatches
385
+ * - API call counting prevents resource exhaustion
386
+ * - Timeout prevents infinite loops
387
+ * - The real security boundary is the API itself (read-only, user's own token)
388
+ */
389
+ async function executeCode(code, namespace) {
390
+ const blocked = validateCode(code);
391
+ if (blocked) throw new Error(`Blocked pattern detected: "${blocked}". Code must not use ${blocked}.`);
392
+ const logs = [];
393
+ const start = Date.now();
394
+ const serialize = (a) => {
395
+ if (typeof a !== "object" || a === null) return String(a);
396
+ try {
397
+ return JSON.stringify(a);
398
+ } catch {
399
+ return String(a);
400
+ }
401
+ };
402
+ const safeConsole = {
403
+ log: (...args) => logs.push(args.map(serialize).join(" ")),
404
+ warn: (...args) => logs.push(`[warn] ${args.map(serialize).join(" ")}`),
405
+ error: (...args) => logs.push(`[error] ${args.map(serialize).join(" ")}`)
406
+ };
407
+ let callCount = 0;
408
+ const countedNamespace = {};
409
+ for (const [name, fn] of Object.entries(namespace)) countedNamespace[name] = async (...args) => {
410
+ callCount++;
411
+ if (callCount > MAX_API_CALLS) throw new Error(`API call limit exceeded (max ${MAX_API_CALLS} calls per execution)`);
412
+ return fn(...args);
413
+ };
414
+ const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
415
+ const fn = new AsyncFunction("codemode", "console", code);
416
+ let timeoutId;
417
+ const resultPromise = fn(countedNamespace, safeConsole);
418
+ const timeoutPromise = new Promise((_, reject) => {
419
+ timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Execution timed out after ${EXECUTION_TIMEOUT_MS}ms`)), EXECUTION_TIMEOUT_MS);
420
+ });
421
+ try {
422
+ return {
423
+ result: await Promise.race([resultPromise, timeoutPromise]),
424
+ logs,
425
+ durationMs: Date.now() - start
426
+ };
427
+ } catch (error) {
428
+ const durationMs = Date.now() - start;
429
+ const message = error instanceof Error ? error.message : String(error);
430
+ const enrichedError = new Error(message, { cause: error });
431
+ enrichedError.logs = logs;
432
+ enrichedError.durationMs = durationMs;
433
+ throw enrichedError;
434
+ } finally {
435
+ clearTimeout(timeoutId);
436
+ }
437
+ }
438
+
319
439
  //#endregion
320
440
  //#region src/tools/compare-test-metrics.ts
321
441
  /**
@@ -397,7 +517,7 @@ const compareTestMetricsMetadata = {
397
517
  Useful for measuring the impact of code changes on test performance or reliability.
398
518
 
399
519
  Parameters:
400
- - projectId (required): Project ID
520
+ - projectId (optional): Project ID — required for user API keys, auto-resolved for project tokens
401
521
  - testName (required): The test name to compare (short name or full name)
402
522
  - Option 1 - Compare by commit:
403
523
  - beforeCommit: Commit SHA for "before" measurement
@@ -480,7 +600,7 @@ areas in your codebase that need attention. Files are ranked by a "risk score"
480
600
  calculated as: (100 - coverage%) × failureCount.
481
601
 
482
602
  Parameters:
483
- - projectId: The project to analyze (required)
603
+ - projectId (optional): Project ID required for user API keys, auto-resolved for project tokens
484
604
  - days: Analysis period for test failures (default: 30)
485
605
  - coverageThreshold: Include files below this coverage % (default: 80)
486
606
 
@@ -554,7 +674,7 @@ const getCoverageForFileMetadata = {
554
674
  description: `Get coverage metrics for a specific file or files matching a path pattern.
555
675
 
556
676
  Parameters:
557
- - projectId: The project to query (required)
677
+ - projectId (optional): Project ID required for user API keys, auto-resolved for project tokens
558
678
  - filePath: File path to search for (exact or partial match)
559
679
 
560
680
  Returns:
@@ -693,7 +813,7 @@ const getFailureClustersMetadata = {
693
813
  description: `Group failed tests by root cause using error message similarity.
694
814
 
695
815
  Parameters:
696
- - projectId (required): The project ID
816
+ - projectId (optional): Project ID — required for user API keys, auto-resolved for project tokens
697
817
  - testRunId (required): The test run ID to analyze
698
818
 
699
819
  Returns:
@@ -901,7 +1021,7 @@ Returns a signed URL that can be opened directly in a browser without requiring
901
1021
  the user to log in. The URL expires after 30 minutes for security.
902
1022
 
903
1023
  Parameters:
904
- - projectId: The project the test run belongs to (required)
1024
+ - projectId (optional): Project ID required for user API keys, auto-resolved for project tokens
905
1025
  - testRunId: The test run to view (required)
906
1026
  - filename: Specific file to open (optional, defaults to index.html)
907
1027
 
@@ -1063,7 +1183,7 @@ const getSlowestTestsMetadata = {
1063
1183
  description: `Get the slowest tests in a project, sorted by P95 duration.
1064
1184
 
1065
1185
  Parameters:
1066
- - projectId (required): Project ID to analyze
1186
+ - projectId (optional): Project ID required for user API keys, auto-resolved for project tokens
1067
1187
  - days (optional): Analysis period in days (default: 30, max: 365)
1068
1188
  - limit (optional): Max tests to return (default: 20, max: 100)
1069
1189
  - framework (optional): Filter by framework (e.g., "playwright", "vitest")
@@ -1257,7 +1377,7 @@ const getTestRunDetailsMetadata = {
1257
1377
 
1258
1378
  Parameters:
1259
1379
  - testRunId (required): The test run ID to get details for
1260
- - projectId (required): Project ID the test run belongs to
1380
+ - projectId (optional): Project ID required for user API keys, auto-resolved for project tokens
1261
1381
  - status (optional): Filter by test status: "passed", "failed", or "skipped"
1262
1382
  - limit (optional): Max tests to return (default: 100, max: 500)
1263
1383
  - offset (optional): Pagination offset (default: 0)
@@ -1362,7 +1482,7 @@ Returns files sorted by coverage percentage (lowest first), filtered
1362
1482
  to only include files below a coverage threshold.
1363
1483
 
1364
1484
  Parameters:
1365
- - projectId: The project to analyze (required)
1485
+ - projectId (optional): Project ID required for user API keys, auto-resolved for project tokens
1366
1486
  - maxCoverage: Include files with coverage at or below this % (default: 10)
1367
1487
  - limit: Maximum number of files to return (default: 20, max: 100)
1368
1488
 
@@ -1460,7 +1580,7 @@ const getUploadStatusMetadata = {
1460
1580
  Use this tool to answer "are my test results ready?" after pushing code.
1461
1581
 
1462
1582
  Parameters:
1463
- - projectId (required): The project ID
1583
+ - projectId (optional): Project ID — required for user API keys, auto-resolved for project tokens
1464
1584
  - sessionId (optional): Specific upload session ID for detailed status
1465
1585
  - commitSha (optional): Filter by commit SHA to find uploads for a specific commit
1466
1586
  - branch (optional): Filter by branch name
@@ -1491,63 +1611,6 @@ Returns (detail mode):
1491
1611
  - coverageReports: Linked coverage report summaries (id, format)`
1492
1612
  };
1493
1613
 
1494
- //#endregion
1495
- //#region src/tools/list-projects.ts
1496
- /**
1497
- * Input schema for list_projects tool
1498
- */
1499
- const listProjectsInputSchema = {
1500
- organizationId: z.string().optional().describe("Filter by organization ID (optional)"),
1501
- limit: z.number().int().min(1).max(100).optional().describe("Maximum number of projects to return (default: 50)")
1502
- };
1503
- /**
1504
- * Output schema for list_projects tool
1505
- */
1506
- const listProjectsOutputSchema = {
1507
- projects: z.array(z.object({
1508
- id: z.string(),
1509
- name: z.string(),
1510
- description: z.string().nullable().optional(),
1511
- organization: z.object({
1512
- id: z.string(),
1513
- name: z.string(),
1514
- slug: z.string()
1515
- })
1516
- })),
1517
- total: z.number()
1518
- };
1519
- /**
1520
- * Execute list_projects tool
1521
- */
1522
- async function executeListProjects(client, input) {
1523
- const response = await client.listProjects({
1524
- organizationId: input.organizationId,
1525
- limit: input.limit
1526
- });
1527
- return {
1528
- projects: response.projects.map((p) => ({
1529
- id: p.id,
1530
- name: p.name,
1531
- description: p.description,
1532
- organization: p.organization
1533
- })),
1534
- total: response.pagination.total
1535
- };
1536
- }
1537
- /**
1538
- * Tool metadata
1539
- */
1540
- const listProjectsMetadata = {
1541
- name: "list_projects",
1542
- title: "List Projects",
1543
- description: `List all projects you have access to.
1544
-
1545
- Returns a list of projects with their IDs, names, and organization info.
1546
- Use this to find project IDs for other tools like get_project_health.
1547
-
1548
- Requires a user API Key (gaf_). Get one from Account Settings in the Gaffer dashboard.`
1549
- };
1550
-
1551
1614
  //#endregion
1552
1615
  //#region src/tools/list-test-runs.ts
1553
1616
  /**
@@ -1635,6 +1698,508 @@ Use cases:
1635
1698
  - "What's the status of tests on my feature branch?"`
1636
1699
  };
1637
1700
 
1701
+ //#endregion
1702
+ //#region src/tools/search-failures.ts
1703
+ /**
1704
+ * Input schema for search_failures tool
1705
+ */
1706
+ const searchFailuresInputSchema = {
1707
+ projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
1708
+ query: z.string().min(1).describe("Search query to match against failure messages, error stacks, or test names."),
1709
+ searchIn: z.enum([
1710
+ "errors",
1711
+ "names",
1712
+ "all"
1713
+ ]).optional().describe("Where to search: \"errors\" (error messages and stacks), \"names\" (test names), or \"all\" (default: \"all\")."),
1714
+ days: z.number().int().min(1).max(365).optional().describe("Number of days to search back (default: 30)"),
1715
+ branch: z.string().optional().describe("Filter to a specific branch"),
1716
+ limit: z.number().int().min(1).max(100).optional().describe("Maximum number of matches to return (default: 20)")
1717
+ };
1718
+ /**
1719
+ * Output schema for search_failures tool
1720
+ */
1721
+ const searchFailuresOutputSchema = {
1722
+ matches: z.array(z.object({
1723
+ testName: z.string(),
1724
+ testRunId: z.string(),
1725
+ branch: z.string().nullable(),
1726
+ commitSha: z.string().nullable(),
1727
+ errorMessage: z.string().nullable(),
1728
+ errorStack: z.string().nullable(),
1729
+ createdAt: z.string()
1730
+ })),
1731
+ total: z.number(),
1732
+ query: z.string()
1733
+ };
1734
+ /**
1735
+ * Execute search_failures tool
1736
+ */
1737
+ async function executeSearchFailures(client, input) {
1738
+ return client.searchFailures(input);
1739
+ }
1740
+ /**
1741
+ * Tool metadata
1742
+ */
1743
+ const searchFailuresMetadata = {
1744
+ name: "search_failures",
1745
+ title: "Search Failures",
1746
+ description: `Search across test failures by error message, stack trace, or test name.
1747
+
1748
+ Use this to find specific failures across test runs — like grep for your test history.
1749
+
1750
+ Examples:
1751
+ - "TypeError: Cannot read properties of undefined" → find all occurrences of this error
1752
+ - "timeout" → find timeout-related failures
1753
+ - "auth" with searchIn="names" → find failing auth tests
1754
+
1755
+ Returns matching failures with test run context (branch, commit, timestamp) for investigation.`
1756
+ };
1757
+
1758
+ //#endregion
1759
+ //#region src/codemode/register-tools.ts
1760
+ const TOOLS = [
1761
+ {
1762
+ metadata: getProjectHealthMetadata,
1763
+ inputSchema: getProjectHealthInputSchema,
1764
+ execute: executeGetProjectHealth,
1765
+ category: "health",
1766
+ keywords: [
1767
+ "health",
1768
+ "score",
1769
+ "pass rate",
1770
+ "trend",
1771
+ "overview"
1772
+ ]
1773
+ },
1774
+ {
1775
+ metadata: getTestHistoryMetadata,
1776
+ inputSchema: getTestHistoryInputSchema,
1777
+ execute: executeGetTestHistory,
1778
+ category: "testing",
1779
+ keywords: [
1780
+ "history",
1781
+ "pass",
1782
+ "fail",
1783
+ "stability",
1784
+ "regression"
1785
+ ]
1786
+ },
1787
+ {
1788
+ metadata: getFlakyTestsMetadata,
1789
+ inputSchema: getFlakyTestsInputSchema,
1790
+ execute: executeGetFlakyTests,
1791
+ category: "testing",
1792
+ keywords: [
1793
+ "flaky",
1794
+ "flip",
1795
+ "inconsistent",
1796
+ "non-deterministic"
1797
+ ]
1798
+ },
1799
+ {
1800
+ metadata: listTestRunsMetadata,
1801
+ inputSchema: listTestRunsInputSchema,
1802
+ execute: executeListTestRuns,
1803
+ category: "testing",
1804
+ keywords: [
1805
+ "runs",
1806
+ "list",
1807
+ "commit",
1808
+ "branch",
1809
+ "recent"
1810
+ ]
1811
+ },
1812
+ {
1813
+ metadata: getReportMetadata,
1814
+ inputSchema: getReportInputSchema,
1815
+ execute: executeGetReport,
1816
+ category: "reports",
1817
+ keywords: [
1818
+ "report",
1819
+ "files",
1820
+ "download",
1821
+ "artifacts"
1822
+ ]
1823
+ },
1824
+ {
1825
+ metadata: getSlowestTestsMetadata,
1826
+ inputSchema: getSlowestTestsInputSchema,
1827
+ execute: executeGetSlowestTests,
1828
+ category: "testing",
1829
+ keywords: [
1830
+ "slow",
1831
+ "performance",
1832
+ "duration",
1833
+ "p95",
1834
+ "bottleneck"
1835
+ ]
1836
+ },
1837
+ {
1838
+ metadata: getTestRunDetailsMetadata,
1839
+ inputSchema: getTestRunDetailsInputSchema,
1840
+ execute: executeGetTestRunDetails,
1841
+ category: "testing",
1842
+ keywords: [
1843
+ "details",
1844
+ "results",
1845
+ "errors",
1846
+ "stack traces",
1847
+ "test cases"
1848
+ ]
1849
+ },
1850
+ {
1851
+ metadata: getFailureClustersMetadata,
1852
+ inputSchema: getFailureClustersInputSchema,
1853
+ execute: executeGetFailureClusters,
1854
+ category: "testing",
1855
+ keywords: [
1856
+ "failure",
1857
+ "clusters",
1858
+ "root cause",
1859
+ "error grouping"
1860
+ ]
1861
+ },
1862
+ {
1863
+ metadata: compareTestMetricsMetadata,
1864
+ inputSchema: compareTestMetricsInputSchema,
1865
+ execute: executeCompareTestMetrics,
1866
+ category: "testing",
1867
+ keywords: [
1868
+ "compare",
1869
+ "before",
1870
+ "after",
1871
+ "regression",
1872
+ "delta"
1873
+ ]
1874
+ },
1875
+ {
1876
+ metadata: getCoverageSummaryMetadata,
1877
+ inputSchema: getCoverageSummaryInputSchema,
1878
+ execute: executeGetCoverageSummary,
1879
+ category: "coverage",
1880
+ keywords: [
1881
+ "coverage",
1882
+ "summary",
1883
+ "lines",
1884
+ "branches",
1885
+ "functions"
1886
+ ]
1887
+ },
1888
+ {
1889
+ metadata: getCoverageForFileMetadata,
1890
+ inputSchema: getCoverageForFileInputSchema,
1891
+ execute: executeGetCoverageForFile,
1892
+ category: "coverage",
1893
+ keywords: [
1894
+ "coverage",
1895
+ "file",
1896
+ "path",
1897
+ "lines",
1898
+ "branches"
1899
+ ]
1900
+ },
1901
+ {
1902
+ metadata: findUncoveredFailureAreasMetadata,
1903
+ inputSchema: findUncoveredFailureAreasInputSchema,
1904
+ execute: executeFindUncoveredFailureAreas,
1905
+ category: "coverage",
1906
+ keywords: [
1907
+ "risk",
1908
+ "uncovered",
1909
+ "failures",
1910
+ "low coverage"
1911
+ ]
1912
+ },
1913
+ {
1914
+ metadata: getUntestedFilesMetadata,
1915
+ inputSchema: getUntestedFilesInputSchema,
1916
+ execute: executeGetUntestedFiles,
1917
+ category: "coverage",
1918
+ keywords: [
1919
+ "untested",
1920
+ "zero coverage",
1921
+ "missing tests"
1922
+ ]
1923
+ },
1924
+ {
1925
+ metadata: getReportBrowserUrlMetadata,
1926
+ inputSchema: getReportBrowserUrlInputSchema,
1927
+ execute: executeGetReportBrowserUrl,
1928
+ category: "reports",
1929
+ keywords: [
1930
+ "browser",
1931
+ "url",
1932
+ "view",
1933
+ "report",
1934
+ "signed"
1935
+ ]
1936
+ },
1937
+ {
1938
+ metadata: getUploadStatusMetadata,
1939
+ inputSchema: getUploadStatusInputSchema,
1940
+ execute: executeGetUploadStatus,
1941
+ category: "uploads",
1942
+ keywords: [
1943
+ "upload",
1944
+ "status",
1945
+ "processing",
1946
+ "CI",
1947
+ "ready"
1948
+ ]
1949
+ },
1950
+ {
1951
+ metadata: searchFailuresMetadata,
1952
+ inputSchema: searchFailuresInputSchema,
1953
+ execute: executeSearchFailures,
1954
+ category: "testing",
1955
+ keywords: [
1956
+ "search",
1957
+ "failure",
1958
+ "error message",
1959
+ "grep",
1960
+ "find"
1961
+ ]
1962
+ }
1963
+ ];
1964
+ /**
1965
+ * Register all tool functions in the codemode registry.
1966
+ */
1967
+ function registerAllTools(registry) {
1968
+ for (const tool of TOOLS) registry.register({
1969
+ name: tool.metadata.name,
1970
+ description: tool.metadata.description,
1971
+ category: tool.category,
1972
+ keywords: tool.keywords,
1973
+ inputSchema: tool.inputSchema,
1974
+ execute: tool.execute
1975
+ });
1976
+ }
1977
+
1978
+ //#endregion
1979
+ //#region src/codemode/type-gen.ts
1980
+ /**
1981
+ * Convert a Zod schema to a TypeScript type string.
1982
+ * Handles the subset of Zod types used in our tool schemas.
1983
+ */
1984
+ function zodToTs(schema) {
1985
+ if (schema instanceof z.ZodEffects) return zodToTs(schema.innerType());
1986
+ if (schema instanceof z.ZodOptional) return `${zodToTs(schema.unwrap())} | undefined`;
1987
+ if (schema instanceof z.ZodNullable) return `${zodToTs(schema.unwrap())} | null`;
1988
+ if (schema instanceof z.ZodDefault) return zodToTs(schema.removeDefault());
1989
+ if (schema instanceof z.ZodString) return "string";
1990
+ if (schema instanceof z.ZodNumber) return "number";
1991
+ if (schema instanceof z.ZodBoolean) return "boolean";
1992
+ if (schema instanceof z.ZodEnum) return schema.options.map((v) => `'${v}'`).join(" | ");
1993
+ if (schema instanceof z.ZodLiteral) {
1994
+ const val = schema.value;
1995
+ return typeof val === "string" ? `'${val}'` : String(val);
1996
+ }
1997
+ if (schema instanceof z.ZodArray) {
1998
+ const inner = zodToTs(schema.element);
1999
+ if (inner.includes("|")) return `(${inner})[]`;
2000
+ return `${inner}[]`;
2001
+ }
2002
+ if (schema instanceof z.ZodObject) {
2003
+ const shape = schema.shape;
2004
+ const entries = Object.entries(shape);
2005
+ if (entries.length === 0) return "{}";
2006
+ return `{ ${entries.map(([key, fieldSchema]) => formatField(key, fieldSchema)).join("; ")} }`;
2007
+ }
2008
+ if (schema instanceof z.ZodRecord) return `Record<string, ${zodToTs(schema.valueSchema)}>`;
2009
+ if (schema instanceof z.ZodUnion) return schema.options.map((o) => zodToTs(o)).join(" | ");
2010
+ console.error(`[gaffer-mcp] zodToTs: unhandled Zod type "${schema.constructor.name}", falling back to "unknown"`);
2011
+ return "unknown";
2012
+ }
2013
+ /**
2014
+ * Format a single field as "name?: type" (with ? for optionals, unwrapping the inner type).
2015
+ */
2016
+ function formatField(key, schema) {
2017
+ const isOptional = schema instanceof z.ZodOptional;
2018
+ return `${key}${isOptional ? "?" : ""}: ${isOptional ? zodToTs(schema.unwrap()) : zodToTs(schema)}`;
2019
+ }
2020
+ /**
2021
+ * Generate a TypeScript function declaration from a function name,
2022
+ * description, and Zod input schema (object shape).
2023
+ */
2024
+ function generateDeclaration(name, description, inputSchema) {
2025
+ const entries = Object.entries(inputSchema);
2026
+ if (entries.length === 0) return `/** ${description} */\n${name}(): Promise<any>`;
2027
+ return `/** ${description} */\n${name}(input: { ${entries.map(([key, schema]) => formatField(key, schema)).join("; ")} }): Promise<any>`;
2028
+ }
2029
+
2030
+ //#endregion
2031
+ //#region src/codemode/registry.ts
2032
+ /**
2033
+ * Registry of codemode functions.
2034
+ * Wraps existing tool execute functions with metadata for discovery and namespace building.
2035
+ */
2036
+ var FunctionRegistry = class {
2037
+ entries = /* @__PURE__ */ new Map();
2038
+ /**
2039
+ * Register a function in the registry
2040
+ */
2041
+ register(entry) {
2042
+ this.entries.set(entry.name, entry);
2043
+ }
2044
+ /**
2045
+ * Get all registered function entries
2046
+ */
2047
+ getAll() {
2048
+ return Array.from(this.entries.values());
2049
+ }
2050
+ /**
2051
+ * Get a single entry by name
2052
+ */
2053
+ get(name) {
2054
+ return this.entries.get(name);
2055
+ }
2056
+ /**
2057
+ * Build the namespace object that gets injected into the executor.
2058
+ * Each function validates input via Zod then calls the tool's execute function.
2059
+ */
2060
+ buildNamespace(client) {
2061
+ const namespace = {};
2062
+ for (const entry of this.entries.values()) namespace[entry.name] = async (input = {}) => {
2063
+ const result = z.object(entry.inputSchema).safeParse(input);
2064
+ if (!result.success) {
2065
+ const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
2066
+ throw new Error(`Invalid input for ${entry.name}: ${issues}`);
2067
+ }
2068
+ try {
2069
+ return await entry.execute(client, result.data);
2070
+ } catch (error) {
2071
+ const message = error instanceof Error ? error.message : String(error);
2072
+ throw new Error(`${entry.name} failed: ${message}`, { cause: error });
2073
+ }
2074
+ };
2075
+ return namespace;
2076
+ }
2077
+ /**
2078
+ * Generate TypeScript declarations for all registered functions.
2079
+ * Used in the execute_code tool description so the LLM knows available functions.
2080
+ */
2081
+ generateAllDeclarations() {
2082
+ return this.getAll().map((entry) => generateDeclaration(entry.name, entry.description, entry.inputSchema)).join("\n\n");
2083
+ }
2084
+ /**
2085
+ * Generate a declaration for a single function
2086
+ */
2087
+ generateDeclaration(name) {
2088
+ const entry = this.entries.get(name);
2089
+ if (!entry) return null;
2090
+ return generateDeclaration(entry.name, entry.description, entry.inputSchema);
2091
+ }
2092
+ /**
2093
+ * Search for functions matching a query.
2094
+ * Scores: name match (10) > category match (5) > keyword match (3) > description match (1)
2095
+ */
2096
+ search(query) {
2097
+ if (!query.trim()) return this.listAll();
2098
+ const terms = query.toLowerCase().split(/\s+/);
2099
+ const scored = [];
2100
+ for (const entry of this.entries.values()) {
2101
+ let score = 0;
2102
+ const nameLower = entry.name.toLowerCase();
2103
+ const categoryLower = entry.category.toLowerCase();
2104
+ const descLower = entry.description.toLowerCase();
2105
+ const keywordsLower = entry.keywords.map((k) => k.toLowerCase());
2106
+ for (const term of terms) {
2107
+ if (nameLower.includes(term)) score += 10;
2108
+ if (categoryLower.includes(term)) score += 5;
2109
+ if (keywordsLower.some((k) => k.includes(term))) score += 3;
2110
+ if (descLower.includes(term)) score += 1;
2111
+ }
2112
+ if (score > 0) scored.push({
2113
+ entry,
2114
+ score
2115
+ });
2116
+ }
2117
+ scored.sort((a, b) => b.score - a.score);
2118
+ return scored.map(({ entry }) => this.toSearchResult(entry));
2119
+ }
2120
+ /**
2121
+ * List all functions (used when search query is empty)
2122
+ */
2123
+ listAll() {
2124
+ return Array.from(this.entries.values()).map((entry) => this.toSearchResult(entry));
2125
+ }
2126
+ toSearchResult(entry) {
2127
+ return {
2128
+ name: entry.name,
2129
+ description: entry.description,
2130
+ category: entry.category,
2131
+ declaration: generateDeclaration(entry.name, entry.description, entry.inputSchema)
2132
+ };
2133
+ }
2134
+ };
2135
+
2136
+ //#endregion
2137
+ //#region src/codemode/search.ts
2138
+ const searchToolsInputSchema = { query: z.string().optional().describe("Search query to find relevant functions. Leave empty to list all available functions.") };
2139
+ /**
2140
+ * Execute search_tools: find functions by keyword matching
2141
+ */
2142
+ function executeSearchTools(registry, input) {
2143
+ return { functions: input.query ? registry.search(input.query) : registry.listAll() };
2144
+ }
2145
+
2146
+ //#endregion
2147
+ //#region src/tools/list-projects.ts
2148
+ /**
2149
+ * Input schema for list_projects tool
2150
+ */
2151
+ const listProjectsInputSchema = {
2152
+ organizationId: z.string().optional().describe("Filter by organization ID (optional)"),
2153
+ limit: z.number().int().min(1).max(100).optional().describe("Maximum number of projects to return (default: 50)")
2154
+ };
2155
+ /**
2156
+ * Output schema for list_projects tool
2157
+ */
2158
+ const listProjectsOutputSchema = {
2159
+ projects: z.array(z.object({
2160
+ id: z.string(),
2161
+ name: z.string(),
2162
+ description: z.string().nullable().optional(),
2163
+ organization: z.object({
2164
+ id: z.string(),
2165
+ name: z.string(),
2166
+ slug: z.string()
2167
+ })
2168
+ })),
2169
+ total: z.number()
2170
+ };
2171
+ /**
2172
+ * Execute list_projects tool
2173
+ */
2174
+ async function executeListProjects(client, input) {
2175
+ const response = await client.listProjects({
2176
+ organizationId: input.organizationId,
2177
+ limit: input.limit
2178
+ });
2179
+ return {
2180
+ projects: response.projects.map((p) => ({
2181
+ id: p.id,
2182
+ name: p.name,
2183
+ description: p.description,
2184
+ organization: p.organization
2185
+ })),
2186
+ total: response.pagination.total
2187
+ };
2188
+ }
2189
+ /**
2190
+ * Tool metadata
2191
+ */
2192
+ const listProjectsMetadata = {
2193
+ name: "list_projects",
2194
+ title: "List Projects",
2195
+ description: `List all projects you have access to.
2196
+
2197
+ Returns a list of projects with their IDs, names, and organization info.
2198
+ Use this to find project IDs for other tools like get_project_health.
2199
+
2200
+ Requires a user API Key (gaf_). Get one from Account Settings in the Gaffer dashboard.`
2201
+ };
2202
+
1638
2203
  //#endregion
1639
2204
  //#region src/index.ts
1640
2205
  /**
@@ -1653,48 +2218,29 @@ function logError(toolName, error) {
1653
2218
  */
1654
2219
  function handleToolError(toolName, error) {
1655
2220
  logError(toolName, error);
2221
+ const message = error instanceof Error ? error.message : "Unknown error";
2222
+ const logs = Array.isArray(error?.logs) ? error.logs : void 0;
2223
+ const durationMs = typeof error?.durationMs === "number" ? error.durationMs : void 0;
2224
+ let text = `Error: ${message}`;
2225
+ if (logs?.length) text += `\n\nCaptured logs:\n${logs.join("\n")}`;
2226
+ if (durationMs !== void 0) text += `\n\nDuration: ${durationMs}ms`;
1656
2227
  return {
1657
2228
  content: [{
1658
2229
  type: "text",
1659
- text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
2230
+ text
1660
2231
  }],
1661
2232
  isError: true
1662
2233
  };
1663
2234
  }
1664
2235
  /**
1665
- * Register a tool with the MCP server using a consistent pattern.
1666
- * Reduces boilerplate by handling error wrapping and response formatting.
1667
- */
1668
- function registerTool(server, client, tool) {
1669
- server.registerTool(tool.metadata.name, {
1670
- title: tool.metadata.title,
1671
- description: tool.metadata.description,
1672
- inputSchema: tool.inputSchema,
1673
- outputSchema: tool.outputSchema
1674
- }, async (input) => {
1675
- try {
1676
- const output = await tool.execute(client, input);
1677
- return {
1678
- content: [{
1679
- type: "text",
1680
- text: JSON.stringify(output, null, 2)
1681
- }],
1682
- structuredContent: output
1683
- };
1684
- } catch (error) {
1685
- return handleToolError(tool.metadata.name, error);
1686
- }
1687
- });
1688
- }
1689
- /**
1690
- * Gaffer MCP Server
2236
+ * Gaffer MCP Server Code Mode
1691
2237
  *
1692
- * Provides AI assistants with access to test history and health metrics.
2238
+ * Instead of individual tools, exposes 3 tools:
2239
+ * - execute_code: Run JavaScript that calls Gaffer API functions
2240
+ * - search_tools: Find available functions by keyword
2241
+ * - list_projects: List projects (user tokens only)
1693
2242
  *
1694
- * Supports two authentication modes:
1695
- * 1. User API Keys (gaf_) - Read-only access to all user's projects
1696
- * Set via GAFFER_API_KEY environment variable
1697
- * 2. Project Upload Tokens (gfr_) - Legacy, single project access
2243
+ * This follows Cloudflare's "code mode" pattern for MCP servers.
1698
2244
  */
1699
2245
  async function main() {
1700
2246
  if (!process.env.GAFFER_API_KEY) {
@@ -1711,169 +2257,153 @@ async function main() {
1711
2257
  process.exit(1);
1712
2258
  }
1713
2259
  const client = GafferApiClient.fromEnv();
2260
+ const registry = new FunctionRegistry();
2261
+ registerAllTools(registry);
2262
+ const namespace = registry.buildNamespace(client);
2263
+ const declarations = registry.generateAllDeclarations();
1714
2264
  const server = new McpServer({
1715
2265
  name: "gaffer",
1716
- version: "0.1.0"
1717
- }, { instructions: `Gaffer provides test analytics and coverage data for your projects.
2266
+ version: "0.7.0"
2267
+ }, { instructions: `Gaffer provides test analytics and coverage data. This server uses **code mode** — instead of individual tools, write JavaScript that calls functions on the \`codemode\` namespace.
1718
2268
 
1719
2269
  ## Authentication
1720
2270
 
1721
- ${client.isUserToken() ? "You have access to multiple projects. Use `list_projects` to find project IDs, then pass `projectId` to all tools." : "Your token is scoped to a single project. Do NOT call `list_projects`. Do NOT pass `projectId` — it resolves automatically. All tools are available."}
1722
-
1723
- ## Coverage Analysis Best Practices
1724
-
1725
- When helping users improve test coverage, combine coverage data with codebase exploration:
1726
-
1727
- 1. **Understand code utilization first**: Before targeting files by coverage percentage, explore which code is critical:
1728
- - Find entry points (route definitions, event handlers, exported functions)
1729
- - Find heavily-imported files (files imported by many others are high-value targets)
1730
- - Identify critical business logic (auth, payments, data mutations)
1731
-
1732
- 2. **Prioritize by impact**: Low coverage alone doesn't indicate priority. Consider:
1733
- - High utilization + low coverage = highest priority
1734
- - Large files with 0% coverage have bigger impact than small files
1735
- - Use find_uncovered_failure_areas for files with both low coverage AND test failures
1736
-
1737
- 3. **Use path-based queries**: The get_untested_files tool may return many files of a certain type (e.g., UI components). For targeted analysis, use get_coverage_for_file with path prefixes to focus on specific areas of the codebase.
1738
-
1739
- 4. **Iterate**: Get baseline → identify targets → write tests → re-check coverage after CI uploads new results.
2271
+ ${client.isUserToken() ? "You have a user API key with access to multiple projects. Use `list_projects` to find project IDs, then pass `projectId` to all codemode functions." : "Your token is scoped to a single project. Do NOT pass `projectId` — it resolves automatically."}
1740
2272
 
1741
- ## Finding Invisible Files
2273
+ ## How to Use
1742
2274
 
1743
- Coverage tools can only report on files that were loaded during test execution. Some files have 0% coverage but don't appear in reports at all - these are "invisible" files that were never imported.
2275
+ 1. Use \`search_tools\` to find relevant functions (or check the execute_code description for all declarations)
2276
+ 2. Use \`execute_code\` to run JavaScript that calls one or more functions
2277
+ 3. Results are returned as JSON — you can chain multiple calls in a single execution
1744
2278
 
1745
- To find invisible files:
1746
- 1. Use get_coverage_for_file with a path prefix (e.g., "server/") to see what Gaffer tracks
1747
- 2. Use the local Glob tool to list all source files in that path
1748
- 3. Compare the lists - files in local but NOT in Gaffer are invisible
1749
- 4. These files need tests that actually import them
2279
+ ## Example
1750
2280
 
1751
- Example: If get_coverage_for_file("server/api") returns user.ts, auth.ts, but Glob finds user.ts, auth.ts, billing.ts - then billing.ts is invisible and needs tests that import it.
1752
-
1753
- ## Agentic CI / Test Failure Diagnosis
1754
-
1755
- When helping diagnose CI failures or fix failing tests:
1756
-
1757
- 1. **Check flakiness first**: Use get_flaky_tests to identify non-deterministic tests.
1758
- Skip flaky tests unless the user specifically wants to stabilize them.
1759
-
1760
- 2. **Get failure details**: Use get_test_run_details with status='failed'
1761
- to see error messages and stack traces for failing tests.
1762
-
1763
- 3. **Group by root cause**: Use get_failure_clusters to see which failures
1764
- share the same underlying error fix the root cause, not individual tests.
1765
-
1766
- 4. **Check history**: Use get_test_history to understand if the failure is new
1767
- (regression) or recurring (existing bug).
1768
-
1769
- 5. **Verify fixes**: After code changes, use compare_test_metrics to confirm
1770
- the specific test now passes.
1771
-
1772
- 6. **Prioritize by risk**: Use find_uncovered_failure_areas to identify
1773
- which failing code has the lowest test coverage — fix those first.
1774
-
1775
- ## Checking Upload Status
2281
+ \`\`\`javascript
2282
+ // Get project health, then check flaky tests if any exist
2283
+ const health = await codemode.get_project_health({ projectId: "proj_abc" });
2284
+ if (health.flakyTestCount > 0) {
2285
+ const flaky = await codemode.get_flaky_tests({ projectId: "proj_abc" });
2286
+ return { health, flaky };
2287
+ }
2288
+ return { health };
2289
+ \`\`\`
2290
+
2291
+ ## Tips
2292
+
2293
+ - Use \`return\` to send data back the return value becomes the tool result
2294
+ - Use \`console.log()\` for debug output (captured and returned alongside results)
2295
+ - You can make up to 20 API calls per execution
2296
+ - All functions are async use \`await\`` });
2297
+ server.registerTool("execute_code", {
2298
+ title: "Execute Code",
2299
+ description: `Execute JavaScript code that calls Gaffer API functions via the \`codemode\` namespace.
2300
+
2301
+ Write async JavaScript — all functions are available as \`codemode.<function_name>(input)\`.
2302
+ Use \`return\` to send results back. Use \`console.log()\` for debug output.
2303
+
2304
+ ## Available Functions
2305
+
2306
+ \`\`\`typescript
2307
+ ${declarations}
2308
+ \`\`\`
2309
+
2310
+ ## Examples
2311
+
2312
+ \`\`\`javascript
2313
+ // Single call
2314
+ const health = await codemode.get_project_health({ projectId: "proj_abc" });
2315
+ return health;
2316
+ \`\`\`
2317
+
2318
+ \`\`\`javascript
2319
+ // Multi-step: get flaky tests and check history for each
2320
+ const flaky = await codemode.get_flaky_tests({ projectId: "proj_abc", limit: 5 });
2321
+ const histories = [];
2322
+ for (const test of flaky.flakyTests) {
2323
+ const history = await codemode.get_test_history({ projectId: "proj_abc", testName: test.name, limit: 5 });
2324
+ histories.push({ test: test.name, score: test.flakinessScore, history: history.summary });
2325
+ }
2326
+ return { flaky: flaky.summary, details: histories };
2327
+ \`\`\`
2328
+
2329
+ \`\`\`javascript
2330
+ // Coverage analysis
2331
+ const summary = await codemode.get_coverage_summary({ projectId: "proj_abc" });
2332
+ const lowFiles = await codemode.get_coverage_for_file({ projectId: "proj_abc", maxCoverage: 50, limit: 10 });
2333
+ return { summary, lowCoverageFiles: lowFiles };
2334
+ \`\`\`
2335
+
2336
+ ## Constraints
2337
+
2338
+ - Max 20 API calls per execution
2339
+ - 30s timeout
2340
+ - No access to Node.js globals (process, require, etc.)`,
2341
+ inputSchema: { code: z.string().describe("JavaScript code to execute. Use `codemode.<function>()` to call API functions. Use `return` for results.") }
2342
+ }, async (input) => {
2343
+ try {
2344
+ const result = await executeCode(input.code, namespace);
2345
+ const output = {};
2346
+ if (result.result !== void 0) output.result = result.result;
2347
+ if (result.logs.length > 0) output.logs = result.logs;
2348
+ output.durationMs = result.durationMs;
2349
+ let text;
2350
+ try {
2351
+ text = JSON.stringify(output, null, 2);
2352
+ } catch {
2353
+ text = JSON.stringify({
2354
+ error: "Result could not be serialized to JSON (possible circular reference). Use console.log() to inspect the result, or return a simpler object.",
2355
+ logs: result.logs.length > 0 ? result.logs : void 0,
2356
+ durationMs: result.durationMs
2357
+ });
2358
+ }
2359
+ return { content: [{
2360
+ type: "text",
2361
+ text
2362
+ }] };
2363
+ } catch (error) {
2364
+ return handleToolError("execute_code", error);
2365
+ }
2366
+ });
2367
+ server.registerTool("search_tools", {
2368
+ title: "Search Tools",
2369
+ description: `Search for available Gaffer API functions by keyword.
1776
2370
 
1777
- When an agent needs to know if CI results are ready:
2371
+ Returns matching functions with their TypeScript declarations so you can use them with execute_code.
1778
2372
 
1779
- 1. Use get_upload_status with commitSha or branch to find upload sessions
1780
- 2. Check processingStatus: "completed" means results are ready, "processing" means wait
1781
- 3. Once completed, use the linked testRunIds to get test results` });
1782
- registerTool(server, client, {
1783
- metadata: getProjectHealthMetadata,
1784
- inputSchema: getProjectHealthInputSchema,
1785
- outputSchema: getProjectHealthOutputSchema,
1786
- execute: executeGetProjectHealth
1787
- });
1788
- registerTool(server, client, {
1789
- metadata: getTestHistoryMetadata,
1790
- inputSchema: getTestHistoryInputSchema,
1791
- outputSchema: getTestHistoryOutputSchema,
1792
- execute: executeGetTestHistory
1793
- });
1794
- registerTool(server, client, {
1795
- metadata: getFlakyTestsMetadata,
1796
- inputSchema: getFlakyTestsInputSchema,
1797
- outputSchema: getFlakyTestsOutputSchema,
1798
- execute: executeGetFlakyTests
1799
- });
1800
- registerTool(server, client, {
1801
- metadata: listTestRunsMetadata,
1802
- inputSchema: listTestRunsInputSchema,
1803
- outputSchema: listTestRunsOutputSchema,
1804
- execute: executeListTestRuns
2373
+ Examples:
2374
+ - "coverage" coverage-related functions
2375
+ - "flaky" flaky test detection
2376
+ - "" (empty) list all available functions`,
2377
+ inputSchema: searchToolsInputSchema
2378
+ }, async (input) => {
2379
+ try {
2380
+ const result = executeSearchTools(registry, input);
2381
+ return { content: [{
2382
+ type: "text",
2383
+ text: JSON.stringify(result, null, 2)
2384
+ }] };
2385
+ } catch (error) {
2386
+ return handleToolError("search_tools", error);
2387
+ }
1805
2388
  });
1806
- if (client.isUserToken()) registerTool(server, client, {
1807
- metadata: listProjectsMetadata,
2389
+ if (client.isUserToken()) server.registerTool(listProjectsMetadata.name, {
2390
+ title: listProjectsMetadata.title,
2391
+ description: listProjectsMetadata.description,
1808
2392
  inputSchema: listProjectsInputSchema,
1809
- outputSchema: listProjectsOutputSchema,
1810
- execute: executeListProjects
1811
- });
1812
- registerTool(server, client, {
1813
- metadata: getReportMetadata,
1814
- inputSchema: getReportInputSchema,
1815
- outputSchema: getReportOutputSchema,
1816
- execute: executeGetReport
1817
- });
1818
- registerTool(server, client, {
1819
- metadata: getSlowestTestsMetadata,
1820
- inputSchema: getSlowestTestsInputSchema,
1821
- outputSchema: getSlowestTestsOutputSchema,
1822
- execute: executeGetSlowestTests
1823
- });
1824
- registerTool(server, client, {
1825
- metadata: getTestRunDetailsMetadata,
1826
- inputSchema: getTestRunDetailsInputSchema,
1827
- outputSchema: getTestRunDetailsOutputSchema,
1828
- execute: executeGetTestRunDetails
1829
- });
1830
- registerTool(server, client, {
1831
- metadata: getFailureClustersMetadata,
1832
- inputSchema: getFailureClustersInputSchema,
1833
- outputSchema: getFailureClustersOutputSchema,
1834
- execute: executeGetFailureClusters
1835
- });
1836
- registerTool(server, client, {
1837
- metadata: compareTestMetricsMetadata,
1838
- inputSchema: compareTestMetricsInputSchema,
1839
- outputSchema: compareTestMetricsOutputSchema,
1840
- execute: executeCompareTestMetrics
1841
- });
1842
- registerTool(server, client, {
1843
- metadata: getCoverageSummaryMetadata,
1844
- inputSchema: getCoverageSummaryInputSchema,
1845
- outputSchema: getCoverageSummaryOutputSchema,
1846
- execute: executeGetCoverageSummary
1847
- });
1848
- registerTool(server, client, {
1849
- metadata: getCoverageForFileMetadata,
1850
- inputSchema: getCoverageForFileInputSchema,
1851
- outputSchema: getCoverageForFileOutputSchema,
1852
- execute: executeGetCoverageForFile
1853
- });
1854
- registerTool(server, client, {
1855
- metadata: findUncoveredFailureAreasMetadata,
1856
- inputSchema: findUncoveredFailureAreasInputSchema,
1857
- outputSchema: findUncoveredFailureAreasOutputSchema,
1858
- execute: executeFindUncoveredFailureAreas
1859
- });
1860
- registerTool(server, client, {
1861
- metadata: getUntestedFilesMetadata,
1862
- inputSchema: getUntestedFilesInputSchema,
1863
- outputSchema: getUntestedFilesOutputSchema,
1864
- execute: executeGetUntestedFiles
1865
- });
1866
- registerTool(server, client, {
1867
- metadata: getReportBrowserUrlMetadata,
1868
- inputSchema: getReportBrowserUrlInputSchema,
1869
- outputSchema: getReportBrowserUrlOutputSchema,
1870
- execute: executeGetReportBrowserUrl
1871
- });
1872
- registerTool(server, client, {
1873
- metadata: getUploadStatusMetadata,
1874
- inputSchema: getUploadStatusInputSchema,
1875
- outputSchema: getUploadStatusOutputSchema,
1876
- execute: executeGetUploadStatus
2393
+ outputSchema: listProjectsOutputSchema
2394
+ }, async (input) => {
2395
+ try {
2396
+ const output = await executeListProjects(client, input);
2397
+ return {
2398
+ content: [{
2399
+ type: "text",
2400
+ text: JSON.stringify(output, null, 2)
2401
+ }],
2402
+ structuredContent: output
2403
+ };
2404
+ } catch (error) {
2405
+ return handleToolError(listProjectsMetadata.name, error);
2406
+ }
1877
2407
  });
1878
2408
  const transport = new StdioServerTransport();
1879
2409
  await server.connect(transport);