@gaffer-sh/mcp 0.6.1 → 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,
@@ -26,23 +25,28 @@ function sleep(ms) {
26
25
  /**
27
26
  * Detect token type from prefix
28
27
  * - gaf_ = user API Key (read-only, cross-project)
29
- * - gfr_ = Project Upload Token (legacy, single project)
28
+ * - gfr_ = Project Token (single project)
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
37
37
  *
38
38
  * Supports two authentication modes:
39
39
  * 1. User API Keys (gaf_) - Read-only access to all user's projects
40
- * 2. Project Upload Tokens (gfr_) - Legacy, single project access
40
+ * 2. Project Tokens (gfr_) - Single project access, auto-resolves projectId
41
+ *
42
+ * All methods use the unified /user/projects/:id/ route tree.
43
+ * Project tokens auto-resolve their projectId via /project on first use.
41
44
  */
42
45
  var GafferApiClient = class GafferApiClient {
43
46
  apiKey;
44
47
  baseUrl;
45
48
  tokenType;
49
+ resolveProjectIdPromise = null;
46
50
  constructor(config) {
47
51
  this.apiKey = config.apiKey;
48
52
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
@@ -52,7 +56,7 @@ var GafferApiClient = class GafferApiClient {
52
56
  * Create client from environment variables
53
57
  *
54
58
  * Supports:
55
- * - GAFFER_API_KEY (for user API Keys gaf_)
59
+ * - GAFFER_API_KEY (for user API Keys gaf_ or project tokens gfr_)
56
60
  */
57
61
  static fromEnv() {
58
62
  const apiKey = process.env.GAFFER_API_KEY;
@@ -69,6 +73,24 @@ var GafferApiClient = class GafferApiClient {
69
73
  return this.tokenType === "user";
70
74
  }
71
75
  /**
76
+ * Resolve the project ID for the current token.
77
+ * For project tokens, fetches from /project on first call and caches the Promise
78
+ * to deduplicate concurrent calls.
79
+ * For user tokens, requires explicit projectId.
80
+ */
81
+ async resolveProjectId(projectId) {
82
+ if (projectId) return projectId;
83
+ if (this.isUserToken()) throw new Error("projectId is required when using a user API Key");
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;
92
+ }
93
+ /**
72
94
  * Make authenticated request to Gaffer API with retry logic
73
95
  */
74
96
  async request(endpoint, params) {
@@ -129,13 +151,8 @@ var GafferApiClient = class GafferApiClient {
129
151
  throw lastError || /* @__PURE__ */ new Error("Request failed after retries");
130
152
  }
131
153
  /**
132
- * List all projects the user has access to
133
- * Requires user API Key (gaf_)
134
- *
135
- * @param options - Query options
136
- * @param options.organizationId - Filter by organization ID
137
- * @param options.limit - Maximum number of results
138
- * @param options.offset - Offset for pagination
154
+ * List all projects the user has access to.
155
+ * Requires user API Key (gaf_). Not available with project tokens.
139
156
  */
140
157
  async listProjects(options = {}) {
141
158
  if (!this.isUserToken()) throw new Error("list_projects is not available with project tokens (gfr_). Your token is already scoped to a single project — call tools directly without passing projectId.");
@@ -147,40 +164,20 @@ var GafferApiClient = class GafferApiClient {
147
164
  }
148
165
  /**
149
166
  * Get project health analytics
150
- *
151
- * @param options - Query options
152
- * @param options.projectId - Required for user tokens, ignored for project tokens
153
- * @param options.days - Analysis period in days (default: 30)
154
167
  */
155
168
  async getProjectHealth(options = {}) {
156
- if (this.isUserToken()) {
157
- if (!options.projectId) throw new Error("projectId is required when using a user API Key");
158
- return this.request(`/user/projects/${options.projectId}/health`, { days: options.days || 30 });
159
- }
160
- return this.request("/project/analytics", { days: options.days || 30 });
169
+ const projectId = await this.resolveProjectId(options.projectId);
170
+ return this.request(`/user/projects/${projectId}/health`, { days: options.days || 30 });
161
171
  }
162
172
  /**
163
173
  * Get test history for a specific test
164
- *
165
- * @param options - Query options
166
- * @param options.projectId - Required for user tokens, ignored for project tokens
167
- * @param options.testName - Test name to search for
168
- * @param options.filePath - File path to search for
169
- * @param options.limit - Maximum number of results
170
174
  */
171
175
  async getTestHistory(options) {
172
176
  const testName = options.testName?.trim();
173
177
  const filePath = options.filePath?.trim();
174
178
  if (!testName && !filePath) throw new Error("Either testName or filePath is required (and must not be empty)");
175
- if (this.isUserToken()) {
176
- if (!options.projectId) throw new Error("projectId is required when using a user API Key");
177
- return this.request(`/user/projects/${options.projectId}/test-history`, {
178
- ...testName && { testName },
179
- ...filePath && { filePath },
180
- ...options.limit && { limit: options.limit }
181
- });
182
- }
183
- return this.request("/project/test-history", {
179
+ const projectId = await this.resolveProjectId(options.projectId);
180
+ return this.request(`/user/projects/${projectId}/test-history`, {
184
181
  ...testName && { testName },
185
182
  ...filePath && { filePath },
186
183
  ...options.limit && { limit: options.limit }
@@ -188,23 +185,10 @@ var GafferApiClient = class GafferApiClient {
188
185
  }
189
186
  /**
190
187
  * Get flaky tests for the project
191
- *
192
- * @param options - Query options
193
- * @param options.projectId - Required for user tokens, ignored for project tokens
194
- * @param options.threshold - Minimum flip rate to be considered flaky (0-1)
195
- * @param options.limit - Maximum number of results
196
- * @param options.days - Analysis period in days
197
188
  */
198
189
  async getFlakyTests(options = {}) {
199
- if (this.isUserToken()) {
200
- if (!options.projectId) throw new Error("projectId is required when using a user API Key");
201
- return this.request(`/user/projects/${options.projectId}/flaky-tests`, {
202
- ...options.threshold && { threshold: options.threshold },
203
- ...options.limit && { limit: options.limit },
204
- ...options.days && { days: options.days }
205
- });
206
- }
207
- return this.request("/project/flaky-tests", {
190
+ const projectId = await this.resolveProjectId(options.projectId);
191
+ return this.request(`/user/projects/${projectId}/flaky-tests`, {
208
192
  ...options.threshold && { threshold: options.threshold },
209
193
  ...options.limit && { limit: options.limit },
210
194
  ...options.days && { days: options.days }
@@ -212,25 +196,10 @@ var GafferApiClient = class GafferApiClient {
212
196
  }
213
197
  /**
214
198
  * List test runs for the project
215
- *
216
- * @param options - Query options
217
- * @param options.projectId - Required for user tokens, ignored for project tokens
218
- * @param options.commitSha - Filter by commit SHA
219
- * @param options.branch - Filter by branch name
220
- * @param options.status - Filter by status ('passed' or 'failed')
221
- * @param options.limit - Maximum number of results
222
199
  */
223
200
  async getTestRuns(options = {}) {
224
- if (this.isUserToken()) {
225
- if (!options.projectId) throw new Error("projectId is required when using a user API Key");
226
- return this.request(`/user/projects/${options.projectId}/test-runs`, {
227
- ...options.commitSha && { commitSha: options.commitSha },
228
- ...options.branch && { branch: options.branch },
229
- ...options.status && { status: options.status },
230
- ...options.limit && { limit: options.limit }
231
- });
232
- }
233
- return this.request("/project/test-runs", {
201
+ const projectId = await this.resolveProjectId(options.projectId);
202
+ return this.request(`/user/projects/${projectId}/test-runs`, {
234
203
  ...options.commitSha && { commitSha: options.commitSha },
235
204
  ...options.branch && { branch: options.branch },
236
205
  ...options.status && { status: options.status },
@@ -238,31 +207,21 @@ var GafferApiClient = class GafferApiClient {
238
207
  });
239
208
  }
240
209
  /**
241
- * Get report files for a test run
242
- *
243
- * @param testRunId - The test run ID
244
- * @returns Report metadata with download URLs for each file
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.
245
213
  */
246
214
  async getReport(testRunId) {
247
- if (!this.isUserToken()) throw new Error("getReport requires a user API Key (gaf_). Upload Tokens (gfr_) cannot access reports via API.");
215
+ if (!this.isUserToken()) throw new Error("getReport requires a user API Key (gaf_). Project tokens (gfr_) cannot access reports via API.");
248
216
  if (!testRunId) throw new Error("testRunId is required");
249
217
  return this.request(`/user/test-runs/${testRunId}/report`);
250
218
  }
251
219
  /**
252
220
  * Get slowest tests for a project
253
- *
254
- * @param options - Query options
255
- * @param options.projectId - The project ID (required)
256
- * @param options.days - Analysis period in days (default: 30)
257
- * @param options.limit - Maximum number of results (default: 20)
258
- * @param options.framework - Filter by test framework
259
- * @param options.branch - Filter by git branch name
260
- * @returns Slowest tests sorted by P95 duration
261
221
  */
262
222
  async getSlowestTests(options) {
263
- if (!this.isUserToken()) throw new Error("getSlowestTests requires a user API Key (gaf_).");
264
- if (!options.projectId) throw new Error("projectId is required");
265
- return this.request(`/user/projects/${options.projectId}/slowest-tests`, {
223
+ const projectId = await this.resolveProjectId(options.projectId);
224
+ return this.request(`/user/projects/${projectId}/slowest-tests`, {
266
225
  ...options.days && { days: options.days },
267
226
  ...options.limit && { limit: options.limit },
268
227
  ...options.framework && { framework: options.framework },
@@ -271,20 +230,11 @@ var GafferApiClient = class GafferApiClient {
271
230
  }
272
231
  /**
273
232
  * Get parsed test results for a specific test run
274
- *
275
- * @param options - Query options
276
- * @param options.projectId - The project ID (required)
277
- * @param options.testRunId - The test run ID (required)
278
- * @param options.status - Filter by test status ('passed', 'failed', 'skipped')
279
- * @param options.limit - Maximum number of results (default: 100)
280
- * @param options.offset - Pagination offset (default: 0)
281
- * @returns Parsed test cases with pagination
282
233
  */
283
234
  async getTestRunDetails(options) {
284
- if (!this.isUserToken()) throw new Error("getTestRunDetails requires a user API Key (gaf_).");
285
- if (!options.projectId) throw new Error("projectId is required");
286
235
  if (!options.testRunId) throw new Error("testRunId is required");
287
- return this.request(`/user/projects/${options.projectId}/test-runs/${options.testRunId}/details`, {
236
+ const projectId = await this.resolveProjectId(options.projectId);
237
+ return this.request(`/user/projects/${projectId}/test-runs/${options.testRunId}/details`, {
288
238
  ...options.status && { status: options.status },
289
239
  ...options.limit && { limit: options.limit },
290
240
  ...options.offset && { offset: options.offset }
@@ -292,21 +242,11 @@ var GafferApiClient = class GafferApiClient {
292
242
  }
293
243
  /**
294
244
  * Compare test metrics between two commits or test runs
295
- *
296
- * @param options - Query options
297
- * @param options.projectId - The project ID (required)
298
- * @param options.testName - The test name to compare (required)
299
- * @param options.beforeCommit - Commit SHA for before (use with afterCommit)
300
- * @param options.afterCommit - Commit SHA for after (use with beforeCommit)
301
- * @param options.beforeRunId - Test run ID for before (use with afterRunId)
302
- * @param options.afterRunId - Test run ID for after (use with beforeRunId)
303
- * @returns Comparison of test metrics
304
245
  */
305
246
  async compareTestMetrics(options) {
306
- if (!this.isUserToken()) throw new Error("compareTestMetrics requires a user API Key (gaf_).");
307
- if (!options.projectId) throw new Error("projectId is required");
308
247
  if (!options.testName) throw new Error("testName is required");
309
- return this.request(`/user/projects/${options.projectId}/compare-test`, {
248
+ const projectId = await this.resolveProjectId(options.projectId);
249
+ return this.request(`/user/projects/${projectId}/compare-test`, {
310
250
  testName: options.testName,
311
251
  ...options.beforeCommit && { beforeCommit: options.beforeCommit },
312
252
  ...options.afterCommit && { afterCommit: options.afterCommit },
@@ -316,35 +256,17 @@ var GafferApiClient = class GafferApiClient {
316
256
  }
317
257
  /**
318
258
  * Get coverage summary for a project
319
- *
320
- * @param options - Query options
321
- * @param options.projectId - The project ID (required)
322
- * @param options.days - Analysis period in days (default: 30)
323
- * @returns Coverage summary with trends and lowest coverage files
324
259
  */
325
260
  async getCoverageSummary(options) {
326
- if (!this.isUserToken()) throw new Error("getCoverageSummary requires a user API Key (gaf_).");
327
- if (!options.projectId) throw new Error("projectId is required");
328
- return this.request(`/user/projects/${options.projectId}/coverage-summary`, { ...options.days && { days: options.days } });
261
+ const projectId = await this.resolveProjectId(options.projectId);
262
+ return this.request(`/user/projects/${projectId}/coverage-summary`, { ...options.days && { days: options.days } });
329
263
  }
330
264
  /**
331
265
  * Get coverage files for a project with filtering
332
- *
333
- * @param options - Query options
334
- * @param options.projectId - The project ID (required)
335
- * @param options.filePath - Filter to specific file path
336
- * @param options.minCoverage - Minimum coverage percentage
337
- * @param options.maxCoverage - Maximum coverage percentage
338
- * @param options.limit - Maximum number of results
339
- * @param options.offset - Pagination offset
340
- * @param options.sortBy - Sort by 'path' or 'coverage'
341
- * @param options.sortOrder - Sort order 'asc' or 'desc'
342
- * @returns List of files with coverage data
343
266
  */
344
267
  async getCoverageFiles(options) {
345
- if (!this.isUserToken()) throw new Error("getCoverageFiles requires a user API Key (gaf_).");
346
- if (!options.projectId) throw new Error("projectId is required");
347
- return this.request(`/user/projects/${options.projectId}/coverage/files`, {
268
+ const projectId = await this.resolveProjectId(options.projectId);
269
+ return this.request(`/user/projects/${projectId}/coverage/files`, {
348
270
  ...options.filePath && { filePath: options.filePath },
349
271
  ...options.minCoverage !== void 0 && { minCoverage: options.minCoverage },
350
272
  ...options.maxCoverage !== void 0 && { maxCoverage: options.maxCoverage },
@@ -356,65 +278,36 @@ var GafferApiClient = class GafferApiClient {
356
278
  }
357
279
  /**
358
280
  * Get risk areas (files with low coverage AND test failures)
359
- *
360
- * @param options - Query options
361
- * @param options.projectId - The project ID (required)
362
- * @param options.days - Analysis period in days (default: 30)
363
- * @param options.coverageThreshold - Include files below this coverage (default: 80)
364
- * @returns List of risk areas sorted by risk score
365
281
  */
366
282
  async getCoverageRiskAreas(options) {
367
- if (!this.isUserToken()) throw new Error("getCoverageRiskAreas requires a user API Key (gaf_).");
368
- if (!options.projectId) throw new Error("projectId is required");
369
- return this.request(`/user/projects/${options.projectId}/coverage/risk-areas`, {
283
+ const projectId = await this.resolveProjectId(options.projectId);
284
+ return this.request(`/user/projects/${projectId}/coverage/risk-areas`, {
370
285
  ...options.days && { days: options.days },
371
286
  ...options.coverageThreshold !== void 0 && { coverageThreshold: options.coverageThreshold }
372
287
  });
373
288
  }
374
289
  /**
375
290
  * Get a browser-navigable URL for viewing a test report
376
- *
377
- * @param options - Query options
378
- * @param options.projectId - The project ID (required)
379
- * @param options.testRunId - The test run ID (required)
380
- * @param options.filename - Specific file to open (default: index.html)
381
- * @returns URL with signed token for browser access
382
291
  */
383
292
  async getReportBrowserUrl(options) {
384
- if (!this.isUserToken()) throw new Error("getReportBrowserUrl requires a user API Key (gaf_).");
385
- if (!options.projectId) throw new Error("projectId is required");
386
293
  if (!options.testRunId) throw new Error("testRunId is required");
387
- return this.request(`/user/projects/${options.projectId}/reports/${options.testRunId}/browser-url`, { ...options.filename && { filename: options.filename } });
294
+ const projectId = await this.resolveProjectId(options.projectId);
295
+ return this.request(`/user/projects/${projectId}/reports/${options.testRunId}/browser-url`, { ...options.filename && { filename: options.filename } });
388
296
  }
389
297
  /**
390
298
  * Get failure clusters for a test run
391
- *
392
- * @param options - Query options
393
- * @param options.projectId - The project ID (required)
394
- * @param options.testRunId - The test run ID (required)
395
- * @returns Failure clusters grouped by error similarity
396
299
  */
397
300
  async getFailureClusters(options) {
398
- if (!this.isUserToken()) throw new Error("getFailureClusters requires a user API Key (gaf_).");
399
- if (!options.projectId) throw new Error("projectId is required");
400
301
  if (!options.testRunId) throw new Error("testRunId is required");
401
- return this.request(`/user/projects/${options.projectId}/test-runs/${options.testRunId}/failure-clusters`);
302
+ const projectId = await this.resolveProjectId(options.projectId);
303
+ return this.request(`/user/projects/${projectId}/test-runs/${options.testRunId}/failure-clusters`);
402
304
  }
403
305
  /**
404
306
  * List upload sessions for a project
405
- *
406
- * @param options - Query options
407
- * @param options.projectId - The project ID (required)
408
- * @param options.commitSha - Filter by commit SHA
409
- * @param options.branch - Filter by branch name
410
- * @param options.limit - Maximum number of results (default: 10)
411
- * @param options.offset - Pagination offset (default: 0)
412
- * @returns Paginated list of upload sessions
413
307
  */
414
308
  async listUploadSessions(options) {
415
- if (!this.isUserToken()) throw new Error("listUploadSessions requires a user API Key (gaf_).");
416
- if (!options.projectId) throw new Error("projectId is required");
417
- return this.request(`/user/projects/${options.projectId}/upload-sessions`, {
309
+ const projectId = await this.resolveProjectId(options.projectId);
310
+ return this.request(`/user/projects/${projectId}/upload-sessions`, {
418
311
  ...options.commitSha && { commitSha: options.commitSha },
419
312
  ...options.branch && { branch: options.branch },
420
313
  ...options.limit && { limit: options.limit },
@@ -423,27 +316,133 @@ var GafferApiClient = class GafferApiClient {
423
316
  }
424
317
  /**
425
318
  * Get upload session detail with linked results
426
- *
427
- * @param options - Query options
428
- * @param options.projectId - The project ID (required)
429
- * @param options.sessionId - The upload session ID (required)
430
- * @returns Upload session details with linked test runs and coverage reports
431
319
  */
432
320
  async getUploadSessionDetail(options) {
433
- if (!this.isUserToken()) throw new Error("getUploadSessionDetail requires a user API Key (gaf_).");
434
- if (!options.projectId) throw new Error("projectId is required");
435
321
  if (!options.sessionId) throw new Error("sessionId is required");
436
- return this.request(`/user/projects/${options.projectId}/upload-sessions/${options.sessionId}`);
322
+ const projectId = await this.resolveProjectId(options.projectId);
323
+ return this.request(`/user/projects/${projectId}/upload-sessions/${options.sessionId}`);
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
+ });
437
338
  }
438
339
  };
439
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
+
440
439
  //#endregion
441
440
  //#region src/tools/compare-test-metrics.ts
442
441
  /**
443
442
  * Input schema for compare_test_metrics tool
444
443
  */
445
444
  const compareTestMetricsInputSchema = {
446
- projectId: z.string().describe("Project ID. Use list_projects to find project IDs."),
445
+ projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
447
446
  testName: z.string().describe("The test name to compare. Can be the short name or full name including describe blocks."),
448
447
  beforeCommit: z.string().optional().describe("Commit SHA for the \"before\" measurement. Use with afterCommit."),
449
448
  afterCommit: z.string().optional().describe("Commit SHA for the \"after\" measurement. Use with beforeCommit."),
@@ -518,7 +517,7 @@ const compareTestMetricsMetadata = {
518
517
  Useful for measuring the impact of code changes on test performance or reliability.
519
518
 
520
519
  Parameters:
521
- - projectId (required): Project ID
520
+ - projectId (optional): Project ID — required for user API keys, auto-resolved for project tokens
522
521
  - testName (required): The test name to compare (short name or full name)
523
522
  - Option 1 - Compare by commit:
524
523
  - beforeCommit: Commit SHA for "before" measurement
@@ -553,7 +552,7 @@ Tip: Use get_test_history first to find the commit SHAs or test run IDs you want
553
552
  * Input schema for find_uncovered_failure_areas tool
554
553
  */
555
554
  const findUncoveredFailureAreasInputSchema = {
556
- projectId: z.string().describe("Project ID. Use list_projects to find project IDs."),
555
+ projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
557
556
  days: z.number().int().min(1).max(365).optional().describe("Number of days to analyze for test failures (default: 30)"),
558
557
  coverageThreshold: z.number().min(0).max(100).optional().describe("Include files with coverage below this percentage (default: 80)")
559
558
  };
@@ -601,7 +600,7 @@ areas in your codebase that need attention. Files are ranked by a "risk score"
601
600
  calculated as: (100 - coverage%) × failureCount.
602
601
 
603
602
  Parameters:
604
- - projectId: The project to analyze (required)
603
+ - projectId (optional): Project ID required for user API keys, auto-resolved for project tokens
605
604
  - days: Analysis period for test failures (default: 30)
606
605
  - coverageThreshold: Include files below this coverage % (default: 80)
607
606
 
@@ -618,7 +617,7 @@ Use this to prioritize which parts of your codebase need better test coverage.`
618
617
  * Input schema for get_coverage_for_file tool
619
618
  */
620
619
  const getCoverageForFileInputSchema = {
621
- projectId: z.string().describe("Project ID. Use list_projects to find project IDs."),
620
+ projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
622
621
  filePath: z.string().describe("File path to get coverage for. Can be exact path or partial match.")
623
622
  };
624
623
  /**
@@ -675,7 +674,7 @@ const getCoverageForFileMetadata = {
675
674
  description: `Get coverage metrics for a specific file or files matching a path pattern.
676
675
 
677
676
  Parameters:
678
- - projectId: The project to query (required)
677
+ - projectId (optional): Project ID required for user API keys, auto-resolved for project tokens
679
678
  - filePath: File path to search for (exact or partial match)
680
679
 
681
680
  Returns:
@@ -701,7 +700,7 @@ Prioritize: high utilization + low coverage = highest impact.`
701
700
  * Input schema for get_coverage_summary tool
702
701
  */
703
702
  const getCoverageSummaryInputSchema = {
704
- projectId: z.string().describe("Project ID. Use list_projects to find project IDs."),
703
+ projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
705
704
  days: z.number().int().min(1).max(365).optional().describe("Number of days to analyze for trends (default: 30)")
706
705
  };
707
706
  /**
@@ -776,7 +775,7 @@ high-value targets in critical code paths rather than just the files with lowest
776
775
  * Input schema for get_failure_clusters tool
777
776
  */
778
777
  const getFailureClustersInputSchema = {
779
- projectId: z.string().describe("Project ID. Use list_projects to find project IDs."),
778
+ projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
780
779
  testRunId: z.string().describe("Test run ID. Use list_test_runs to find test run IDs.")
781
780
  };
782
781
  /**
@@ -814,7 +813,7 @@ const getFailureClustersMetadata = {
814
813
  description: `Group failed tests by root cause using error message similarity.
815
814
 
816
815
  Parameters:
817
- - projectId (required): The project ID
816
+ - projectId (optional): Project ID — required for user API keys, auto-resolved for project tokens
818
817
  - testRunId (required): The test run ID to analyze
819
818
 
820
819
  Returns:
@@ -979,7 +978,7 @@ Use this to understand the current state of your test suite.`
979
978
  * Input schema for get_report_browser_url tool
980
979
  */
981
980
  const getReportBrowserUrlInputSchema = {
982
- projectId: z.string().describe("Project ID. Use list_projects to find project IDs."),
981
+ projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
983
982
  testRunId: z.string().describe("The test run ID to get the report URL for. Use list_test_runs to find test run IDs."),
984
983
  filename: z.string().optional().describe("Specific file to open (default: index.html or first HTML file)")
985
984
  };
@@ -1022,7 +1021,7 @@ Returns a signed URL that can be opened directly in a browser without requiring
1022
1021
  the user to log in. The URL expires after 30 minutes for security.
1023
1022
 
1024
1023
  Parameters:
1025
- - projectId: The project the test run belongs to (required)
1024
+ - projectId (optional): Project ID required for user API keys, auto-resolved for project tokens
1026
1025
  - testRunId: The test run to view (required)
1027
1026
  - filename: Specific file to open (optional, defaults to index.html)
1028
1027
 
@@ -1125,7 +1124,7 @@ Use cases:
1125
1124
  * Input schema for get_slowest_tests tool
1126
1125
  */
1127
1126
  const getSlowestTestsInputSchema = {
1128
- projectId: z.string().describe("Project ID. Use list_projects to find project IDs."),
1127
+ projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
1129
1128
  days: z.number().int().min(1).max(365).optional().describe("Analysis period in days (default: 30)"),
1130
1129
  limit: z.number().int().min(1).max(100).optional().describe("Maximum number of tests to return (default: 20)"),
1131
1130
  framework: z.string().optional().describe("Filter by test framework (e.g., \"playwright\", \"vitest\", \"jest\")"),
@@ -1184,7 +1183,7 @@ const getSlowestTestsMetadata = {
1184
1183
  description: `Get the slowest tests in a project, sorted by P95 duration.
1185
1184
 
1186
1185
  Parameters:
1187
- - projectId (required): Project ID to analyze
1186
+ - projectId (optional): Project ID required for user API keys, auto-resolved for project tokens
1188
1187
  - days (optional): Analysis period in days (default: 30, max: 365)
1189
1188
  - limit (optional): Max tests to return (default: 20, max: 100)
1190
1189
  - framework (optional): Filter by framework (e.g., "playwright", "vitest")
@@ -1302,7 +1301,7 @@ Use this to investigate flaky tests or understand test stability.`
1302
1301
  */
1303
1302
  const getTestRunDetailsInputSchema = {
1304
1303
  testRunId: z.string().describe("The test run ID to get details for. Use list_test_runs to find test run IDs."),
1305
- projectId: z.string().describe("Project ID. Use list_projects to find project IDs."),
1304
+ projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
1306
1305
  status: z.enum([
1307
1306
  "passed",
1308
1307
  "failed",
@@ -1378,7 +1377,7 @@ const getTestRunDetailsMetadata = {
1378
1377
 
1379
1378
  Parameters:
1380
1379
  - testRunId (required): The test run ID to get details for
1381
- - projectId (required): Project ID the test run belongs to
1380
+ - projectId (optional): Project ID required for user API keys, auto-resolved for project tokens
1382
1381
  - status (optional): Filter by test status: "passed", "failed", or "skipped"
1383
1382
  - limit (optional): Max tests to return (default: 100, max: 500)
1384
1383
  - offset (optional): Pagination offset (default: 0)
@@ -1416,7 +1415,7 @@ use get_test_history, get_flaky_tests, or get_slowest_tests instead.`
1416
1415
  * Input schema for get_untested_files tool
1417
1416
  */
1418
1417
  const getUntestedFilesInputSchema = {
1419
- projectId: z.string().describe("Project ID. Use list_projects to find project IDs."),
1418
+ projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
1420
1419
  maxCoverage: z.number().min(0).max(100).optional().describe("Maximum coverage percentage to include (default: 10 for \"untested\")"),
1421
1420
  limit: z.number().int().min(1).max(100).optional().describe("Maximum number of files to return (default: 20)")
1422
1421
  };
@@ -1483,7 +1482,7 @@ Returns files sorted by coverage percentage (lowest first), filtered
1483
1482
  to only include files below a coverage threshold.
1484
1483
 
1485
1484
  Parameters:
1486
- - projectId: The project to analyze (required)
1485
+ - projectId (optional): Project ID required for user API keys, auto-resolved for project tokens
1487
1486
  - maxCoverage: Include files with coverage at or below this % (default: 10)
1488
1487
  - limit: Maximum number of files to return (default: 20, max: 100)
1489
1488
 
@@ -1507,7 +1506,7 @@ for those specific paths.`
1507
1506
  * Input schema for get_upload_status tool
1508
1507
  */
1509
1508
  const getUploadStatusInputSchema = {
1510
- projectId: z.string().describe("Project ID. Use list_projects to find project IDs."),
1509
+ projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
1511
1510
  sessionId: z.string().optional().describe("Specific upload session ID. If provided, returns detailed status for that session. Otherwise, lists recent sessions."),
1512
1511
  commitSha: z.string().optional().describe("Filter sessions by commit SHA. Useful for checking if results for a specific commit are ready."),
1513
1512
  branch: z.string().optional().describe("Filter sessions by branch name.")
@@ -1581,7 +1580,7 @@ const getUploadStatusMetadata = {
1581
1580
  Use this tool to answer "are my test results ready?" after pushing code.
1582
1581
 
1583
1582
  Parameters:
1584
- - projectId (required): The project ID
1583
+ - projectId (optional): Project ID — required for user API keys, auto-resolved for project tokens
1585
1584
  - sessionId (optional): Specific upload session ID for detailed status
1586
1585
  - commitSha (optional): Filter by commit SHA to find uploads for a specific commit
1587
1586
  - branch (optional): Filter by branch name
@@ -1612,63 +1611,6 @@ Returns (detail mode):
1612
1611
  - coverageReports: Linked coverage report summaries (id, format)`
1613
1612
  };
1614
1613
 
1615
- //#endregion
1616
- //#region src/tools/list-projects.ts
1617
- /**
1618
- * Input schema for list_projects tool
1619
- */
1620
- const listProjectsInputSchema = {
1621
- organizationId: z.string().optional().describe("Filter by organization ID (optional)"),
1622
- limit: z.number().int().min(1).max(100).optional().describe("Maximum number of projects to return (default: 50)")
1623
- };
1624
- /**
1625
- * Output schema for list_projects tool
1626
- */
1627
- const listProjectsOutputSchema = {
1628
- projects: z.array(z.object({
1629
- id: z.string(),
1630
- name: z.string(),
1631
- description: z.string().nullable().optional(),
1632
- organization: z.object({
1633
- id: z.string(),
1634
- name: z.string(),
1635
- slug: z.string()
1636
- })
1637
- })),
1638
- total: z.number()
1639
- };
1640
- /**
1641
- * Execute list_projects tool
1642
- */
1643
- async function executeListProjects(client, input) {
1644
- const response = await client.listProjects({
1645
- organizationId: input.organizationId,
1646
- limit: input.limit
1647
- });
1648
- return {
1649
- projects: response.projects.map((p) => ({
1650
- id: p.id,
1651
- name: p.name,
1652
- description: p.description,
1653
- organization: p.organization
1654
- })),
1655
- total: response.pagination.total
1656
- };
1657
- }
1658
- /**
1659
- * Tool metadata
1660
- */
1661
- const listProjectsMetadata = {
1662
- name: "list_projects",
1663
- title: "List Projects",
1664
- description: `List all projects you have access to.
1665
-
1666
- Returns a list of projects with their IDs, names, and organization info.
1667
- Use this to find project IDs for other tools like get_project_health.
1668
-
1669
- Requires a user API Key (gaf_). Get one from Account Settings in the Gaffer dashboard.`
1670
- };
1671
-
1672
1614
  //#endregion
1673
1615
  //#region src/tools/list-test-runs.ts
1674
1616
  /**
@@ -1756,6 +1698,508 @@ Use cases:
1756
1698
  - "What's the status of tests on my feature branch?"`
1757
1699
  };
1758
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
+
1759
2203
  //#endregion
1760
2204
  //#region src/index.ts
1761
2205
  /**
@@ -1774,48 +2218,29 @@ function logError(toolName, error) {
1774
2218
  */
1775
2219
  function handleToolError(toolName, error) {
1776
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`;
1777
2227
  return {
1778
2228
  content: [{
1779
2229
  type: "text",
1780
- text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
2230
+ text
1781
2231
  }],
1782
2232
  isError: true
1783
2233
  };
1784
2234
  }
1785
2235
  /**
1786
- * Register a tool with the MCP server using a consistent pattern.
1787
- * Reduces boilerplate by handling error wrapping and response formatting.
1788
- */
1789
- function registerTool(server, client, tool) {
1790
- server.registerTool(tool.metadata.name, {
1791
- title: tool.metadata.title,
1792
- description: tool.metadata.description,
1793
- inputSchema: tool.inputSchema,
1794
- outputSchema: tool.outputSchema
1795
- }, async (input) => {
1796
- try {
1797
- const output = await tool.execute(client, input);
1798
- return {
1799
- content: [{
1800
- type: "text",
1801
- text: JSON.stringify(output, null, 2)
1802
- }],
1803
- structuredContent: output
1804
- };
1805
- } catch (error) {
1806
- return handleToolError(tool.metadata.name, error);
1807
- }
1808
- });
1809
- }
1810
- /**
1811
- * Gaffer MCP Server
2236
+ * Gaffer MCP Server Code Mode
1812
2237
  *
1813
- * 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)
1814
2242
  *
1815
- * Supports two authentication modes:
1816
- * 1. User API Keys (gaf_) - Read-only access to all user's projects
1817
- * Set via GAFFER_API_KEY environment variable
1818
- * 2. Project Upload Tokens (gfr_) - Legacy, single project access
2243
+ * This follows Cloudflare's "code mode" pattern for MCP servers.
1819
2244
  */
1820
2245
  async function main() {
1821
2246
  if (!process.env.GAFFER_API_KEY) {
@@ -1832,169 +2257,153 @@ async function main() {
1832
2257
  process.exit(1);
1833
2258
  }
1834
2259
  const client = GafferApiClient.fromEnv();
2260
+ const registry = new FunctionRegistry();
2261
+ registerAllTools(registry);
2262
+ const namespace = registry.buildNamespace(client);
2263
+ const declarations = registry.generateAllDeclarations();
1835
2264
  const server = new McpServer({
1836
2265
  name: "gaffer",
1837
- version: "0.1.0"
1838
- }, { 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.
1839
2268
 
1840
2269
  ## Authentication
1841
2270
 
1842
- ${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 is resolved automatically. Note: some tools (coverage, failure clusters, slowest tests, etc.) require a user API key and are not available."}
1843
-
1844
- ## Coverage Analysis Best Practices
1845
-
1846
- When helping users improve test coverage, combine coverage data with codebase exploration:
1847
-
1848
- 1. **Understand code utilization first**: Before targeting files by coverage percentage, explore which code is critical:
1849
- - Find entry points (route definitions, event handlers, exported functions)
1850
- - Find heavily-imported files (files imported by many others are high-value targets)
1851
- - Identify critical business logic (auth, payments, data mutations)
1852
-
1853
- 2. **Prioritize by impact**: Low coverage alone doesn't indicate priority. Consider:
1854
- - High utilization + low coverage = highest priority
1855
- - Large files with 0% coverage have bigger impact than small files
1856
- - Use find_uncovered_failure_areas for files with both low coverage AND test failures
1857
-
1858
- 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.
1859
-
1860
- 4. **Iterate**: Get baseline → identify targets → write tests → re-check coverage after CI uploads new results.
1861
-
1862
- ## Finding Invisible Files
1863
-
1864
- 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.
1865
-
1866
- To find invisible files:
1867
- 1. Use get_coverage_for_file with a path prefix (e.g., "server/") to see what Gaffer tracks
1868
- 2. Use the local Glob tool to list all source files in that path
1869
- 3. Compare the lists - files in local but NOT in Gaffer are invisible
1870
- 4. These files need tests that actually import them
1871
-
1872
- 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.
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."}
1873
2272
 
1874
- ## Agentic CI / Test Failure Diagnosis
2273
+ ## How to Use
1875
2274
 
1876
- When helping diagnose CI failures or fix failing tests:
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
1877
2278
 
1878
- 1. **Check flakiness first**: Use get_flaky_tests to identify non-deterministic tests.
1879
- Skip flaky tests unless the user specifically wants to stabilize them.
2279
+ ## Example
1880
2280
 
1881
- 2. **Get failure details**: Use get_test_run_details with status='failed'
1882
- to see error messages and stack traces for failing tests.
1883
-
1884
- 3. **Group by root cause**: Use get_failure_clusters to see which failures
1885
- share the same underlying error fix the root cause, not individual tests.
1886
-
1887
- 4. **Check history**: Use get_test_history to understand if the failure is new
1888
- (regression) or recurring (existing bug).
1889
-
1890
- 5. **Verify fixes**: After code changes, use compare_test_metrics to confirm
1891
- the specific test now passes.
1892
-
1893
- 6. **Prioritize by risk**: Use find_uncovered_failure_areas to identify
1894
- which failing code has the lowest test coverage fix those first.
1895
-
1896
- ## 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.
1897
2370
 
1898
- 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.
1899
2372
 
1900
- 1. Use get_upload_status with commitSha or branch to find upload sessions
1901
- 2. Check processingStatus: "completed" means results are ready, "processing" means wait
1902
- 3. Once completed, use the linked testRunIds to get test results` });
1903
- registerTool(server, client, {
1904
- metadata: getProjectHealthMetadata,
1905
- inputSchema: getProjectHealthInputSchema,
1906
- outputSchema: getProjectHealthOutputSchema,
1907
- execute: executeGetProjectHealth
1908
- });
1909
- registerTool(server, client, {
1910
- metadata: getTestHistoryMetadata,
1911
- inputSchema: getTestHistoryInputSchema,
1912
- outputSchema: getTestHistoryOutputSchema,
1913
- execute: executeGetTestHistory
1914
- });
1915
- registerTool(server, client, {
1916
- metadata: getFlakyTestsMetadata,
1917
- inputSchema: getFlakyTestsInputSchema,
1918
- outputSchema: getFlakyTestsOutputSchema,
1919
- execute: executeGetFlakyTests
1920
- });
1921
- registerTool(server, client, {
1922
- metadata: listTestRunsMetadata,
1923
- inputSchema: listTestRunsInputSchema,
1924
- outputSchema: listTestRunsOutputSchema,
1925
- 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
+ }
1926
2388
  });
1927
- if (client.isUserToken()) registerTool(server, client, {
1928
- metadata: listProjectsMetadata,
2389
+ if (client.isUserToken()) server.registerTool(listProjectsMetadata.name, {
2390
+ title: listProjectsMetadata.title,
2391
+ description: listProjectsMetadata.description,
1929
2392
  inputSchema: listProjectsInputSchema,
1930
- outputSchema: listProjectsOutputSchema,
1931
- execute: executeListProjects
1932
- });
1933
- registerTool(server, client, {
1934
- metadata: getReportMetadata,
1935
- inputSchema: getReportInputSchema,
1936
- outputSchema: getReportOutputSchema,
1937
- execute: executeGetReport
1938
- });
1939
- registerTool(server, client, {
1940
- metadata: getSlowestTestsMetadata,
1941
- inputSchema: getSlowestTestsInputSchema,
1942
- outputSchema: getSlowestTestsOutputSchema,
1943
- execute: executeGetSlowestTests
1944
- });
1945
- registerTool(server, client, {
1946
- metadata: getTestRunDetailsMetadata,
1947
- inputSchema: getTestRunDetailsInputSchema,
1948
- outputSchema: getTestRunDetailsOutputSchema,
1949
- execute: executeGetTestRunDetails
1950
- });
1951
- registerTool(server, client, {
1952
- metadata: getFailureClustersMetadata,
1953
- inputSchema: getFailureClustersInputSchema,
1954
- outputSchema: getFailureClustersOutputSchema,
1955
- execute: executeGetFailureClusters
1956
- });
1957
- registerTool(server, client, {
1958
- metadata: compareTestMetricsMetadata,
1959
- inputSchema: compareTestMetricsInputSchema,
1960
- outputSchema: compareTestMetricsOutputSchema,
1961
- execute: executeCompareTestMetrics
1962
- });
1963
- registerTool(server, client, {
1964
- metadata: getCoverageSummaryMetadata,
1965
- inputSchema: getCoverageSummaryInputSchema,
1966
- outputSchema: getCoverageSummaryOutputSchema,
1967
- execute: executeGetCoverageSummary
1968
- });
1969
- registerTool(server, client, {
1970
- metadata: getCoverageForFileMetadata,
1971
- inputSchema: getCoverageForFileInputSchema,
1972
- outputSchema: getCoverageForFileOutputSchema,
1973
- execute: executeGetCoverageForFile
1974
- });
1975
- registerTool(server, client, {
1976
- metadata: findUncoveredFailureAreasMetadata,
1977
- inputSchema: findUncoveredFailureAreasInputSchema,
1978
- outputSchema: findUncoveredFailureAreasOutputSchema,
1979
- execute: executeFindUncoveredFailureAreas
1980
- });
1981
- registerTool(server, client, {
1982
- metadata: getUntestedFilesMetadata,
1983
- inputSchema: getUntestedFilesInputSchema,
1984
- outputSchema: getUntestedFilesOutputSchema,
1985
- execute: executeGetUntestedFiles
1986
- });
1987
- registerTool(server, client, {
1988
- metadata: getReportBrowserUrlMetadata,
1989
- inputSchema: getReportBrowserUrlInputSchema,
1990
- outputSchema: getReportBrowserUrlOutputSchema,
1991
- execute: executeGetReportBrowserUrl
1992
- });
1993
- registerTool(server, client, {
1994
- metadata: getUploadStatusMetadata,
1995
- inputSchema: getUploadStatusInputSchema,
1996
- outputSchema: getUploadStatusOutputSchema,
1997
- 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
+ }
1998
2407
  });
1999
2408
  const transport = new StdioServerTransport();
2000
2409
  await server.connect(transport);