@gaffer-sh/mcp 0.6.1 → 0.6.2
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 +61 -182
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ function sleep(ms) {
|
|
|
26
26
|
/**
|
|
27
27
|
* Detect token type from prefix
|
|
28
28
|
* - gaf_ = user API Key (read-only, cross-project)
|
|
29
|
-
* - gfr_ = Project
|
|
29
|
+
* - gfr_ = Project Token (single project)
|
|
30
30
|
*/
|
|
31
31
|
function detectTokenType(token) {
|
|
32
32
|
if (token.startsWith("gaf_")) return "user";
|
|
@@ -37,12 +37,16 @@ function detectTokenType(token) {
|
|
|
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
|
|
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
|
+
resolvedProjectId = 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,18 @@ 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.
|
|
78
|
+
* For user tokens, requires explicit projectId.
|
|
79
|
+
*/
|
|
80
|
+
async resolveProjectId(projectId) {
|
|
81
|
+
if (projectId) return projectId;
|
|
82
|
+
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;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
72
88
|
* Make authenticated request to Gaffer API with retry logic
|
|
73
89
|
*/
|
|
74
90
|
async request(endpoint, params) {
|
|
@@ -129,13 +145,8 @@ var GafferApiClient = class GafferApiClient {
|
|
|
129
145
|
throw lastError || /* @__PURE__ */ new Error("Request failed after retries");
|
|
130
146
|
}
|
|
131
147
|
/**
|
|
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
|
|
148
|
+
* List all projects the user has access to.
|
|
149
|
+
* Requires user API Key (gaf_). Not available with project tokens.
|
|
139
150
|
*/
|
|
140
151
|
async listProjects(options = {}) {
|
|
141
152
|
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 +158,20 @@ var GafferApiClient = class GafferApiClient {
|
|
|
147
158
|
}
|
|
148
159
|
/**
|
|
149
160
|
* 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
161
|
*/
|
|
155
162
|
async getProjectHealth(options = {}) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return this.request(`/user/projects/${options.projectId}/health`, { days: options.days || 30 });
|
|
159
|
-
}
|
|
160
|
-
return this.request("/project/analytics", { days: options.days || 30 });
|
|
163
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
164
|
+
return this.request(`/user/projects/${projectId}/health`, { days: options.days || 30 });
|
|
161
165
|
}
|
|
162
166
|
/**
|
|
163
167
|
* 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
168
|
*/
|
|
171
169
|
async getTestHistory(options) {
|
|
172
170
|
const testName = options.testName?.trim();
|
|
173
171
|
const filePath = options.filePath?.trim();
|
|
174
172
|
if (!testName && !filePath) throw new Error("Either testName or filePath is required (and must not be empty)");
|
|
175
|
-
|
|
176
|
-
|
|
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", {
|
|
173
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
174
|
+
return this.request(`/user/projects/${projectId}/test-history`, {
|
|
184
175
|
...testName && { testName },
|
|
185
176
|
...filePath && { filePath },
|
|
186
177
|
...options.limit && { limit: options.limit }
|
|
@@ -188,23 +179,10 @@ var GafferApiClient = class GafferApiClient {
|
|
|
188
179
|
}
|
|
189
180
|
/**
|
|
190
181
|
* 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
182
|
*/
|
|
198
183
|
async getFlakyTests(options = {}) {
|
|
199
|
-
|
|
200
|
-
|
|
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", {
|
|
184
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
185
|
+
return this.request(`/user/projects/${projectId}/flaky-tests`, {
|
|
208
186
|
...options.threshold && { threshold: options.threshold },
|
|
209
187
|
...options.limit && { limit: options.limit },
|
|
210
188
|
...options.days && { days: options.days }
|
|
@@ -212,25 +190,10 @@ var GafferApiClient = class GafferApiClient {
|
|
|
212
190
|
}
|
|
213
191
|
/**
|
|
214
192
|
* 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
193
|
*/
|
|
223
194
|
async getTestRuns(options = {}) {
|
|
224
|
-
|
|
225
|
-
|
|
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", {
|
|
195
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
196
|
+
return this.request(`/user/projects/${projectId}/test-runs`, {
|
|
234
197
|
...options.commitSha && { commitSha: options.commitSha },
|
|
235
198
|
...options.branch && { branch: options.branch },
|
|
236
199
|
...options.status && { status: options.status },
|
|
@@ -239,30 +202,18 @@ var GafferApiClient = class GafferApiClient {
|
|
|
239
202
|
}
|
|
240
203
|
/**
|
|
241
204
|
* 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
|
|
245
205
|
*/
|
|
246
206
|
async getReport(testRunId) {
|
|
247
|
-
if (!this.isUserToken()) throw new Error("getReport requires a user API Key (gaf_).
|
|
207
|
+
if (!this.isUserToken()) throw new Error("getReport requires a user API Key (gaf_). Project tokens (gfr_) cannot access reports via API.");
|
|
248
208
|
if (!testRunId) throw new Error("testRunId is required");
|
|
249
209
|
return this.request(`/user/test-runs/${testRunId}/report`);
|
|
250
210
|
}
|
|
251
211
|
/**
|
|
252
212
|
* 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
213
|
*/
|
|
262
214
|
async getSlowestTests(options) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return this.request(`/user/projects/${options.projectId}/slowest-tests`, {
|
|
215
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
216
|
+
return this.request(`/user/projects/${projectId}/slowest-tests`, {
|
|
266
217
|
...options.days && { days: options.days },
|
|
267
218
|
...options.limit && { limit: options.limit },
|
|
268
219
|
...options.framework && { framework: options.framework },
|
|
@@ -271,20 +222,11 @@ var GafferApiClient = class GafferApiClient {
|
|
|
271
222
|
}
|
|
272
223
|
/**
|
|
273
224
|
* 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
225
|
*/
|
|
283
226
|
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
227
|
if (!options.testRunId) throw new Error("testRunId is required");
|
|
287
|
-
|
|
228
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
229
|
+
return this.request(`/user/projects/${projectId}/test-runs/${options.testRunId}/details`, {
|
|
288
230
|
...options.status && { status: options.status },
|
|
289
231
|
...options.limit && { limit: options.limit },
|
|
290
232
|
...options.offset && { offset: options.offset }
|
|
@@ -292,21 +234,11 @@ var GafferApiClient = class GafferApiClient {
|
|
|
292
234
|
}
|
|
293
235
|
/**
|
|
294
236
|
* 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
237
|
*/
|
|
305
238
|
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
239
|
if (!options.testName) throw new Error("testName is required");
|
|
309
|
-
|
|
240
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
241
|
+
return this.request(`/user/projects/${projectId}/compare-test`, {
|
|
310
242
|
testName: options.testName,
|
|
311
243
|
...options.beforeCommit && { beforeCommit: options.beforeCommit },
|
|
312
244
|
...options.afterCommit && { afterCommit: options.afterCommit },
|
|
@@ -316,35 +248,17 @@ var GafferApiClient = class GafferApiClient {
|
|
|
316
248
|
}
|
|
317
249
|
/**
|
|
318
250
|
* 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
251
|
*/
|
|
325
252
|
async getCoverageSummary(options) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return this.request(`/user/projects/${options.projectId}/coverage-summary`, { ...options.days && { days: options.days } });
|
|
253
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
254
|
+
return this.request(`/user/projects/${projectId}/coverage-summary`, { ...options.days && { days: options.days } });
|
|
329
255
|
}
|
|
330
256
|
/**
|
|
331
257
|
* 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
258
|
*/
|
|
344
259
|
async getCoverageFiles(options) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return this.request(`/user/projects/${options.projectId}/coverage/files`, {
|
|
260
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
261
|
+
return this.request(`/user/projects/${projectId}/coverage/files`, {
|
|
348
262
|
...options.filePath && { filePath: options.filePath },
|
|
349
263
|
...options.minCoverage !== void 0 && { minCoverage: options.minCoverage },
|
|
350
264
|
...options.maxCoverage !== void 0 && { maxCoverage: options.maxCoverage },
|
|
@@ -356,65 +270,36 @@ var GafferApiClient = class GafferApiClient {
|
|
|
356
270
|
}
|
|
357
271
|
/**
|
|
358
272
|
* 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
273
|
*/
|
|
366
274
|
async getCoverageRiskAreas(options) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return this.request(`/user/projects/${options.projectId}/coverage/risk-areas`, {
|
|
275
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
276
|
+
return this.request(`/user/projects/${projectId}/coverage/risk-areas`, {
|
|
370
277
|
...options.days && { days: options.days },
|
|
371
278
|
...options.coverageThreshold !== void 0 && { coverageThreshold: options.coverageThreshold }
|
|
372
279
|
});
|
|
373
280
|
}
|
|
374
281
|
/**
|
|
375
282
|
* 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
283
|
*/
|
|
383
284
|
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
285
|
if (!options.testRunId) throw new Error("testRunId is required");
|
|
387
|
-
|
|
286
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
287
|
+
return this.request(`/user/projects/${projectId}/reports/${options.testRunId}/browser-url`, { ...options.filename && { filename: options.filename } });
|
|
388
288
|
}
|
|
389
289
|
/**
|
|
390
290
|
* 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
291
|
*/
|
|
397
292
|
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
293
|
if (!options.testRunId) throw new Error("testRunId is required");
|
|
401
|
-
|
|
294
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
295
|
+
return this.request(`/user/projects/${projectId}/test-runs/${options.testRunId}/failure-clusters`);
|
|
402
296
|
}
|
|
403
297
|
/**
|
|
404
298
|
* 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
299
|
*/
|
|
414
300
|
async listUploadSessions(options) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return this.request(`/user/projects/${options.projectId}/upload-sessions`, {
|
|
301
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
302
|
+
return this.request(`/user/projects/${projectId}/upload-sessions`, {
|
|
418
303
|
...options.commitSha && { commitSha: options.commitSha },
|
|
419
304
|
...options.branch && { branch: options.branch },
|
|
420
305
|
...options.limit && { limit: options.limit },
|
|
@@ -423,17 +308,11 @@ var GafferApiClient = class GafferApiClient {
|
|
|
423
308
|
}
|
|
424
309
|
/**
|
|
425
310
|
* 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
311
|
*/
|
|
432
312
|
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
313
|
if (!options.sessionId) throw new Error("sessionId is required");
|
|
436
|
-
|
|
314
|
+
const projectId = await this.resolveProjectId(options.projectId);
|
|
315
|
+
return this.request(`/user/projects/${projectId}/upload-sessions/${options.sessionId}`);
|
|
437
316
|
}
|
|
438
317
|
};
|
|
439
318
|
|
|
@@ -443,7 +322,7 @@ var GafferApiClient = class GafferApiClient {
|
|
|
443
322
|
* Input schema for compare_test_metrics tool
|
|
444
323
|
*/
|
|
445
324
|
const compareTestMetricsInputSchema = {
|
|
446
|
-
projectId: z.string().describe("Project ID.
|
|
325
|
+
projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
|
|
447
326
|
testName: z.string().describe("The test name to compare. Can be the short name or full name including describe blocks."),
|
|
448
327
|
beforeCommit: z.string().optional().describe("Commit SHA for the \"before\" measurement. Use with afterCommit."),
|
|
449
328
|
afterCommit: z.string().optional().describe("Commit SHA for the \"after\" measurement. Use with beforeCommit."),
|
|
@@ -553,7 +432,7 @@ Tip: Use get_test_history first to find the commit SHAs or test run IDs you want
|
|
|
553
432
|
* Input schema for find_uncovered_failure_areas tool
|
|
554
433
|
*/
|
|
555
434
|
const findUncoveredFailureAreasInputSchema = {
|
|
556
|
-
projectId: z.string().describe("Project ID.
|
|
435
|
+
projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
|
|
557
436
|
days: z.number().int().min(1).max(365).optional().describe("Number of days to analyze for test failures (default: 30)"),
|
|
558
437
|
coverageThreshold: z.number().min(0).max(100).optional().describe("Include files with coverage below this percentage (default: 80)")
|
|
559
438
|
};
|
|
@@ -618,7 +497,7 @@ Use this to prioritize which parts of your codebase need better test coverage.`
|
|
|
618
497
|
* Input schema for get_coverage_for_file tool
|
|
619
498
|
*/
|
|
620
499
|
const getCoverageForFileInputSchema = {
|
|
621
|
-
projectId: z.string().describe("Project ID.
|
|
500
|
+
projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
|
|
622
501
|
filePath: z.string().describe("File path to get coverage for. Can be exact path or partial match.")
|
|
623
502
|
};
|
|
624
503
|
/**
|
|
@@ -701,7 +580,7 @@ Prioritize: high utilization + low coverage = highest impact.`
|
|
|
701
580
|
* Input schema for get_coverage_summary tool
|
|
702
581
|
*/
|
|
703
582
|
const getCoverageSummaryInputSchema = {
|
|
704
|
-
projectId: z.string().describe("Project ID.
|
|
583
|
+
projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
|
|
705
584
|
days: z.number().int().min(1).max(365).optional().describe("Number of days to analyze for trends (default: 30)")
|
|
706
585
|
};
|
|
707
586
|
/**
|
|
@@ -776,7 +655,7 @@ high-value targets in critical code paths rather than just the files with lowest
|
|
|
776
655
|
* Input schema for get_failure_clusters tool
|
|
777
656
|
*/
|
|
778
657
|
const getFailureClustersInputSchema = {
|
|
779
|
-
projectId: z.string().describe("Project ID.
|
|
658
|
+
projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
|
|
780
659
|
testRunId: z.string().describe("Test run ID. Use list_test_runs to find test run IDs.")
|
|
781
660
|
};
|
|
782
661
|
/**
|
|
@@ -979,7 +858,7 @@ Use this to understand the current state of your test suite.`
|
|
|
979
858
|
* Input schema for get_report_browser_url tool
|
|
980
859
|
*/
|
|
981
860
|
const getReportBrowserUrlInputSchema = {
|
|
982
|
-
projectId: z.string().describe("Project ID.
|
|
861
|
+
projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
|
|
983
862
|
testRunId: z.string().describe("The test run ID to get the report URL for. Use list_test_runs to find test run IDs."),
|
|
984
863
|
filename: z.string().optional().describe("Specific file to open (default: index.html or first HTML file)")
|
|
985
864
|
};
|
|
@@ -1125,7 +1004,7 @@ Use cases:
|
|
|
1125
1004
|
* Input schema for get_slowest_tests tool
|
|
1126
1005
|
*/
|
|
1127
1006
|
const getSlowestTestsInputSchema = {
|
|
1128
|
-
projectId: z.string().describe("Project ID.
|
|
1007
|
+
projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
|
|
1129
1008
|
days: z.number().int().min(1).max(365).optional().describe("Analysis period in days (default: 30)"),
|
|
1130
1009
|
limit: z.number().int().min(1).max(100).optional().describe("Maximum number of tests to return (default: 20)"),
|
|
1131
1010
|
framework: z.string().optional().describe("Filter by test framework (e.g., \"playwright\", \"vitest\", \"jest\")"),
|
|
@@ -1302,7 +1181,7 @@ Use this to investigate flaky tests or understand test stability.`
|
|
|
1302
1181
|
*/
|
|
1303
1182
|
const getTestRunDetailsInputSchema = {
|
|
1304
1183
|
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.
|
|
1184
|
+
projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
|
|
1306
1185
|
status: z.enum([
|
|
1307
1186
|
"passed",
|
|
1308
1187
|
"failed",
|
|
@@ -1416,7 +1295,7 @@ use get_test_history, get_flaky_tests, or get_slowest_tests instead.`
|
|
|
1416
1295
|
* Input schema for get_untested_files tool
|
|
1417
1296
|
*/
|
|
1418
1297
|
const getUntestedFilesInputSchema = {
|
|
1419
|
-
projectId: z.string().describe("Project ID.
|
|
1298
|
+
projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
|
|
1420
1299
|
maxCoverage: z.number().min(0).max(100).optional().describe("Maximum coverage percentage to include (default: 10 for \"untested\")"),
|
|
1421
1300
|
limit: z.number().int().min(1).max(100).optional().describe("Maximum number of files to return (default: 20)")
|
|
1422
1301
|
};
|
|
@@ -1507,7 +1386,7 @@ for those specific paths.`
|
|
|
1507
1386
|
* Input schema for get_upload_status tool
|
|
1508
1387
|
*/
|
|
1509
1388
|
const getUploadStatusInputSchema = {
|
|
1510
|
-
projectId: z.string().describe("Project ID.
|
|
1389
|
+
projectId: z.string().optional().describe("Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically."),
|
|
1511
1390
|
sessionId: z.string().optional().describe("Specific upload session ID. If provided, returns detailed status for that session. Otherwise, lists recent sessions."),
|
|
1512
1391
|
commitSha: z.string().optional().describe("Filter sessions by commit SHA. Useful for checking if results for a specific commit are ready."),
|
|
1513
1392
|
branch: z.string().optional().describe("Filter sessions by branch name.")
|
|
@@ -1839,7 +1718,7 @@ async function main() {
|
|
|
1839
1718
|
|
|
1840
1719
|
## Authentication
|
|
1841
1720
|
|
|
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
|
|
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."}
|
|
1843
1722
|
|
|
1844
1723
|
## Coverage Analysis Best Practices
|
|
1845
1724
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/api-client.ts","../src/tools/compare-test-metrics.ts","../src/tools/find-uncovered-failure-areas.ts","../src/tools/get-coverage-for-file.ts","../src/tools/get-coverage-summary.ts","../src/tools/get-failure-clusters.ts","../src/tools/get-flaky-tests.ts","../src/tools/get-project-health.ts","../src/tools/get-report-browser-url.ts","../src/tools/get-report.ts","../src/tools/get-slowest-tests.ts","../src/tools/get-test-history.ts","../src/tools/get-test-run-details.ts","../src/tools/get-untested-files.ts","../src/tools/get-upload-status.ts","../src/tools/list-projects.ts","../src/tools/list-test-runs.ts","../src/index.ts"],"sourcesContent":["import type {\n AnalyticsResponse,\n ApiErrorResponse,\n BrowserUrlResponse,\n CompareTestResponse,\n CoverageFilesResponse,\n CoverageRiskAreasResponse,\n CoverageSummaryResponse,\n FailureClustersResponse,\n FlakyTestsResponse,\n GafferConfig,\n ProjectsResponse,\n ReportResponse,\n SlowestTestsResponse,\n TestHistoryResponse,\n TestRunDetailsResponse,\n TestRunsResponse,\n UploadSessionDetailResponse,\n UploadSessionsResponse,\n} from './types.js'\nimport { createRequire } from 'node:module'\n\nconst require = createRequire(import.meta.url)\nconst pkg = require('../package.json') as { version: string }\n\n// Request timeout in milliseconds (30 seconds)\nconst REQUEST_TIMEOUT_MS = 30000\n\n// Retry configuration\nconst MAX_RETRIES = 3\nconst INITIAL_RETRY_DELAY_MS = 1000\nconst RETRYABLE_STATUS_CODES = [401, 429, 500, 502, 503, 504]\n\n/**\n * Sleep for a given number of milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\n/**\n * Token type detection based on prefix\n */\nexport type TokenType = 'user' | 'project'\n\n/**\n * Detect token type from prefix\n * - gaf_ = user API Key (read-only, cross-project)\n * - gfr_ = Project Upload Token (legacy, single project)\n */\nexport function detectTokenType(token: string): TokenType {\n if (token.startsWith('gaf_')) {\n return 'user'\n }\n return 'project'\n}\n\n/**\n * Gaffer API v1 client for MCP server\n *\n * Supports two authentication modes:\n * 1. User API Keys (gaf_) - Read-only access to all user's projects\n * 2. Project Upload Tokens (gfr_) - Legacy, single project access\n */\nexport class GafferApiClient {\n private apiKey: string\n private baseUrl: string\n public readonly tokenType: TokenType\n\n constructor(config: GafferConfig) {\n this.apiKey = config.apiKey\n this.baseUrl = config.baseUrl.replace(/\\/$/, '') // Remove trailing slash\n this.tokenType = detectTokenType(config.apiKey)\n }\n\n /**\n * Create client from environment variables\n *\n * Supports:\n * - GAFFER_API_KEY (for user API Keys gaf_)\n */\n static fromEnv(): GafferApiClient {\n const apiKey = process.env.GAFFER_API_KEY\n if (!apiKey) {\n throw new Error('GAFFER_API_KEY environment variable is required')\n }\n\n const baseUrl = process.env.GAFFER_API_URL || 'https://app.gaffer.sh'\n\n return new GafferApiClient({ apiKey, baseUrl })\n }\n\n /**\n * Check if using a user API Key (enables cross-project features)\n */\n isUserToken(): boolean {\n return this.tokenType === 'user'\n }\n\n /**\n * Make authenticated request to Gaffer API with retry logic\n */\n private async request<T>(\n endpoint: string,\n params?: Record<string, string | number>,\n ): Promise<T> {\n const url = new URL(`/api/v1${endpoint}`, this.baseUrl)\n\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value))\n }\n }\n }\n\n let lastError: Error | null = null\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n // Set up request timeout\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)\n\n try {\n const response = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n 'X-API-Key': this.apiKey,\n 'Accept': 'application/json',\n 'User-Agent': `gaffer-mcp/${pkg.version}`,\n },\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({})) as ApiErrorResponse\n\n // Check if we should retry this status code\n if (RETRYABLE_STATUS_CODES.includes(response.status) && attempt < MAX_RETRIES) {\n // For 429 (rate limit), use Retry-After header if available\n let delayMs = INITIAL_RETRY_DELAY_MS * (2 ** attempt)\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After')\n if (retryAfter) {\n delayMs = Math.max(delayMs, Number.parseInt(retryAfter, 10) * 1000)\n }\n }\n lastError = new Error(errorData.error?.message || `API request failed: ${response.status}`)\n await sleep(delayMs)\n continue\n }\n\n const errorMessage = errorData.error?.message || `API request failed: ${response.status}`\n throw new Error(errorMessage)\n }\n\n return response.json() as Promise<T>\n }\n catch (error) {\n clearTimeout(timeoutId)\n\n if (error instanceof Error && error.name === 'AbortError') {\n lastError = new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`)\n if (attempt < MAX_RETRIES) {\n await sleep(INITIAL_RETRY_DELAY_MS * (2 ** attempt))\n continue\n }\n throw lastError\n }\n\n // For network errors, retry\n if (error instanceof TypeError && attempt < MAX_RETRIES) {\n lastError = error\n await sleep(INITIAL_RETRY_DELAY_MS * (2 ** attempt))\n continue\n }\n\n throw error\n }\n finally {\n clearTimeout(timeoutId)\n }\n }\n\n // Should not reach here, but just in case\n throw lastError || new Error('Request failed after retries')\n }\n\n /**\n * List all projects the user has access to\n * Requires user API Key (gaf_)\n *\n * @param options - Query options\n * @param options.organizationId - Filter by organization ID\n * @param options.limit - Maximum number of results\n * @param options.offset - Offset for pagination\n */\n async listProjects(options: {\n organizationId?: string\n limit?: number\n offset?: number\n } = {}): Promise<ProjectsResponse> {\n if (!this.isUserToken()) {\n 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.')\n }\n\n return this.request<ProjectsResponse>('/user/projects', {\n ...(options.organizationId && { organizationId: options.organizationId }),\n ...(options.limit && { limit: options.limit }),\n ...(options.offset && { offset: options.offset }),\n })\n }\n\n /**\n * Get project health analytics\n *\n * @param options - Query options\n * @param options.projectId - Required for user tokens, ignored for project tokens\n * @param options.days - Analysis period in days (default: 30)\n */\n async getProjectHealth(options: {\n projectId?: string\n days?: number\n } = {}): Promise<AnalyticsResponse> {\n if (this.isUserToken()) {\n if (!options.projectId) {\n throw new Error('projectId is required when using a user API Key')\n }\n return this.request<AnalyticsResponse>(`/user/projects/${options.projectId}/health`, {\n days: options.days || 30,\n })\n }\n\n // Legacy: project token uses /project/analytics\n return this.request<AnalyticsResponse>('/project/analytics', {\n days: options.days || 30,\n })\n }\n\n /**\n * Get test history for a specific test\n *\n * @param options - Query options\n * @param options.projectId - Required for user tokens, ignored for project tokens\n * @param options.testName - Test name to search for\n * @param options.filePath - File path to search for\n * @param options.limit - Maximum number of results\n */\n async getTestHistory(options: {\n projectId?: string\n testName?: string\n filePath?: string\n limit?: number\n }): Promise<TestHistoryResponse> {\n const testName = options.testName?.trim()\n const filePath = options.filePath?.trim()\n\n if (!testName && !filePath) {\n throw new Error('Either testName or filePath is required (and must not be empty)')\n }\n\n if (this.isUserToken()) {\n if (!options.projectId) {\n throw new Error('projectId is required when using a user API Key')\n }\n return this.request<TestHistoryResponse>(`/user/projects/${options.projectId}/test-history`, {\n ...(testName && { testName }),\n ...(filePath && { filePath }),\n ...(options.limit && { limit: options.limit }),\n })\n }\n\n // Legacy: project token uses /project/test-history\n return this.request<TestHistoryResponse>('/project/test-history', {\n ...(testName && { testName }),\n ...(filePath && { filePath }),\n ...(options.limit && { limit: options.limit }),\n })\n }\n\n /**\n * Get flaky tests for the project\n *\n * @param options - Query options\n * @param options.projectId - Required for user tokens, ignored for project tokens\n * @param options.threshold - Minimum flip rate to be considered flaky (0-1)\n * @param options.limit - Maximum number of results\n * @param options.days - Analysis period in days\n */\n async getFlakyTests(options: {\n projectId?: string\n threshold?: number\n limit?: number\n days?: number\n } = {}): Promise<FlakyTestsResponse> {\n if (this.isUserToken()) {\n if (!options.projectId) {\n throw new Error('projectId is required when using a user API Key')\n }\n return this.request<FlakyTestsResponse>(`/user/projects/${options.projectId}/flaky-tests`, {\n ...(options.threshold && { threshold: options.threshold }),\n ...(options.limit && { limit: options.limit }),\n ...(options.days && { days: options.days }),\n })\n }\n\n // Legacy: project token uses /project/flaky-tests\n return this.request<FlakyTestsResponse>('/project/flaky-tests', {\n ...(options.threshold && { threshold: options.threshold }),\n ...(options.limit && { limit: options.limit }),\n ...(options.days && { days: options.days }),\n })\n }\n\n /**\n * List test runs for the project\n *\n * @param options - Query options\n * @param options.projectId - Required for user tokens, ignored for project tokens\n * @param options.commitSha - Filter by commit SHA\n * @param options.branch - Filter by branch name\n * @param options.status - Filter by status ('passed' or 'failed')\n * @param options.limit - Maximum number of results\n */\n async getTestRuns(options: {\n projectId?: string\n commitSha?: string\n branch?: string\n status?: 'passed' | 'failed'\n limit?: number\n } = {}): Promise<TestRunsResponse> {\n if (this.isUserToken()) {\n if (!options.projectId) {\n throw new Error('projectId is required when using a user API Key')\n }\n return this.request<TestRunsResponse>(`/user/projects/${options.projectId}/test-runs`, {\n ...(options.commitSha && { commitSha: options.commitSha }),\n ...(options.branch && { branch: options.branch }),\n ...(options.status && { status: options.status }),\n ...(options.limit && { limit: options.limit }),\n })\n }\n\n // Legacy: project token uses /project/test-runs\n return this.request<TestRunsResponse>('/project/test-runs', {\n ...(options.commitSha && { commitSha: options.commitSha }),\n ...(options.branch && { branch: options.branch }),\n ...(options.status && { status: options.status }),\n ...(options.limit && { limit: options.limit }),\n })\n }\n\n /**\n * Get report files for a test run\n *\n * @param testRunId - The test run ID\n * @returns Report metadata with download URLs for each file\n */\n async getReport(testRunId: string): Promise<ReportResponse> {\n if (!this.isUserToken()) {\n throw new Error('getReport requires a user API Key (gaf_). Upload Tokens (gfr_) cannot access reports via API.')\n }\n\n if (!testRunId) {\n throw new Error('testRunId is required')\n }\n\n return this.request<ReportResponse>(`/user/test-runs/${testRunId}/report`)\n }\n\n /**\n * Get slowest tests for a project\n *\n * @param options - Query options\n * @param options.projectId - The project ID (required)\n * @param options.days - Analysis period in days (default: 30)\n * @param options.limit - Maximum number of results (default: 20)\n * @param options.framework - Filter by test framework\n * @param options.branch - Filter by git branch name\n * @returns Slowest tests sorted by P95 duration\n */\n async getSlowestTests(options: {\n projectId: string\n days?: number\n limit?: number\n framework?: string\n branch?: string\n }): Promise<SlowestTestsResponse> {\n if (!this.isUserToken()) {\n throw new Error('getSlowestTests requires a user API Key (gaf_).')\n }\n\n if (!options.projectId) {\n throw new Error('projectId is required')\n }\n\n return this.request<SlowestTestsResponse>(`/user/projects/${options.projectId}/slowest-tests`, {\n ...(options.days && { days: options.days }),\n ...(options.limit && { limit: options.limit }),\n ...(options.framework && { framework: options.framework }),\n ...(options.branch && { branch: options.branch }),\n })\n }\n\n /**\n * Get parsed test results for a specific test run\n *\n * @param options - Query options\n * @param options.projectId - The project ID (required)\n * @param options.testRunId - The test run ID (required)\n * @param options.status - Filter by test status ('passed', 'failed', 'skipped')\n * @param options.limit - Maximum number of results (default: 100)\n * @param options.offset - Pagination offset (default: 0)\n * @returns Parsed test cases with pagination\n */\n async getTestRunDetails(options: {\n projectId: string\n testRunId: string\n status?: 'passed' | 'failed' | 'skipped'\n limit?: number\n offset?: number\n }): Promise<TestRunDetailsResponse> {\n if (!this.isUserToken()) {\n throw new Error('getTestRunDetails requires a user API Key (gaf_).')\n }\n\n if (!options.projectId) {\n throw new Error('projectId is required')\n }\n\n if (!options.testRunId) {\n throw new Error('testRunId is required')\n }\n\n return this.request<TestRunDetailsResponse>(\n `/user/projects/${options.projectId}/test-runs/${options.testRunId}/details`,\n {\n ...(options.status && { status: options.status }),\n ...(options.limit && { limit: options.limit }),\n ...(options.offset && { offset: options.offset }),\n },\n )\n }\n\n /**\n * Compare test metrics between two commits or test runs\n *\n * @param options - Query options\n * @param options.projectId - The project ID (required)\n * @param options.testName - The test name to compare (required)\n * @param options.beforeCommit - Commit SHA for before (use with afterCommit)\n * @param options.afterCommit - Commit SHA for after (use with beforeCommit)\n * @param options.beforeRunId - Test run ID for before (use with afterRunId)\n * @param options.afterRunId - Test run ID for after (use with beforeRunId)\n * @returns Comparison of test metrics\n */\n async compareTestMetrics(options: {\n projectId: string\n testName: string\n beforeCommit?: string\n afterCommit?: string\n beforeRunId?: string\n afterRunId?: string\n }): Promise<CompareTestResponse> {\n if (!this.isUserToken()) {\n throw new Error('compareTestMetrics requires a user API Key (gaf_).')\n }\n\n if (!options.projectId) {\n throw new Error('projectId is required')\n }\n\n if (!options.testName) {\n throw new Error('testName is required')\n }\n\n return this.request<CompareTestResponse>(\n `/user/projects/${options.projectId}/compare-test`,\n {\n testName: options.testName,\n ...(options.beforeCommit && { beforeCommit: options.beforeCommit }),\n ...(options.afterCommit && { afterCommit: options.afterCommit }),\n ...(options.beforeRunId && { beforeRunId: options.beforeRunId }),\n ...(options.afterRunId && { afterRunId: options.afterRunId }),\n },\n )\n }\n\n /**\n * Get coverage summary for a project\n *\n * @param options - Query options\n * @param options.projectId - The project ID (required)\n * @param options.days - Analysis period in days (default: 30)\n * @returns Coverage summary with trends and lowest coverage files\n */\n async getCoverageSummary(options: {\n projectId: string\n days?: number\n }): Promise<CoverageSummaryResponse> {\n if (!this.isUserToken()) {\n throw new Error('getCoverageSummary requires a user API Key (gaf_).')\n }\n\n if (!options.projectId) {\n throw new Error('projectId is required')\n }\n\n return this.request<CoverageSummaryResponse>(\n `/user/projects/${options.projectId}/coverage-summary`,\n {\n ...(options.days && { days: options.days }),\n },\n )\n }\n\n /**\n * Get coverage files for a project with filtering\n *\n * @param options - Query options\n * @param options.projectId - The project ID (required)\n * @param options.filePath - Filter to specific file path\n * @param options.minCoverage - Minimum coverage percentage\n * @param options.maxCoverage - Maximum coverage percentage\n * @param options.limit - Maximum number of results\n * @param options.offset - Pagination offset\n * @param options.sortBy - Sort by 'path' or 'coverage'\n * @param options.sortOrder - Sort order 'asc' or 'desc'\n * @returns List of files with coverage data\n */\n async getCoverageFiles(options: {\n projectId: string\n filePath?: string\n minCoverage?: number\n maxCoverage?: number\n limit?: number\n offset?: number\n sortBy?: 'path' | 'coverage'\n sortOrder?: 'asc' | 'desc'\n }): Promise<CoverageFilesResponse> {\n if (!this.isUserToken()) {\n throw new Error('getCoverageFiles requires a user API Key (gaf_).')\n }\n\n if (!options.projectId) {\n throw new Error('projectId is required')\n }\n\n return this.request<CoverageFilesResponse>(\n `/user/projects/${options.projectId}/coverage/files`,\n {\n ...(options.filePath && { filePath: options.filePath }),\n ...(options.minCoverage !== undefined && { minCoverage: options.minCoverage }),\n ...(options.maxCoverage !== undefined && { maxCoverage: options.maxCoverage }),\n ...(options.limit && { limit: options.limit }),\n ...(options.offset && { offset: options.offset }),\n ...(options.sortBy && { sortBy: options.sortBy }),\n ...(options.sortOrder && { sortOrder: options.sortOrder }),\n },\n )\n }\n\n /**\n * Get risk areas (files with low coverage AND test failures)\n *\n * @param options - Query options\n * @param options.projectId - The project ID (required)\n * @param options.days - Analysis period in days (default: 30)\n * @param options.coverageThreshold - Include files below this coverage (default: 80)\n * @returns List of risk areas sorted by risk score\n */\n async getCoverageRiskAreas(options: {\n projectId: string\n days?: number\n coverageThreshold?: number\n }): Promise<CoverageRiskAreasResponse> {\n if (!this.isUserToken()) {\n throw new Error('getCoverageRiskAreas requires a user API Key (gaf_).')\n }\n\n if (!options.projectId) {\n throw new Error('projectId is required')\n }\n\n return this.request<CoverageRiskAreasResponse>(\n `/user/projects/${options.projectId}/coverage/risk-areas`,\n {\n ...(options.days && { days: options.days }),\n ...(options.coverageThreshold !== undefined && { coverageThreshold: options.coverageThreshold }),\n },\n )\n }\n\n /**\n * Get a browser-navigable URL for viewing a test report\n *\n * @param options - Query options\n * @param options.projectId - The project ID (required)\n * @param options.testRunId - The test run ID (required)\n * @param options.filename - Specific file to open (default: index.html)\n * @returns URL with signed token for browser access\n */\n async getReportBrowserUrl(options: {\n projectId: string\n testRunId: string\n filename?: string\n }): Promise<BrowserUrlResponse> {\n if (!this.isUserToken()) {\n throw new Error('getReportBrowserUrl requires a user API Key (gaf_).')\n }\n\n if (!options.projectId) {\n throw new Error('projectId is required')\n }\n\n if (!options.testRunId) {\n throw new Error('testRunId is required')\n }\n\n return this.request<BrowserUrlResponse>(\n `/user/projects/${options.projectId}/reports/${options.testRunId}/browser-url`,\n {\n ...(options.filename && { filename: options.filename }),\n },\n )\n }\n\n /**\n * Get failure clusters for a test run\n *\n * @param options - Query options\n * @param options.projectId - The project ID (required)\n * @param options.testRunId - The test run ID (required)\n * @returns Failure clusters grouped by error similarity\n */\n async getFailureClusters(options: {\n projectId: string\n testRunId: string\n }): Promise<FailureClustersResponse> {\n if (!this.isUserToken()) {\n throw new Error('getFailureClusters requires a user API Key (gaf_).')\n }\n\n if (!options.projectId) {\n throw new Error('projectId is required')\n }\n\n if (!options.testRunId) {\n throw new Error('testRunId is required')\n }\n\n return this.request<FailureClustersResponse>(\n `/user/projects/${options.projectId}/test-runs/${options.testRunId}/failure-clusters`,\n )\n }\n\n /**\n * List upload sessions for a project\n *\n * @param options - Query options\n * @param options.projectId - The project ID (required)\n * @param options.commitSha - Filter by commit SHA\n * @param options.branch - Filter by branch name\n * @param options.limit - Maximum number of results (default: 10)\n * @param options.offset - Pagination offset (default: 0)\n * @returns Paginated list of upload sessions\n */\n async listUploadSessions(options: {\n projectId: string\n commitSha?: string\n branch?: string\n limit?: number\n offset?: number\n }): Promise<UploadSessionsResponse> {\n if (!this.isUserToken()) {\n throw new Error('listUploadSessions requires a user API Key (gaf_).')\n }\n\n if (!options.projectId) {\n throw new Error('projectId is required')\n }\n\n return this.request<UploadSessionsResponse>(\n `/user/projects/${options.projectId}/upload-sessions`,\n {\n ...(options.commitSha && { commitSha: options.commitSha }),\n ...(options.branch && { branch: options.branch }),\n ...(options.limit && { limit: options.limit }),\n ...(options.offset && { offset: options.offset }),\n },\n )\n }\n\n /**\n * Get upload session detail with linked results\n *\n * @param options - Query options\n * @param options.projectId - The project ID (required)\n * @param options.sessionId - The upload session ID (required)\n * @returns Upload session details with linked test runs and coverage reports\n */\n async getUploadSessionDetail(options: {\n projectId: string\n sessionId: string\n }): Promise<UploadSessionDetailResponse> {\n if (!this.isUserToken()) {\n throw new Error('getUploadSessionDetail requires a user API Key (gaf_).')\n }\n\n if (!options.projectId) {\n throw new Error('projectId is required')\n }\n\n if (!options.sessionId) {\n throw new Error('sessionId is required')\n }\n\n return this.request<UploadSessionDetailResponse>(\n `/user/projects/${options.projectId}/upload-sessions/${options.sessionId}`,\n )\n }\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport type { CompareTestResponse } from '../types.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for compare_test_metrics tool\n */\nexport const compareTestMetricsInputSchema = {\n projectId: z\n .string()\n .describe('Project ID. Use list_projects to find project IDs.'),\n testName: z\n .string()\n .describe('The test name to compare. Can be the short name or full name including describe blocks.'),\n beforeCommit: z\n .string()\n .optional()\n .describe('Commit SHA for the \"before\" measurement. Use with afterCommit.'),\n afterCommit: z\n .string()\n .optional()\n .describe('Commit SHA for the \"after\" measurement. Use with beforeCommit.'),\n beforeRunId: z\n .string()\n .optional()\n .describe('Test run ID for the \"before\" measurement. Use with afterRunId.'),\n afterRunId: z\n .string()\n .optional()\n .describe('Test run ID for the \"after\" measurement. Use with beforeRunId.'),\n}\n\n/**\n * Output schema for compare_test_metrics tool\n */\nexport const compareTestMetricsOutputSchema = {\n testName: z.string(),\n before: z.object({\n testRunId: z.string(),\n commit: z.string().nullable(),\n branch: z.string().nullable(),\n status: z.enum(['passed', 'failed', 'skipped']),\n durationMs: z.number().nullable(),\n createdAt: z.string(),\n }),\n after: z.object({\n testRunId: z.string(),\n commit: z.string().nullable(),\n branch: z.string().nullable(),\n status: z.enum(['passed', 'failed', 'skipped']),\n durationMs: z.number().nullable(),\n createdAt: z.string(),\n }),\n change: z.object({\n durationMs: z.number().nullable(),\n percentChange: z.number().nullable(),\n statusChanged: z.boolean(),\n }),\n}\n\nexport interface CompareTestMetricsInput {\n projectId: string\n testName: string\n beforeCommit?: string\n afterCommit?: string\n beforeRunId?: string\n afterRunId?: string\n}\n\n// Re-export response type from types.ts for convenience\nexport type CompareTestMetricsOutput = CompareTestResponse\n\n/**\n * Execute compare_test_metrics tool\n */\nexport async function executeCompareTestMetrics(\n client: GafferApiClient,\n input: CompareTestMetricsInput,\n): Promise<CompareTestMetricsOutput> {\n // Validate input - check for presence of required pairs\n const hasCommits = input.beforeCommit && input.afterCommit\n const hasRunIds = input.beforeRunId && input.afterRunId\n\n if (!hasCommits && !hasRunIds) {\n throw new Error('Must provide either (beforeCommit + afterCommit) or (beforeRunId + afterRunId)')\n }\n\n // Validate non-empty strings\n if (hasCommits) {\n if (input.beforeCommit!.trim().length === 0 || input.afterCommit!.trim().length === 0) {\n throw new Error('beforeCommit and afterCommit must not be empty strings')\n }\n }\n\n if (hasRunIds) {\n if (input.beforeRunId!.trim().length === 0 || input.afterRunId!.trim().length === 0) {\n throw new Error('beforeRunId and afterRunId must not be empty strings')\n }\n }\n\n const response = await client.compareTestMetrics({\n projectId: input.projectId,\n testName: input.testName,\n beforeCommit: input.beforeCommit,\n afterCommit: input.afterCommit,\n beforeRunId: input.beforeRunId,\n afterRunId: input.afterRunId,\n })\n\n return response\n}\n\n/**\n * Tool metadata\n */\nexport const compareTestMetricsMetadata = {\n name: 'compare_test_metrics',\n title: 'Compare Test Metrics',\n description: `Compare test metrics between two commits or test runs.\n\nUseful for measuring the impact of code changes on test performance or reliability.\n\nParameters:\n- projectId (required): Project ID\n- testName (required): The test name to compare (short name or full name)\n- Option 1 - Compare by commit:\n - beforeCommit: Commit SHA for \"before\" measurement\n - afterCommit: Commit SHA for \"after\" measurement\n- Option 2 - Compare by test run:\n - beforeRunId: Test run ID for \"before\" measurement\n - afterRunId: Test run ID for \"after\" measurement\n\nReturns:\n- testName: The test that was compared\n- before: Metrics from the before commit/run\n - testRunId, commit, branch, status, durationMs, createdAt\n- after: Metrics from the after commit/run\n - testRunId, commit, branch, status, durationMs, createdAt\n- change: Calculated changes\n - durationMs: Duration difference (negative = faster)\n - percentChange: Percentage change (negative = improvement)\n - statusChanged: Whether pass/fail status changed\n\nUse cases:\n- \"Did my fix make this test faster?\"\n- \"Compare test performance between these two commits\"\n- \"Did this test start failing after my changes?\"\n- \"Show me the before/after for the slow test I optimized\"\n\nTip: Use get_test_history first to find the commit SHAs or test run IDs you want to compare.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for find_uncovered_failure_areas tool\n */\nexport const findUncoveredFailureAreasInputSchema = {\n projectId: z\n .string()\n .describe('Project ID. Use list_projects to find project IDs.'),\n days: z\n .number()\n .int()\n .min(1)\n .max(365)\n .optional()\n .describe('Number of days to analyze for test failures (default: 30)'),\n coverageThreshold: z\n .number()\n .min(0)\n .max(100)\n .optional()\n .describe('Include files with coverage below this percentage (default: 80)'),\n}\n\n/**\n * Output schema for find_uncovered_failure_areas tool\n */\nexport const findUncoveredFailureAreasOutputSchema = {\n hasCoverage: z.boolean(),\n hasTestResults: z.boolean(),\n riskAreas: z.array(z.object({\n filePath: z.string(),\n coverage: z.number(),\n failureCount: z.number(),\n riskScore: z.number(),\n testNames: z.array(z.string()),\n })),\n message: z.string().optional(),\n}\n\nexport interface FindUncoveredFailureAreasInput {\n projectId: string\n days?: number\n coverageThreshold?: number\n}\n\nexport interface FindUncoveredFailureAreasOutput {\n hasCoverage: boolean\n hasTestResults: boolean\n riskAreas: Array<{\n filePath: string\n coverage: number\n failureCount: number\n riskScore: number\n testNames: string[]\n }>\n message?: string\n}\n\n/**\n * Execute find_uncovered_failure_areas tool\n */\nexport async function executeFindUncoveredFailureAreas(\n client: GafferApiClient,\n input: FindUncoveredFailureAreasInput,\n): Promise<FindUncoveredFailureAreasOutput> {\n const response = await client.getCoverageRiskAreas({\n projectId: input.projectId,\n days: input.days,\n coverageThreshold: input.coverageThreshold,\n })\n\n return {\n hasCoverage: response.hasCoverage,\n hasTestResults: response.hasTestResults,\n riskAreas: response.riskAreas,\n message: response.message,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const findUncoveredFailureAreasMetadata = {\n name: 'find_uncovered_failure_areas',\n title: 'Find Uncovered Failure Areas',\n description: `Find areas of code that have both low coverage AND test failures.\n\nThis cross-references test failures with coverage data to identify high-risk\nareas in your codebase that need attention. Files are ranked by a \"risk score\"\ncalculated as: (100 - coverage%) × failureCount.\n\nParameters:\n- projectId: The project to analyze (required)\n- days: Analysis period for test failures (default: 30)\n- coverageThreshold: Include files below this coverage % (default: 80)\n\nReturns:\n- List of risk areas sorted by risk score (highest risk first)\n- Each area includes: file path, coverage %, failure count, risk score, test names\n\nUse this to prioritize which parts of your codebase need better test coverage.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_coverage_for_file tool\n */\nexport const getCoverageForFileInputSchema = {\n projectId: z\n .string()\n .describe('Project ID. Use list_projects to find project IDs.'),\n filePath: z\n .string()\n .describe('File path to get coverage for. Can be exact path or partial match.'),\n}\n\n/**\n * Output schema for get_coverage_for_file tool\n */\nexport const getCoverageForFileOutputSchema = {\n hasCoverage: z.boolean(),\n files: z.array(z.object({\n path: z.string(),\n lines: z.object({\n covered: z.number(),\n total: z.number(),\n percentage: z.number(),\n }),\n branches: z.object({\n covered: z.number(),\n total: z.number(),\n percentage: z.number(),\n }),\n functions: z.object({\n covered: z.number(),\n total: z.number(),\n percentage: z.number(),\n }),\n })),\n message: z.string().optional(),\n}\n\nexport interface GetCoverageForFileInput {\n projectId: string\n filePath: string\n}\n\nexport interface GetCoverageForFileOutput {\n hasCoverage: boolean\n files: Array<{\n path: string\n lines: { covered: number, total: number, percentage: number }\n branches: { covered: number, total: number, percentage: number }\n functions: { covered: number, total: number, percentage: number }\n }>\n message?: string\n}\n\n/**\n * Execute get_coverage_for_file tool\n */\nexport async function executeGetCoverageForFile(\n client: GafferApiClient,\n input: GetCoverageForFileInput,\n): Promise<GetCoverageForFileOutput> {\n const response = await client.getCoverageFiles({\n projectId: input.projectId,\n filePath: input.filePath,\n limit: 10, // Return up to 10 matching files\n })\n\n return {\n hasCoverage: response.hasCoverage,\n files: response.files.map(f => ({\n path: f.path,\n lines: f.lines,\n branches: f.branches,\n functions: f.functions,\n })),\n message: response.message,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getCoverageForFileMetadata = {\n name: 'get_coverage_for_file',\n title: 'Get Coverage for File',\n description: `Get coverage metrics for a specific file or files matching a path pattern.\n\nParameters:\n- projectId: The project to query (required)\n- filePath: File path to search for (exact or partial match)\n\nReturns:\n- Line coverage (covered/total/percentage)\n- Branch coverage (covered/total/percentage)\n- Function coverage (covered/total/percentage)\n\nThis is the preferred tool for targeted coverage analysis. Use path prefixes to focus on\nspecific areas of the codebase:\n- \"server/services\" - Backend service layer\n- \"server/utils\" - Backend utilities\n- \"src/api\" - API routes\n- \"lib/core\" - Core business logic\n\nBefore querying, explore the codebase to identify critical paths - entry points,\nheavily-imported files, and code handling auth/payments/data mutations.\nPrioritize: high utilization + low coverage = highest impact.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_coverage_summary tool\n */\nexport const getCoverageSummaryInputSchema = {\n projectId: z\n .string()\n .describe('Project ID. Use list_projects to find project IDs.'),\n days: z\n .number()\n .int()\n .min(1)\n .max(365)\n .optional()\n .describe('Number of days to analyze for trends (default: 30)'),\n}\n\n/**\n * Output schema for get_coverage_summary tool\n */\nexport const getCoverageSummaryOutputSchema = {\n hasCoverage: z.boolean(),\n current: z.object({\n lines: z.number(),\n branches: z.number(),\n functions: z.number(),\n }).optional(),\n trend: z.object({\n direction: z.enum(['up', 'down', 'stable']),\n change: z.number(),\n }).optional(),\n totalReports: z.number(),\n latestReportDate: z.string().nullable().optional(),\n lowestCoverageFiles: z.array(z.object({\n path: z.string(),\n coverage: z.number(),\n })).optional(),\n message: z.string().optional(),\n}\n\nexport interface GetCoverageSummaryInput {\n projectId: string\n days?: number\n}\n\nexport interface GetCoverageSummaryOutput {\n hasCoverage: boolean\n current?: {\n lines: number\n branches: number\n functions: number\n }\n trend?: {\n direction: 'up' | 'down' | 'stable'\n change: number\n }\n totalReports: number\n latestReportDate?: string | null\n lowestCoverageFiles?: Array<{\n path: string\n coverage: number\n }>\n message?: string\n}\n\n/**\n * Execute get_coverage_summary tool\n */\nexport async function executeGetCoverageSummary(\n client: GafferApiClient,\n input: GetCoverageSummaryInput,\n): Promise<GetCoverageSummaryOutput> {\n const response = await client.getCoverageSummary({\n projectId: input.projectId,\n days: input.days,\n })\n\n return {\n hasCoverage: response.hasCoverage,\n current: response.current,\n trend: response.trend,\n totalReports: response.totalReports,\n latestReportDate: response.latestReportDate,\n lowestCoverageFiles: response.lowestCoverageFiles,\n message: response.message,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getCoverageSummaryMetadata = {\n name: 'get_coverage_summary',\n title: 'Get Coverage Summary',\n description: `Get the coverage metrics summary for a project.\n\nReturns:\n- Current coverage percentages (lines, branches, functions)\n- Trend direction (up, down, stable) and change amount\n- Total number of coverage reports\n- Latest report date\n- Top 5 files with lowest coverage\n\nUse this to understand your project's overall test coverage health.\n\nAfter getting the summary, use get_coverage_for_file with path prefixes to drill into\nspecific areas (e.g., \"server/services\", \"src/api\", \"lib/core\"). This helps identify\nhigh-value targets in critical code paths rather than just the files with lowest coverage.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport type { FailureClustersResponse } from '../types.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_failure_clusters tool\n */\nexport const getFailureClustersInputSchema = {\n projectId: z\n .string()\n .describe('Project ID. Use list_projects to find project IDs.'),\n testRunId: z\n .string()\n .describe('Test run ID. Use list_test_runs to find test run IDs.'),\n}\n\n/**\n * Output schema for get_failure_clusters tool\n */\nexport const getFailureClustersOutputSchema = {\n clusters: z.array(z.object({\n representativeError: z.string(),\n count: z.number(),\n tests: z.array(z.object({\n name: z.string(),\n fullName: z.string(),\n errorMessage: z.string(),\n filePath: z.string().nullable(),\n })),\n similarity: z.number(),\n })),\n totalFailures: z.number(),\n}\n\nexport interface GetFailureClustersInput {\n projectId: string\n testRunId: string\n}\n\n/**\n * Execute get_failure_clusters tool\n */\nexport async function executeGetFailureClusters(\n client: GafferApiClient,\n input: GetFailureClustersInput,\n): Promise<FailureClustersResponse> {\n return client.getFailureClusters({\n projectId: input.projectId,\n testRunId: input.testRunId,\n })\n}\n\n/**\n * Tool metadata\n */\nexport const getFailureClustersMetadata = {\n name: 'get_failure_clusters',\n title: 'Get Failure Clusters',\n description: `Group failed tests by root cause using error message similarity.\n\nParameters:\n- projectId (required): The project ID\n- testRunId (required): The test run ID to analyze\n\nReturns:\n- clusters: Array of failure clusters, each containing:\n - representativeError: The error message representing this cluster\n - count: Number of tests with this same root cause\n - tests: Array of individual failed tests in this cluster\n - name: Short test name\n - fullName: Full test name including describe blocks\n - errorMessage: The specific error message\n - filePath: Test file path (null if not recorded)\n - similarity: Similarity threshold used for clustering (0-1)\n- totalFailures: Total number of failed tests across all clusters\n\nUse cases:\n- \"Group these 15 failures by root cause\" — often reveals 2-3 distinct bugs\n- \"Which error affects the most tests?\" — fix the largest cluster first\n- \"Are these failures related?\" — check if they land in the same cluster\n\nTip: Use get_test_run_details with status='failed' first to see raw failures,\nthen use this tool to understand which failures share the same root cause.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_flaky_tests tool\n */\nexport const getFlakyTestsInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n threshold: z\n .number()\n .min(0)\n .max(1)\n .optional()\n .describe('Minimum flip rate to be considered flaky (0-1, default: 0.1 = 10%)'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum number of flaky tests to return (default: 50)'),\n days: z\n .number()\n .int()\n .min(1)\n .max(365)\n .optional()\n .describe('Analysis period in days (default: 30)'),\n}\n\n/**\n * Output schema for get_flaky_tests tool\n */\nexport const getFlakyTestsOutputSchema = {\n flakyTests: z.array(z.object({\n name: z.string(),\n flipRate: z.number(),\n flipCount: z.number(),\n totalRuns: z.number(),\n lastSeen: z.string(),\n flakinessScore: z.number(),\n })),\n summary: z.object({\n threshold: z.number(),\n totalFlaky: z.number(),\n period: z.number(),\n }),\n}\n\nexport interface GetFlakyTestsInput {\n projectId?: string\n threshold?: number\n limit?: number\n days?: number\n}\n\nexport interface GetFlakyTestsOutput {\n flakyTests: Array<{\n name: string\n flipRate: number\n flipCount: number\n totalRuns: number\n lastSeen: string\n flakinessScore: number\n }>\n summary: {\n threshold: number\n totalFlaky: number\n period: number\n }\n}\n\n/**\n * Execute get_flaky_tests tool\n */\nexport async function executeGetFlakyTests(\n client: GafferApiClient,\n input: GetFlakyTestsInput,\n): Promise<GetFlakyTestsOutput> {\n const response = await client.getFlakyTests({\n projectId: input.projectId,\n threshold: input.threshold,\n limit: input.limit,\n days: input.days,\n })\n\n return {\n flakyTests: response.flakyTests,\n summary: response.summary,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getFlakyTestsMetadata = {\n name: 'get_flaky_tests',\n title: 'Get Flaky Tests',\n description: `Get the list of flaky tests in a project.\n\nA test is considered flaky if it frequently switches between pass and fail states.\nTests are ranked by a composite flakinessScore that factors in flip behavior,\nfailure rate, and duration variability.\n\nReturns:\n- List of flaky tests sorted by flakinessScore (most flaky first), with:\n - name: Test name\n - flipRate: How often the test flips between pass/fail (0-1)\n - flipCount: Number of status transitions\n - totalRuns: Total test executions analyzed\n - lastSeen: When the test last ran\n - flakinessScore: Composite score (0-1) combining flip proximity, failure rate, and duration variability\n- Summary with threshold used and total count\n\nUse this after get_project_health shows flaky tests exist, to identify which\nspecific tests are flaky and need investigation.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_project_health tool\n */\nexport const getProjectHealthInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n days: z\n .number()\n .int()\n .min(1)\n .max(365)\n .optional()\n .describe('Number of days to analyze (default: 30)'),\n}\n\n/**\n * Output schema for get_project_health tool\n */\nexport const getProjectHealthOutputSchema = {\n projectName: z.string(),\n healthScore: z.number(),\n passRate: z.number().nullable(),\n testRunCount: z.number(),\n flakyTestCount: z.number(),\n trend: z.enum(['up', 'down', 'stable']),\n period: z.object({\n days: z.number(),\n start: z.string(),\n end: z.string(),\n }),\n}\n\nexport interface GetProjectHealthInput {\n projectId?: string\n days?: number\n}\n\nexport interface GetProjectHealthOutput {\n projectName: string\n healthScore: number\n passRate: number | null\n testRunCount: number\n flakyTestCount: number\n trend: 'up' | 'down' | 'stable'\n period: {\n days: number\n start: string\n end: string\n }\n}\n\n/**\n * Execute get_project_health tool\n */\nexport async function executeGetProjectHealth(\n client: GafferApiClient,\n input: GetProjectHealthInput,\n): Promise<GetProjectHealthOutput> {\n const response = await client.getProjectHealth({\n projectId: input.projectId,\n days: input.days,\n })\n\n return {\n projectName: response.analytics.projectName,\n healthScore: response.analytics.healthScore,\n passRate: response.analytics.passRate,\n testRunCount: response.analytics.testRunCount,\n flakyTestCount: response.analytics.flakyTestCount,\n trend: response.analytics.trend,\n period: response.analytics.period,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getProjectHealthMetadata = {\n name: 'get_project_health',\n title: 'Get Project Health',\n description: `Get the health metrics for a project.\n\nReturns:\n- Health score (0-100): Overall project health based on pass rate and trend\n- Pass rate: Percentage of tests passing\n- Test run count: Number of test runs in the period\n- Flaky test count: Number of tests with inconsistent results\n- Trend: Whether test health is improving (up), declining (down), or stable\n\nUse this to understand the current state of your test suite.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_report_browser_url tool\n */\nexport const getReportBrowserUrlInputSchema = {\n projectId: z\n .string()\n .describe('Project ID. Use list_projects to find project IDs.'),\n testRunId: z\n .string()\n .describe('The test run ID to get the report URL for. Use list_test_runs to find test run IDs.'),\n filename: z\n .string()\n .optional()\n .describe('Specific file to open (default: index.html or first HTML file)'),\n}\n\n/**\n * Output schema for get_report_browser_url tool\n */\nexport const getReportBrowserUrlOutputSchema = {\n url: z.string(),\n filename: z.string(),\n testRunId: z.string(),\n expiresAt: z.string(),\n expiresInSeconds: z.number(),\n}\n\nexport interface GetReportBrowserUrlInput {\n projectId: string\n testRunId: string\n filename?: string\n}\n\nexport interface GetReportBrowserUrlOutput {\n url: string\n filename: string\n testRunId: string\n expiresAt: string\n expiresInSeconds: number\n}\n\n/**\n * Execute get_report_browser_url tool\n */\nexport async function executeGetReportBrowserUrl(\n client: GafferApiClient,\n input: GetReportBrowserUrlInput,\n): Promise<GetReportBrowserUrlOutput> {\n const response = await client.getReportBrowserUrl({\n projectId: input.projectId,\n testRunId: input.testRunId,\n filename: input.filename,\n })\n\n return {\n url: response.url,\n filename: response.filename,\n testRunId: response.testRunId,\n expiresAt: response.expiresAt,\n expiresInSeconds: response.expiresInSeconds,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getReportBrowserUrlMetadata = {\n name: 'get_report_browser_url',\n title: 'Get Report Browser URL',\n description: `Get a browser-navigable URL for viewing a test report (Playwright, Vitest, etc.).\n\nReturns a signed URL that can be opened directly in a browser without requiring\nthe user to log in. The URL expires after 30 minutes for security.\n\nParameters:\n- projectId: The project the test run belongs to (required)\n- testRunId: The test run to view (required)\n- filename: Specific file to open (optional, defaults to index.html)\n\nReturns:\n- url: Browser-navigable URL with signed token\n- filename: The file being accessed\n- expiresAt: ISO timestamp when the URL expires\n- expiresInSeconds: Time until expiration\n\nThe returned URL can be shared with users who need to view the report.\nNote: URLs expire after 30 minutes for security.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport type { ReportFile, ReportResponse } from '../types.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_report tool\n */\nexport const getReportInputSchema = {\n testRunId: z\n .string()\n .describe('The test run ID to get report files for. Use list_test_runs to find test run IDs.'),\n}\n\n/**\n * Output schema for get_report tool\n */\nexport const getReportOutputSchema = {\n testRunId: z.string(),\n projectId: z.string(),\n projectName: z.string(),\n resultSchema: z.string().optional(),\n files: z.array(z.object({\n filename: z.string(),\n size: z.number(),\n contentType: z.string(),\n downloadUrl: z.string(),\n })),\n urlExpiresInSeconds: z.number().optional(),\n}\n\nexport interface GetReportInput {\n testRunId: string\n}\n\n// Re-export types from types.ts for convenience\nexport type { ReportFile, ReportResponse }\n\n// Output type matches ReportResponse\nexport type GetReportOutput = ReportResponse\n\n/**\n * Execute get_report tool\n */\nexport async function executeGetReport(\n client: GafferApiClient,\n input: GetReportInput,\n): Promise<GetReportOutput> {\n const response = await client.getReport(input.testRunId)\n\n return {\n testRunId: response.testRunId,\n projectId: response.projectId,\n projectName: response.projectName,\n resultSchema: response.resultSchema,\n files: response.files.map(file => ({\n filename: file.filename,\n size: file.size,\n contentType: file.contentType,\n downloadUrl: file.downloadUrl,\n })),\n urlExpiresInSeconds: response.urlExpiresInSeconds,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getReportMetadata = {\n name: 'get_report',\n title: 'Get Report Files',\n description: `Get URLs for report files uploaded with a test run.\n\nIMPORTANT: This tool returns download URLs, not file content. You must fetch the URLs separately.\n\nReturns for each file:\n- filename: The file name (e.g., \"report.html\", \"results.json\", \"junit.xml\")\n- size: File size in bytes\n- contentType: MIME type (e.g., \"text/html\", \"application/json\", \"application/xml\")\n- downloadUrl: Presigned URL to download the file (valid for ~5 minutes)\n\nHow to use the returned URLs:\n\n1. **JSON files** (results.json, coverage.json):\n Use WebFetch with the downloadUrl to retrieve and parse the JSON content.\n Example: WebFetch(url=downloadUrl, prompt=\"Extract test results from this JSON\")\n\n2. **XML files** (junit.xml, xunit.xml):\n Use WebFetch with the downloadUrl to retrieve and parse the XML content.\n Example: WebFetch(url=downloadUrl, prompt=\"Parse the test results from this JUnit XML\")\n\n3. **HTML reports** (Playwright, pytest-html, Vitest):\n These are typically bundled React/JavaScript applications that require a browser.\n They cannot be meaningfully parsed by WebFetch.\n For programmatic analysis, use get_test_run_details instead.\n\nRecommendations:\n- For analyzing test results programmatically: Use get_test_run_details (returns parsed test data)\n- For JSON/XML files: Use this tool + WebFetch on the downloadUrl\n- For HTML reports: Direct users to view in browser, or use get_test_run_details\n\nUse cases:\n- \"What files are in this test run?\" (list available reports)\n- \"Get the coverage data from this run\" (then WebFetch the JSON URL)\n- \"Parse the JUnit XML results\" (then WebFetch the XML URL)`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport type { SlowestTestEntry, SlowestTestsResponse } from '../types.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_slowest_tests tool\n */\nexport const getSlowestTestsInputSchema = {\n projectId: z\n .string()\n .describe('Project ID. Use list_projects to find project IDs.'),\n days: z\n .number()\n .int()\n .min(1)\n .max(365)\n .optional()\n .describe('Analysis period in days (default: 30)'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum number of tests to return (default: 20)'),\n framework: z\n .string()\n .optional()\n .describe('Filter by test framework (e.g., \"playwright\", \"vitest\", \"jest\")'),\n branch: z\n .string()\n .optional()\n .describe('Filter by git branch name (e.g., \"main\", \"develop\")'),\n}\n\n/**\n * Output schema for get_slowest_tests tool\n */\nexport const getSlowestTestsOutputSchema = {\n slowestTests: z.array(z.object({\n name: z.string(),\n fullName: z.string(),\n filePath: z.string().optional(),\n framework: z.string().optional(),\n avgDurationMs: z.number(),\n p95DurationMs: z.number(),\n runCount: z.number(),\n })),\n summary: z.object({\n projectId: z.string(),\n projectName: z.string(),\n period: z.number(),\n totalReturned: z.number(),\n }),\n}\n\nexport interface GetSlowestTestsInput {\n projectId: string\n days?: number\n limit?: number\n framework?: string\n branch?: string\n}\n\n// Re-export types from types.ts for convenience\nexport type { SlowestTestEntry, SlowestTestsResponse }\n\n// Output type matches SlowestTestsResponse\nexport type GetSlowestTestsOutput = SlowestTestsResponse\n\n/**\n * Execute get_slowest_tests tool\n */\nexport async function executeGetSlowestTests(\n client: GafferApiClient,\n input: GetSlowestTestsInput,\n): Promise<GetSlowestTestsOutput> {\n const response = await client.getSlowestTests({\n projectId: input.projectId,\n days: input.days,\n limit: input.limit,\n framework: input.framework,\n branch: input.branch,\n })\n\n return {\n slowestTests: response.slowestTests.map(test => ({\n name: test.name,\n fullName: test.fullName,\n filePath: test.filePath,\n framework: test.framework,\n avgDurationMs: test.avgDurationMs,\n p95DurationMs: test.p95DurationMs,\n runCount: test.runCount,\n })),\n summary: response.summary,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getSlowestTestsMetadata = {\n name: 'get_slowest_tests',\n title: 'Get Slowest Tests',\n description: `Get the slowest tests in a project, sorted by P95 duration.\n\nParameters:\n- projectId (required): Project ID to analyze\n- days (optional): Analysis period in days (default: 30, max: 365)\n- limit (optional): Max tests to return (default: 20, max: 100)\n- framework (optional): Filter by framework (e.g., \"playwright\", \"vitest\")\n- branch (optional): Filter by git branch (e.g., \"main\", \"develop\")\n\nReturns:\n- List of slowest tests with:\n - name: Short test name\n - fullName: Full test name including describe blocks\n - filePath: Test file path (if available)\n - framework: Test framework used\n - avgDurationMs: Average test duration in milliseconds\n - p95DurationMs: 95th percentile duration (used for sorting)\n - runCount: Number of times the test ran in the period\n- Summary with project info and period\n\nUse cases:\n- \"Which tests are slowing down my CI pipeline?\"\n- \"Find the slowest Playwright tests to optimize\"\n- \"Show me e2e tests taking over 30 seconds\"\n- \"What are the slowest tests on the main branch?\"`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_test_history tool\n */\nexport const getTestHistoryInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n testName: z\n .string()\n .optional()\n .describe('Exact test name to search for'),\n filePath: z\n .string()\n .optional()\n .describe('File path containing the test'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum number of results (default: 20)'),\n}\n\n/**\n * Output schema for get_test_history tool\n */\nexport const getTestHistoryOutputSchema = {\n history: z.array(z.object({\n testRunId: z.string(),\n createdAt: z.string(),\n branch: z.string().optional(),\n commitSha: z.string().optional(),\n status: z.enum(['passed', 'failed', 'skipped', 'pending']),\n durationMs: z.number(),\n message: z.string().optional(),\n })),\n summary: z.object({\n totalRuns: z.number(),\n passedRuns: z.number(),\n failedRuns: z.number(),\n passRate: z.number().nullable(),\n }),\n}\n\nexport interface GetTestHistoryInput {\n projectId?: string\n testName?: string\n filePath?: string\n limit?: number\n}\n\nexport interface GetTestHistoryOutput {\n history: Array<{\n testRunId: string\n createdAt: string\n branch?: string\n commitSha?: string\n status: 'passed' | 'failed' | 'skipped' | 'pending'\n durationMs: number\n message?: string\n }>\n summary: {\n totalRuns: number\n passedRuns: number\n failedRuns: number\n passRate: number | null\n }\n}\n\n/**\n * Execute get_test_history tool\n */\nexport async function executeGetTestHistory(\n client: GafferApiClient,\n input: GetTestHistoryInput,\n): Promise<GetTestHistoryOutput> {\n if (!input.testName && !input.filePath) {\n throw new Error('Either testName or filePath is required')\n }\n\n const response = await client.getTestHistory({\n projectId: input.projectId,\n testName: input.testName,\n filePath: input.filePath,\n limit: input.limit || 20,\n })\n\n return {\n history: response.history.map(entry => ({\n testRunId: entry.testRunId,\n createdAt: entry.createdAt,\n branch: entry.branch,\n commitSha: entry.commitSha,\n status: entry.test.status,\n durationMs: entry.test.durationMs,\n message: entry.test.message || undefined, // Convert null to undefined for schema compliance\n })),\n summary: {\n totalRuns: response.summary.totalRuns,\n passedRuns: response.summary.passedRuns,\n failedRuns: response.summary.failedRuns,\n passRate: response.summary.passRate,\n },\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getTestHistoryMetadata = {\n name: 'get_test_history',\n title: 'Get Test History',\n description: `Get the pass/fail history for a specific test.\n\nSearch by either:\n- testName: The exact name of the test (e.g., \"should handle user login\")\n- filePath: The file path containing the test (e.g., \"tests/auth.test.ts\")\n\nReturns:\n- History of test runs showing pass/fail status over time\n- Duration of each run\n- Branch and commit information\n- Error messages for failed runs\n- Summary statistics (pass rate, total runs)\n\nUse this to investigate flaky tests or understand test stability.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_test_run_details tool\n */\nexport const getTestRunDetailsInputSchema = {\n testRunId: z\n .string()\n .describe('The test run ID to get details for. Use list_test_runs to find test run IDs.'),\n projectId: z\n .string()\n .describe('Project ID. Use list_projects to find project IDs.'),\n status: z\n .enum(['passed', 'failed', 'skipped'])\n .optional()\n .describe('Filter tests by status. Returns only tests matching this status.'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe('Maximum number of tests to return (default: 100, max: 500)'),\n offset: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe('Number of tests to skip for pagination (default: 0)'),\n}\n\n/**\n * Output schema for get_test_run_details tool\n */\nexport const getTestRunDetailsOutputSchema = {\n testRunId: z.string(),\n commitSha: z.string().nullable(),\n branch: z.string().nullable(),\n framework: z.string().nullable(),\n createdAt: z.string(),\n summary: z.object({\n passed: z.number(),\n failed: z.number(),\n skipped: z.number(),\n total: z.number(),\n }),\n tests: z.array(z.object({\n name: z.string(),\n fullName: z.string(),\n status: z.enum(['passed', 'failed', 'skipped']),\n durationMs: z.number().nullable(),\n filePath: z.string().nullable(),\n error: z.string().nullable(),\n errorStack: z.string().nullable(),\n })),\n pagination: z.object({\n total: z.number(),\n limit: z.number(),\n offset: z.number(),\n hasMore: z.boolean(),\n }),\n}\n\nexport interface GetTestRunDetailsInput {\n testRunId: string\n projectId: string\n status?: 'passed' | 'failed' | 'skipped'\n limit?: number\n offset?: number\n}\n\nexport interface GetTestRunDetailsOutput {\n testRunId: string\n commitSha: string | null\n branch: string | null\n framework: string | null\n createdAt: string\n summary: {\n passed: number\n failed: number\n skipped: number\n total: number\n }\n tests: Array<{\n name: string\n fullName: string\n status: 'passed' | 'failed' | 'skipped'\n durationMs: number | null\n filePath: string | null\n error: string | null\n errorStack: string | null\n }>\n pagination: {\n total: number\n limit: number\n offset: number\n hasMore: boolean\n }\n}\n\n/**\n * Execute get_test_run_details tool\n */\nexport async function executeGetTestRunDetails(\n client: GafferApiClient,\n input: GetTestRunDetailsInput,\n): Promise<GetTestRunDetailsOutput> {\n const response = await client.getTestRunDetails({\n projectId: input.projectId,\n testRunId: input.testRunId,\n status: input.status,\n limit: input.limit,\n offset: input.offset,\n })\n\n return {\n testRunId: response.testRunId,\n commitSha: response.commitSha,\n branch: response.branch,\n framework: response.framework,\n createdAt: response.createdAt,\n summary: response.summary,\n tests: response.tests,\n pagination: response.pagination,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getTestRunDetailsMetadata = {\n name: 'get_test_run_details',\n title: 'Get Test Run Details',\n description: `Get parsed test results for a specific test run.\n\nParameters:\n- testRunId (required): The test run ID to get details for\n- projectId (required): Project ID the test run belongs to\n- status (optional): Filter by test status: \"passed\", \"failed\", or \"skipped\"\n- limit (optional): Max tests to return (default: 100, max: 500)\n- offset (optional): Pagination offset (default: 0)\n\nReturns:\n- testRunId: The test run ID\n- commitSha: Git commit SHA (null if not recorded)\n- branch: Git branch name (null if not recorded)\n- framework: Test framework (e.g., \"playwright\", \"vitest\")\n- createdAt: When the test run was created (ISO 8601)\n- summary: Overall counts (passed, failed, skipped, total)\n- tests: Array of individual test results with:\n - name: Short test name\n - fullName: Full test name including describe blocks\n - status: Test status (passed, failed, skipped)\n - durationMs: Test duration in milliseconds (null if not recorded)\n - filePath: Test file path (null if not recorded)\n - error: Error message for failed tests (null otherwise)\n - errorStack: Full stack trace for failed tests (null otherwise)\n- pagination: Pagination info (total, limit, offset, hasMore)\n\nUse cases:\n- \"Show me all failed tests from this test run\"\n- \"Get the test results from commit abc123\"\n- \"List tests that took the longest in this run\"\n- \"Find tests with errors in the auth module\"\n\nNote: For aggregate analytics like flaky test detection or duration trends,\nuse get_test_history, get_flaky_tests, or get_slowest_tests instead.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_untested_files tool\n */\nexport const getUntestedFilesInputSchema = {\n projectId: z\n .string()\n .describe('Project ID. Use list_projects to find project IDs.'),\n maxCoverage: z\n .number()\n .min(0)\n .max(100)\n .optional()\n .describe('Maximum coverage percentage to include (default: 10 for \"untested\")'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum number of files to return (default: 20)'),\n}\n\n/**\n * Output schema for get_untested_files tool\n */\nexport const getUntestedFilesOutputSchema = {\n hasCoverage: z.boolean(),\n files: z.array(z.object({\n path: z.string(),\n lines: z.object({\n covered: z.number(),\n total: z.number(),\n percentage: z.number(),\n }),\n branches: z.object({\n covered: z.number(),\n total: z.number(),\n percentage: z.number(),\n }),\n functions: z.object({\n covered: z.number(),\n total: z.number(),\n percentage: z.number(),\n }),\n })),\n totalCount: z.number(),\n message: z.string().optional(),\n}\n\nexport interface GetUntestedFilesInput {\n projectId: string\n maxCoverage?: number\n limit?: number\n}\n\nexport interface GetUntestedFilesOutput {\n hasCoverage: boolean\n files: Array<{\n path: string\n lines: { covered: number, total: number, percentage: number }\n branches: { covered: number, total: number, percentage: number }\n functions: { covered: number, total: number, percentage: number }\n }>\n totalCount: number\n message?: string\n}\n\n/**\n * Execute get_untested_files tool\n */\nexport async function executeGetUntestedFiles(\n client: GafferApiClient,\n input: GetUntestedFilesInput,\n): Promise<GetUntestedFilesOutput> {\n const maxCoverage = input.maxCoverage ?? 10\n const limit = input.limit ?? 20\n\n const response = await client.getCoverageFiles({\n projectId: input.projectId,\n maxCoverage,\n limit,\n sortBy: 'coverage',\n sortOrder: 'asc', // Lowest coverage first\n })\n\n return {\n hasCoverage: response.hasCoverage,\n files: response.files.map(f => ({\n path: f.path,\n lines: f.lines,\n branches: f.branches,\n functions: f.functions,\n })),\n totalCount: response.pagination.total,\n message: response.message,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getUntestedFilesMetadata = {\n name: 'get_untested_files',\n title: 'Get Untested Files',\n description: `Get files with little or no test coverage.\n\nReturns files sorted by coverage percentage (lowest first), filtered\nto only include files below a coverage threshold.\n\nParameters:\n- projectId: The project to analyze (required)\n- maxCoverage: Include files with coverage at or below this % (default: 10)\n- limit: Maximum number of files to return (default: 20, max: 100)\n\nReturns:\n- List of files sorted by coverage (lowest first)\n- Each file includes line/branch/function coverage metrics\n- Total count of files matching the criteria\n\nIMPORTANT: Results may be dominated by certain file types (e.g., UI components) that are\nnumerous but not necessarily the highest priority. For targeted analysis of specific code\nareas (backend, services, utilities), use get_coverage_for_file with path prefixes instead.\n\nTo prioritize effectively, explore the codebase to understand which code is heavily utilized\n(entry points, frequently-imported files, critical business logic) and then query coverage\nfor those specific paths.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport type { UploadSessionDetailResponse, UploadSessionsResponse } from '../types.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_upload_status tool\n */\nexport const getUploadStatusInputSchema = {\n projectId: z\n .string()\n .describe('Project ID. Use list_projects to find project IDs.'),\n sessionId: z\n .string()\n .optional()\n .describe('Specific upload session ID. If provided, returns detailed status for that session. Otherwise, lists recent sessions.'),\n commitSha: z\n .string()\n .optional()\n .describe('Filter sessions by commit SHA. Useful for checking if results for a specific commit are ready.'),\n branch: z\n .string()\n .optional()\n .describe('Filter sessions by branch name.'),\n}\n\n/**\n * Output schema for get_upload_status tool\n */\nexport const getUploadStatusOutputSchema = {\n sessions: z.array(z.object({\n id: z.string(),\n processingStatus: z.string(),\n commitSha: z.string().nullable(),\n branch: z.string().nullable(),\n pendingFileCount: z.number(),\n failedFileCount: z.number(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })).optional(),\n session: z.object({\n id: z.string(),\n processingStatus: z.string(),\n commitSha: z.string().nullable(),\n branch: z.string().nullable(),\n createdAt: z.string(),\n }).optional(),\n testRuns: z.array(z.object({\n id: z.string(),\n framework: z.string().nullable(),\n summary: z.object({\n passed: z.number(),\n failed: z.number(),\n skipped: z.number(),\n total: z.number(),\n }),\n createdAt: z.string(),\n })).optional(),\n coverageReports: z.array(z.object({\n id: z.string(),\n format: z.string(),\n createdAt: z.string(),\n })).optional(),\n pagination: z.object({\n total: z.number(),\n limit: z.number(),\n offset: z.number(),\n hasMore: z.boolean(),\n }).optional(),\n}\n\nexport interface GetUploadStatusInput {\n projectId: string\n sessionId?: string\n commitSha?: string\n branch?: string\n}\n\nexport type GetUploadStatusOutput = UploadSessionsResponse | UploadSessionDetailResponse\n\n/**\n * Execute get_upload_status tool\n */\nexport async function executeGetUploadStatus(\n client: GafferApiClient,\n input: GetUploadStatusInput,\n): Promise<GetUploadStatusOutput> {\n if (input.sessionId) {\n return client.getUploadSessionDetail({\n projectId: input.projectId,\n sessionId: input.sessionId,\n })\n }\n\n return client.listUploadSessions({\n projectId: input.projectId,\n commitSha: input.commitSha,\n branch: input.branch,\n })\n}\n\n/**\n * Tool metadata\n */\nexport const getUploadStatusMetadata = {\n name: 'get_upload_status',\n title: 'Get Upload Status',\n description: `Check if CI results have been uploaded and processed.\n\nUse this tool to answer \"are my test results ready?\" after pushing code.\n\nParameters:\n- projectId (required): The project ID\n- sessionId (optional): Specific upload session ID for detailed status\n- commitSha (optional): Filter by commit SHA to find uploads for a specific commit\n- branch (optional): Filter by branch name\n\nBehavior:\n- If sessionId is provided: returns detailed status with linked test runs and coverage reports\n- Otherwise: returns a list of recent upload sessions (filtered by commitSha/branch if provided)\n\nProcessing statuses:\n- \"pending\" — upload received, processing not started\n- \"processing\" — files are being parsed\n- \"completed\" — all files processed successfully, results are ready\n- \"error\" — some files failed to process\n\nWorkflow:\n1. After pushing code, call with commitSha to find the upload session\n2. Check processingStatus — if \"completed\", results are ready\n3. If \"processing\" or \"pending\", wait and check again\n4. Once completed, use the linked testRunIds with get_test_run_details\n\nReturns (list mode):\n- sessions: Array of upload sessions with processing status\n- pagination: Pagination info\n\nReturns (detail mode):\n- session: Upload session details\n- testRuns: Linked test run summaries (id, framework, pass/fail counts)\n- coverageReports: Linked coverage report summaries (id, format)`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for list_projects tool\n */\nexport const listProjectsInputSchema = {\n organizationId: z\n .string()\n .optional()\n .describe('Filter by organization ID (optional)'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum number of projects to return (default: 50)'),\n}\n\n/**\n * Output schema for list_projects tool\n */\nexport const listProjectsOutputSchema = {\n projects: z.array(z.object({\n id: z.string(),\n name: z.string(),\n description: z.string().nullable().optional(),\n organization: z.object({\n id: z.string(),\n name: z.string(),\n slug: z.string(),\n }),\n })),\n total: z.number(),\n}\n\nexport interface ListProjectsInput {\n organizationId?: string\n limit?: number\n}\n\nexport interface ListProjectsOutput {\n projects: Array<{\n id: string\n name: string\n description?: string | null\n organization: {\n id: string\n name: string\n slug: string\n }\n }>\n total: number\n}\n\n/**\n * Execute list_projects tool\n */\nexport async function executeListProjects(\n client: GafferApiClient,\n input: ListProjectsInput,\n): Promise<ListProjectsOutput> {\n const response = await client.listProjects({\n organizationId: input.organizationId,\n limit: input.limit,\n })\n\n return {\n projects: response.projects.map(p => ({\n id: p.id,\n name: p.name,\n description: p.description,\n organization: p.organization,\n })),\n total: response.pagination.total,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const listProjectsMetadata = {\n name: 'list_projects',\n title: 'List Projects',\n description: `List all projects you have access to.\n\nReturns a list of projects with their IDs, names, and organization info.\nUse this to find project IDs for other tools like get_project_health.\n\nRequires a user API Key (gaf_). Get one from Account Settings in the Gaffer dashboard.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for list_test_runs tool\n */\nexport const listTestRunsInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n commitSha: z\n .string()\n .optional()\n .describe('Filter by commit SHA (exact or prefix match)'),\n branch: z\n .string()\n .optional()\n .describe('Filter by branch name'),\n status: z\n .enum(['passed', 'failed'])\n .optional()\n .describe('Filter by status: \"passed\" (no failures) or \"failed\" (has failures)'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum number of test runs to return (default: 20)'),\n}\n\n/**\n * Output schema for list_test_runs tool\n */\nexport const listTestRunsOutputSchema = {\n testRuns: z.array(z.object({\n id: z.string(),\n commitSha: z.string().optional(),\n branch: z.string().optional(),\n passedCount: z.number(),\n failedCount: z.number(),\n skippedCount: z.number(),\n totalCount: z.number(),\n createdAt: z.string(),\n })),\n pagination: z.object({\n total: z.number(),\n hasMore: z.boolean(),\n }),\n}\n\nexport interface ListTestRunsInput {\n projectId?: string\n commitSha?: string\n branch?: string\n status?: 'passed' | 'failed'\n limit?: number\n}\n\nexport interface ListTestRunsOutput {\n testRuns: Array<{\n id: string\n commitSha?: string\n branch?: string\n passedCount: number\n failedCount: number\n skippedCount: number\n totalCount: number\n createdAt: string\n }>\n pagination: {\n total: number\n hasMore: boolean\n }\n}\n\n/**\n * Execute list_test_runs tool\n */\nexport async function executeListTestRuns(\n client: GafferApiClient,\n input: ListTestRunsInput,\n): Promise<ListTestRunsOutput> {\n const response = await client.getTestRuns({\n projectId: input.projectId,\n commitSha: input.commitSha,\n branch: input.branch,\n status: input.status,\n limit: input.limit || 20,\n })\n\n return {\n testRuns: response.testRuns.map(run => ({\n id: run.id,\n commitSha: run.commitSha || undefined,\n branch: run.branch || undefined,\n passedCount: run.summary.passed,\n failedCount: run.summary.failed,\n skippedCount: run.summary.skipped,\n totalCount: run.summary.total,\n createdAt: run.createdAt,\n })),\n pagination: {\n total: response.pagination.total,\n hasMore: response.pagination.hasMore,\n },\n }\n}\n\n/**\n * Tool metadata\n */\nexport const listTestRunsMetadata = {\n name: 'list_test_runs',\n title: 'List Test Runs',\n description: `List recent test runs for a project with optional filtering.\n\nFilter by:\n- commitSha: Filter by commit SHA (supports prefix matching)\n- branch: Filter by branch name\n- status: Filter by \"passed\" (no failures) or \"failed\" (has failures)\n\nReturns:\n- List of test runs with:\n - id: Test run ID (can be used with get_test_run for details)\n - commitSha: Git commit SHA\n - branch: Git branch name\n - passedCount/failedCount/skippedCount: Test counts\n - createdAt: When the test run was created\n- Pagination info (total count, hasMore flag)\n\nUse cases:\n- \"What tests failed in commit abc123?\"\n- \"Show me recent test runs on main branch\"\n- \"What's the status of tests on my feature branch?\"`,\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { GafferApiClient } from './api-client.js'\nimport {\n compareTestMetricsInputSchema,\n compareTestMetricsMetadata,\n compareTestMetricsOutputSchema,\n executeCompareTestMetrics,\n} from './tools/compare-test-metrics.js'\nimport {\n executeFindUncoveredFailureAreas,\n findUncoveredFailureAreasInputSchema,\n findUncoveredFailureAreasMetadata,\n findUncoveredFailureAreasOutputSchema,\n} from './tools/find-uncovered-failure-areas.js'\nimport {\n executeGetCoverageForFile,\n getCoverageForFileInputSchema,\n getCoverageForFileMetadata,\n getCoverageForFileOutputSchema,\n} from './tools/get-coverage-for-file.js'\nimport {\n executeGetCoverageSummary,\n getCoverageSummaryInputSchema,\n getCoverageSummaryMetadata,\n getCoverageSummaryOutputSchema,\n} from './tools/get-coverage-summary.js'\nimport {\n executeGetFailureClusters,\n getFailureClustersInputSchema,\n getFailureClustersMetadata,\n getFailureClustersOutputSchema,\n} from './tools/get-failure-clusters.js'\nimport {\n executeGetFlakyTests,\n getFlakyTestsInputSchema,\n getFlakyTestsMetadata,\n getFlakyTestsOutputSchema,\n} from './tools/get-flaky-tests.js'\nimport {\n executeGetProjectHealth,\n getProjectHealthInputSchema,\n getProjectHealthMetadata,\n getProjectHealthOutputSchema,\n} from './tools/get-project-health.js'\nimport {\n executeGetReportBrowserUrl,\n getReportBrowserUrlInputSchema,\n getReportBrowserUrlMetadata,\n getReportBrowserUrlOutputSchema,\n} from './tools/get-report-browser-url.js'\nimport {\n executeGetReport,\n getReportInputSchema,\n getReportMetadata,\n getReportOutputSchema,\n} from './tools/get-report.js'\nimport {\n executeGetSlowestTests,\n getSlowestTestsInputSchema,\n getSlowestTestsMetadata,\n getSlowestTestsOutputSchema,\n} from './tools/get-slowest-tests.js'\nimport {\n executeGetTestHistory,\n getTestHistoryInputSchema,\n getTestHistoryMetadata,\n getTestHistoryOutputSchema,\n} from './tools/get-test-history.js'\nimport {\n executeGetTestRunDetails,\n getTestRunDetailsInputSchema,\n getTestRunDetailsMetadata,\n getTestRunDetailsOutputSchema,\n} from './tools/get-test-run-details.js'\nimport {\n executeGetUntestedFiles,\n getUntestedFilesInputSchema,\n getUntestedFilesMetadata,\n getUntestedFilesOutputSchema,\n} from './tools/get-untested-files.js'\nimport {\n executeGetUploadStatus,\n getUploadStatusInputSchema,\n getUploadStatusMetadata,\n getUploadStatusOutputSchema,\n} from './tools/get-upload-status.js'\nimport {\n executeListProjects,\n listProjectsInputSchema,\n listProjectsMetadata,\n listProjectsOutputSchema,\n} from './tools/list-projects.js'\nimport {\n executeListTestRuns,\n listTestRunsInputSchema,\n listTestRunsMetadata,\n listTestRunsOutputSchema,\n} from './tools/list-test-runs.js'\n\n/**\n * Log error to stderr for observability\n * MCP uses stdout for communication, so stderr is safe for logging\n */\nfunction logError(toolName: string, error: unknown): void {\n const timestamp = new Date().toISOString()\n const message = error instanceof Error ? error.message : 'Unknown error'\n const stack = error instanceof Error ? error.stack : undefined\n console.error(`[${timestamp}] [gaffer-mcp] ${toolName} failed: ${message}`)\n if (stack) {\n console.error(stack)\n }\n}\n\n/**\n * Handle tool error: log it and return MCP error response\n */\nfunction handleToolError(toolName: string, error: unknown): { content: { type: 'text', text: string }[], isError: true } {\n logError(toolName, error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return {\n content: [{ type: 'text' as const, text: `Error: ${message}` }],\n isError: true,\n }\n}\n\n/**\n * Tool definition for registration helper.\n * Schema types use `any` to accommodate the MCP SDK's complex Zod type requirements.\n */\ninterface ToolDefinition<TInput, TOutput> {\n metadata: { name: string, title: string, description: string }\n inputSchema: any\n outputSchema: any\n execute: (client: GafferApiClient, input: TInput) => Promise<TOutput>\n}\n\n/**\n * Register a tool with the MCP server using a consistent pattern.\n * Reduces boilerplate by handling error wrapping and response formatting.\n */\nfunction registerTool<TInput, TOutput>(\n server: McpServer,\n client: GafferApiClient,\n tool: ToolDefinition<TInput, TOutput>,\n): void {\n server.registerTool(\n tool.metadata.name,\n {\n title: tool.metadata.title,\n description: tool.metadata.description,\n inputSchema: tool.inputSchema,\n outputSchema: tool.outputSchema,\n },\n async (input: TInput) => {\n try {\n const output = await tool.execute(client, input)\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(output, null, 2) }],\n structuredContent: output as unknown as Record<string, unknown>,\n }\n }\n catch (error) {\n return handleToolError(tool.metadata.name, error)\n }\n },\n )\n}\n\n/**\n * Gaffer MCP Server\n *\n * Provides AI assistants with access to test history and health metrics.\n *\n * Supports two authentication modes:\n * 1. User API Keys (gaf_) - Read-only access to all user's projects\n * Set via GAFFER_API_KEY environment variable\n * 2. Project Upload Tokens (gfr_) - Legacy, single project access\n */\nasync function main() {\n // Validate API key is present\n const apiKey = process.env.GAFFER_API_KEY\n if (!apiKey) {\n console.error('Error: GAFFER_API_KEY environment variable is required')\n console.error('')\n console.error('Get your API Key from: https://app.gaffer.sh/account/api-keys')\n console.error('')\n console.error('Then configure Claude Code or Cursor with:')\n console.error(JSON.stringify({\n mcpServers: {\n gaffer: {\n command: 'npx',\n args: ['-y', '@gaffer-sh/mcp'],\n env: {\n GAFFER_API_KEY: 'gaf_your-api-key-here',\n },\n },\n },\n }, null, 2))\n process.exit(1)\n }\n\n // Create API client\n const client = GafferApiClient.fromEnv()\n\n // Create MCP server\n const server = new McpServer(\n {\n name: 'gaffer',\n version: '0.1.0',\n },\n {\n instructions: `Gaffer provides test analytics and coverage data for your projects.\n\n## Authentication\n\n${client.isUserToken()\n ? 'You have access to multiple projects. Use `list_projects` to find project IDs, then pass `projectId` to all tools.'\n : '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.'}\n\n## Coverage Analysis Best Practices\n\nWhen helping users improve test coverage, combine coverage data with codebase exploration:\n\n1. **Understand code utilization first**: Before targeting files by coverage percentage, explore which code is critical:\n - Find entry points (route definitions, event handlers, exported functions)\n - Find heavily-imported files (files imported by many others are high-value targets)\n - Identify critical business logic (auth, payments, data mutations)\n\n2. **Prioritize by impact**: Low coverage alone doesn't indicate priority. Consider:\n - High utilization + low coverage = highest priority\n - Large files with 0% coverage have bigger impact than small files\n - Use find_uncovered_failure_areas for files with both low coverage AND test failures\n\n3. **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.\n\n4. **Iterate**: Get baseline → identify targets → write tests → re-check coverage after CI uploads new results.\n\n## Finding Invisible Files\n\nCoverage 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.\n\nTo find invisible files:\n1. Use get_coverage_for_file with a path prefix (e.g., \"server/\") to see what Gaffer tracks\n2. Use the local Glob tool to list all source files in that path\n3. Compare the lists - files in local but NOT in Gaffer are invisible\n4. These files need tests that actually import them\n\nExample: 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.\n\n## Agentic CI / Test Failure Diagnosis\n\nWhen helping diagnose CI failures or fix failing tests:\n\n1. **Check flakiness first**: Use get_flaky_tests to identify non-deterministic tests.\n Skip flaky tests unless the user specifically wants to stabilize them.\n\n2. **Get failure details**: Use get_test_run_details with status='failed'\n to see error messages and stack traces for failing tests.\n\n3. **Group by root cause**: Use get_failure_clusters to see which failures\n share the same underlying error — fix the root cause, not individual tests.\n\n4. **Check history**: Use get_test_history to understand if the failure is new\n (regression) or recurring (existing bug).\n\n5. **Verify fixes**: After code changes, use compare_test_metrics to confirm\n the specific test now passes.\n\n6. **Prioritize by risk**: Use find_uncovered_failure_areas to identify\n which failing code has the lowest test coverage — fix those first.\n\n## Checking Upload Status\n\nWhen an agent needs to know if CI results are ready:\n\n1. Use get_upload_status with commitSha or branch to find upload sessions\n2. Check processingStatus: \"completed\" means results are ready, \"processing\" means wait\n3. Once completed, use the linked testRunIds to get test results`,\n },\n )\n\n // Register all tools using the helper\n registerTool(server, client, {\n metadata: getProjectHealthMetadata,\n inputSchema: getProjectHealthInputSchema,\n outputSchema: getProjectHealthOutputSchema,\n execute: executeGetProjectHealth,\n })\n\n registerTool(server, client, {\n metadata: getTestHistoryMetadata,\n inputSchema: getTestHistoryInputSchema,\n outputSchema: getTestHistoryOutputSchema,\n execute: executeGetTestHistory,\n })\n\n registerTool(server, client, {\n metadata: getFlakyTestsMetadata,\n inputSchema: getFlakyTestsInputSchema,\n outputSchema: getFlakyTestsOutputSchema,\n execute: executeGetFlakyTests,\n })\n\n registerTool(server, client, {\n metadata: listTestRunsMetadata,\n inputSchema: listTestRunsInputSchema,\n outputSchema: listTestRunsOutputSchema,\n execute: executeListTestRuns,\n })\n\n if (client.isUserToken()) {\n registerTool(server, client, {\n metadata: listProjectsMetadata,\n inputSchema: listProjectsInputSchema,\n outputSchema: listProjectsOutputSchema,\n execute: executeListProjects,\n })\n }\n\n registerTool(server, client, {\n metadata: getReportMetadata,\n inputSchema: getReportInputSchema,\n outputSchema: getReportOutputSchema,\n execute: executeGetReport,\n })\n\n registerTool(server, client, {\n metadata: getSlowestTestsMetadata,\n inputSchema: getSlowestTestsInputSchema,\n outputSchema: getSlowestTestsOutputSchema,\n execute: executeGetSlowestTests,\n })\n\n registerTool(server, client, {\n metadata: getTestRunDetailsMetadata,\n inputSchema: getTestRunDetailsInputSchema,\n outputSchema: getTestRunDetailsOutputSchema,\n execute: executeGetTestRunDetails,\n })\n\n registerTool(server, client, {\n metadata: getFailureClustersMetadata,\n inputSchema: getFailureClustersInputSchema,\n outputSchema: getFailureClustersOutputSchema,\n execute: executeGetFailureClusters,\n })\n\n registerTool(server, client, {\n metadata: compareTestMetricsMetadata,\n inputSchema: compareTestMetricsInputSchema,\n outputSchema: compareTestMetricsOutputSchema,\n execute: executeCompareTestMetrics,\n })\n\n registerTool(server, client, {\n metadata: getCoverageSummaryMetadata,\n inputSchema: getCoverageSummaryInputSchema,\n outputSchema: getCoverageSummaryOutputSchema,\n execute: executeGetCoverageSummary,\n })\n\n registerTool(server, client, {\n metadata: getCoverageForFileMetadata,\n inputSchema: getCoverageForFileInputSchema,\n outputSchema: getCoverageForFileOutputSchema,\n execute: executeGetCoverageForFile,\n })\n\n registerTool(server, client, {\n metadata: findUncoveredFailureAreasMetadata,\n inputSchema: findUncoveredFailureAreasInputSchema,\n outputSchema: findUncoveredFailureAreasOutputSchema,\n execute: executeFindUncoveredFailureAreas,\n })\n\n registerTool(server, client, {\n metadata: getUntestedFilesMetadata,\n inputSchema: getUntestedFilesInputSchema,\n outputSchema: getUntestedFilesOutputSchema,\n execute: executeGetUntestedFiles,\n })\n\n registerTool(server, client, {\n metadata: getReportBrowserUrlMetadata,\n inputSchema: getReportBrowserUrlInputSchema,\n outputSchema: getReportBrowserUrlOutputSchema,\n execute: executeGetReportBrowserUrl,\n })\n\n registerTool(server, client, {\n metadata: getUploadStatusMetadata,\n inputSchema: getUploadStatusInputSchema,\n outputSchema: getUploadStatusOutputSchema,\n execute: executeGetUploadStatus,\n })\n\n // Connect via stdio transport\n const transport = new StdioServerTransport()\n await server.connect(transport)\n}\n\n// Run the server\nmain().catch((error) => {\n console.error('Fatal error:', error)\n process.exit(1)\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/api-client.ts","../src/tools/compare-test-metrics.ts","../src/tools/find-uncovered-failure-areas.ts","../src/tools/get-coverage-for-file.ts","../src/tools/get-coverage-summary.ts","../src/tools/get-failure-clusters.ts","../src/tools/get-flaky-tests.ts","../src/tools/get-project-health.ts","../src/tools/get-report-browser-url.ts","../src/tools/get-report.ts","../src/tools/get-slowest-tests.ts","../src/tools/get-test-history.ts","../src/tools/get-test-run-details.ts","../src/tools/get-untested-files.ts","../src/tools/get-upload-status.ts","../src/tools/list-projects.ts","../src/tools/list-test-runs.ts","../src/index.ts"],"sourcesContent":["import type {\n AnalyticsResponse,\n ApiErrorResponse,\n BrowserUrlResponse,\n CompareTestResponse,\n CoverageFilesResponse,\n CoverageRiskAreasResponse,\n CoverageSummaryResponse,\n FailureClustersResponse,\n FlakyTestsResponse,\n GafferConfig,\n ProjectsResponse,\n ReportResponse,\n SlowestTestsResponse,\n TestHistoryResponse,\n TestRunDetailsResponse,\n TestRunsResponse,\n UploadSessionDetailResponse,\n UploadSessionsResponse,\n} from './types.js'\nimport { createRequire } from 'node:module'\n\nconst require = createRequire(import.meta.url)\nconst pkg = require('../package.json') as { version: string }\n\n// Request timeout in milliseconds (30 seconds)\nconst REQUEST_TIMEOUT_MS = 30000\n\n// Retry configuration\nconst MAX_RETRIES = 3\nconst INITIAL_RETRY_DELAY_MS = 1000\nconst RETRYABLE_STATUS_CODES = [401, 429, 500, 502, 503, 504]\n\n/**\n * Sleep for a given number of milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\n/**\n * Token type detection based on prefix\n */\nexport type TokenType = 'user' | 'project'\n\n/**\n * Detect token type from prefix\n * - gaf_ = user API Key (read-only, cross-project)\n * - gfr_ = Project Token (single project)\n */\nexport function detectTokenType(token: string): TokenType {\n if (token.startsWith('gaf_')) {\n return 'user'\n }\n return 'project'\n}\n\n/**\n * Gaffer API v1 client for MCP server\n *\n * Supports two authentication modes:\n * 1. User API Keys (gaf_) - Read-only access to all user's projects\n * 2. Project Tokens (gfr_) - Single project access, auto-resolves projectId\n *\n * All methods use the unified /user/projects/:id/ route tree.\n * Project tokens auto-resolve their projectId via /project on first use.\n */\nexport class GafferApiClient {\n private apiKey: string\n private baseUrl: string\n public readonly tokenType: TokenType\n private resolvedProjectId: string | null = null\n\n constructor(config: GafferConfig) {\n this.apiKey = config.apiKey\n this.baseUrl = config.baseUrl.replace(/\\/$/, '') // Remove trailing slash\n this.tokenType = detectTokenType(config.apiKey)\n }\n\n /**\n * Create client from environment variables\n *\n * Supports:\n * - GAFFER_API_KEY (for user API Keys gaf_ or project tokens gfr_)\n */\n static fromEnv(): GafferApiClient {\n const apiKey = process.env.GAFFER_API_KEY\n if (!apiKey) {\n throw new Error('GAFFER_API_KEY environment variable is required')\n }\n\n const baseUrl = process.env.GAFFER_API_URL || 'https://app.gaffer.sh'\n\n return new GafferApiClient({ apiKey, baseUrl })\n }\n\n /**\n * Check if using a user API Key (enables cross-project features)\n */\n isUserToken(): boolean {\n return this.tokenType === 'user'\n }\n\n /**\n * Resolve the project ID for the current token.\n * For project tokens, fetches from /project on first call and caches.\n * For user tokens, requires explicit projectId.\n */\n async resolveProjectId(projectId?: string): Promise<string> {\n if (projectId) {\n return projectId\n }\n\n if (this.isUserToken()) {\n throw new Error('projectId is required when using a user API Key')\n }\n\n // Project token: resolve from /project endpoint (cached)\n if (this.resolvedProjectId) {\n return this.resolvedProjectId\n }\n\n const response = await this.request<{ project: { id: string } }>('/project')\n this.resolvedProjectId = response.project.id\n return this.resolvedProjectId\n }\n\n /**\n * Make authenticated request to Gaffer API with retry logic\n */\n private async request<T>(\n endpoint: string,\n params?: Record<string, string | number>,\n ): Promise<T> {\n const url = new URL(`/api/v1${endpoint}`, this.baseUrl)\n\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value))\n }\n }\n }\n\n let lastError: Error | null = null\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n // Set up request timeout\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)\n\n try {\n const response = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n 'X-API-Key': this.apiKey,\n 'Accept': 'application/json',\n 'User-Agent': `gaffer-mcp/${pkg.version}`,\n },\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({})) as ApiErrorResponse\n\n // Check if we should retry this status code\n if (RETRYABLE_STATUS_CODES.includes(response.status) && attempt < MAX_RETRIES) {\n // For 429 (rate limit), use Retry-After header if available\n let delayMs = INITIAL_RETRY_DELAY_MS * (2 ** attempt)\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After')\n if (retryAfter) {\n delayMs = Math.max(delayMs, Number.parseInt(retryAfter, 10) * 1000)\n }\n }\n lastError = new Error(errorData.error?.message || `API request failed: ${response.status}`)\n await sleep(delayMs)\n continue\n }\n\n const errorMessage = errorData.error?.message || `API request failed: ${response.status}`\n throw new Error(errorMessage)\n }\n\n return response.json() as Promise<T>\n }\n catch (error) {\n clearTimeout(timeoutId)\n\n if (error instanceof Error && error.name === 'AbortError') {\n lastError = new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`)\n if (attempt < MAX_RETRIES) {\n await sleep(INITIAL_RETRY_DELAY_MS * (2 ** attempt))\n continue\n }\n throw lastError\n }\n\n // For network errors, retry\n if (error instanceof TypeError && attempt < MAX_RETRIES) {\n lastError = error\n await sleep(INITIAL_RETRY_DELAY_MS * (2 ** attempt))\n continue\n }\n\n throw error\n }\n finally {\n clearTimeout(timeoutId)\n }\n }\n\n // Should not reach here, but just in case\n throw lastError || new Error('Request failed after retries')\n }\n\n /**\n * List all projects the user has access to.\n * Requires user API Key (gaf_). Not available with project tokens.\n */\n async listProjects(options: {\n organizationId?: string\n limit?: number\n offset?: number\n } = {}): Promise<ProjectsResponse> {\n if (!this.isUserToken()) {\n 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.')\n }\n\n return this.request<ProjectsResponse>('/user/projects', {\n ...(options.organizationId && { organizationId: options.organizationId }),\n ...(options.limit && { limit: options.limit }),\n ...(options.offset && { offset: options.offset }),\n })\n }\n\n /**\n * Get project health analytics\n */\n async getProjectHealth(options: {\n projectId?: string\n days?: number\n } = {}): Promise<AnalyticsResponse> {\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<AnalyticsResponse>(`/user/projects/${projectId}/health`, {\n days: options.days || 30,\n })\n }\n\n /**\n * Get test history for a specific test\n */\n async getTestHistory(options: {\n projectId?: string\n testName?: string\n filePath?: string\n limit?: number\n }): Promise<TestHistoryResponse> {\n const testName = options.testName?.trim()\n const filePath = options.filePath?.trim()\n\n if (!testName && !filePath) {\n throw new Error('Either testName or filePath is required (and must not be empty)')\n }\n\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<TestHistoryResponse>(`/user/projects/${projectId}/test-history`, {\n ...(testName && { testName }),\n ...(filePath && { filePath }),\n ...(options.limit && { limit: options.limit }),\n })\n }\n\n /**\n * Get flaky tests for the project\n */\n async getFlakyTests(options: {\n projectId?: string\n threshold?: number\n limit?: number\n days?: number\n } = {}): Promise<FlakyTestsResponse> {\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<FlakyTestsResponse>(`/user/projects/${projectId}/flaky-tests`, {\n ...(options.threshold && { threshold: options.threshold }),\n ...(options.limit && { limit: options.limit }),\n ...(options.days && { days: options.days }),\n })\n }\n\n /**\n * List test runs for the project\n */\n async getTestRuns(options: {\n projectId?: string\n commitSha?: string\n branch?: string\n status?: 'passed' | 'failed'\n limit?: number\n } = {}): Promise<TestRunsResponse> {\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<TestRunsResponse>(`/user/projects/${projectId}/test-runs`, {\n ...(options.commitSha && { commitSha: options.commitSha }),\n ...(options.branch && { branch: options.branch }),\n ...(options.status && { status: options.status }),\n ...(options.limit && { limit: options.limit }),\n })\n }\n\n /**\n * Get report files for a test run\n */\n async getReport(testRunId: string): Promise<ReportResponse> {\n if (!this.isUserToken()) {\n throw new Error('getReport requires a user API Key (gaf_). Project tokens (gfr_) cannot access reports via API.')\n }\n\n if (!testRunId) {\n throw new Error('testRunId is required')\n }\n\n return this.request<ReportResponse>(`/user/test-runs/${testRunId}/report`)\n }\n\n /**\n * Get slowest tests for a project\n */\n async getSlowestTests(options: {\n projectId?: string\n days?: number\n limit?: number\n framework?: string\n branch?: string\n }): Promise<SlowestTestsResponse> {\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<SlowestTestsResponse>(`/user/projects/${projectId}/slowest-tests`, {\n ...(options.days && { days: options.days }),\n ...(options.limit && { limit: options.limit }),\n ...(options.framework && { framework: options.framework }),\n ...(options.branch && { branch: options.branch }),\n })\n }\n\n /**\n * Get parsed test results for a specific test run\n */\n async getTestRunDetails(options: {\n projectId?: string\n testRunId: string\n status?: 'passed' | 'failed' | 'skipped'\n limit?: number\n offset?: number\n }): Promise<TestRunDetailsResponse> {\n if (!options.testRunId) {\n throw new Error('testRunId is required')\n }\n\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<TestRunDetailsResponse>(\n `/user/projects/${projectId}/test-runs/${options.testRunId}/details`,\n {\n ...(options.status && { status: options.status }),\n ...(options.limit && { limit: options.limit }),\n ...(options.offset && { offset: options.offset }),\n },\n )\n }\n\n /**\n * Compare test metrics between two commits or test runs\n */\n async compareTestMetrics(options: {\n projectId?: string\n testName: string\n beforeCommit?: string\n afterCommit?: string\n beforeRunId?: string\n afterRunId?: string\n }): Promise<CompareTestResponse> {\n if (!options.testName) {\n throw new Error('testName is required')\n }\n\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<CompareTestResponse>(\n `/user/projects/${projectId}/compare-test`,\n {\n testName: options.testName,\n ...(options.beforeCommit && { beforeCommit: options.beforeCommit }),\n ...(options.afterCommit && { afterCommit: options.afterCommit }),\n ...(options.beforeRunId && { beforeRunId: options.beforeRunId }),\n ...(options.afterRunId && { afterRunId: options.afterRunId }),\n },\n )\n }\n\n /**\n * Get coverage summary for a project\n */\n async getCoverageSummary(options: {\n projectId?: string\n days?: number\n }): Promise<CoverageSummaryResponse> {\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<CoverageSummaryResponse>(\n `/user/projects/${projectId}/coverage-summary`,\n {\n ...(options.days && { days: options.days }),\n },\n )\n }\n\n /**\n * Get coverage files for a project with filtering\n */\n async getCoverageFiles(options: {\n projectId?: string\n filePath?: string\n minCoverage?: number\n maxCoverage?: number\n limit?: number\n offset?: number\n sortBy?: 'path' | 'coverage'\n sortOrder?: 'asc' | 'desc'\n }): Promise<CoverageFilesResponse> {\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<CoverageFilesResponse>(\n `/user/projects/${projectId}/coverage/files`,\n {\n ...(options.filePath && { filePath: options.filePath }),\n ...(options.minCoverage !== undefined && { minCoverage: options.minCoverage }),\n ...(options.maxCoverage !== undefined && { maxCoverage: options.maxCoverage }),\n ...(options.limit && { limit: options.limit }),\n ...(options.offset && { offset: options.offset }),\n ...(options.sortBy && { sortBy: options.sortBy }),\n ...(options.sortOrder && { sortOrder: options.sortOrder }),\n },\n )\n }\n\n /**\n * Get risk areas (files with low coverage AND test failures)\n */\n async getCoverageRiskAreas(options: {\n projectId?: string\n days?: number\n coverageThreshold?: number\n }): Promise<CoverageRiskAreasResponse> {\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<CoverageRiskAreasResponse>(\n `/user/projects/${projectId}/coverage/risk-areas`,\n {\n ...(options.days && { days: options.days }),\n ...(options.coverageThreshold !== undefined && { coverageThreshold: options.coverageThreshold }),\n },\n )\n }\n\n /**\n * Get a browser-navigable URL for viewing a test report\n */\n async getReportBrowserUrl(options: {\n projectId?: string\n testRunId: string\n filename?: string\n }): Promise<BrowserUrlResponse> {\n if (!options.testRunId) {\n throw new Error('testRunId is required')\n }\n\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<BrowserUrlResponse>(\n `/user/projects/${projectId}/reports/${options.testRunId}/browser-url`,\n {\n ...(options.filename && { filename: options.filename }),\n },\n )\n }\n\n /**\n * Get failure clusters for a test run\n */\n async getFailureClusters(options: {\n projectId?: string\n testRunId: string\n }): Promise<FailureClustersResponse> {\n if (!options.testRunId) {\n throw new Error('testRunId is required')\n }\n\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<FailureClustersResponse>(\n `/user/projects/${projectId}/test-runs/${options.testRunId}/failure-clusters`,\n )\n }\n\n /**\n * List upload sessions for a project\n */\n async listUploadSessions(options: {\n projectId?: string\n commitSha?: string\n branch?: string\n limit?: number\n offset?: number\n }): Promise<UploadSessionsResponse> {\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<UploadSessionsResponse>(\n `/user/projects/${projectId}/upload-sessions`,\n {\n ...(options.commitSha && { commitSha: options.commitSha }),\n ...(options.branch && { branch: options.branch }),\n ...(options.limit && { limit: options.limit }),\n ...(options.offset && { offset: options.offset }),\n },\n )\n }\n\n /**\n * Get upload session detail with linked results\n */\n async getUploadSessionDetail(options: {\n projectId?: string\n sessionId: string\n }): Promise<UploadSessionDetailResponse> {\n if (!options.sessionId) {\n throw new Error('sessionId is required')\n }\n\n const projectId = await this.resolveProjectId(options.projectId)\n return this.request<UploadSessionDetailResponse>(\n `/user/projects/${projectId}/upload-sessions/${options.sessionId}`,\n )\n }\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport type { CompareTestResponse } from '../types.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for compare_test_metrics tool\n */\nexport const compareTestMetricsInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n testName: z\n .string()\n .describe('The test name to compare. Can be the short name or full name including describe blocks.'),\n beforeCommit: z\n .string()\n .optional()\n .describe('Commit SHA for the \"before\" measurement. Use with afterCommit.'),\n afterCommit: z\n .string()\n .optional()\n .describe('Commit SHA for the \"after\" measurement. Use with beforeCommit.'),\n beforeRunId: z\n .string()\n .optional()\n .describe('Test run ID for the \"before\" measurement. Use with afterRunId.'),\n afterRunId: z\n .string()\n .optional()\n .describe('Test run ID for the \"after\" measurement. Use with beforeRunId.'),\n}\n\n/**\n * Output schema for compare_test_metrics tool\n */\nexport const compareTestMetricsOutputSchema = {\n testName: z.string(),\n before: z.object({\n testRunId: z.string(),\n commit: z.string().nullable(),\n branch: z.string().nullable(),\n status: z.enum(['passed', 'failed', 'skipped']),\n durationMs: z.number().nullable(),\n createdAt: z.string(),\n }),\n after: z.object({\n testRunId: z.string(),\n commit: z.string().nullable(),\n branch: z.string().nullable(),\n status: z.enum(['passed', 'failed', 'skipped']),\n durationMs: z.number().nullable(),\n createdAt: z.string(),\n }),\n change: z.object({\n durationMs: z.number().nullable(),\n percentChange: z.number().nullable(),\n statusChanged: z.boolean(),\n }),\n}\n\nexport interface CompareTestMetricsInput {\n projectId?: string\n testName: string\n beforeCommit?: string\n afterCommit?: string\n beforeRunId?: string\n afterRunId?: string\n}\n\n// Re-export response type from types.ts for convenience\nexport type CompareTestMetricsOutput = CompareTestResponse\n\n/**\n * Execute compare_test_metrics tool\n */\nexport async function executeCompareTestMetrics(\n client: GafferApiClient,\n input: CompareTestMetricsInput,\n): Promise<CompareTestMetricsOutput> {\n // Validate input - check for presence of required pairs\n const hasCommits = input.beforeCommit && input.afterCommit\n const hasRunIds = input.beforeRunId && input.afterRunId\n\n if (!hasCommits && !hasRunIds) {\n throw new Error('Must provide either (beforeCommit + afterCommit) or (beforeRunId + afterRunId)')\n }\n\n // Validate non-empty strings\n if (hasCommits) {\n if (input.beforeCommit!.trim().length === 0 || input.afterCommit!.trim().length === 0) {\n throw new Error('beforeCommit and afterCommit must not be empty strings')\n }\n }\n\n if (hasRunIds) {\n if (input.beforeRunId!.trim().length === 0 || input.afterRunId!.trim().length === 0) {\n throw new Error('beforeRunId and afterRunId must not be empty strings')\n }\n }\n\n const response = await client.compareTestMetrics({\n projectId: input.projectId,\n testName: input.testName,\n beforeCommit: input.beforeCommit,\n afterCommit: input.afterCommit,\n beforeRunId: input.beforeRunId,\n afterRunId: input.afterRunId,\n })\n\n return response\n}\n\n/**\n * Tool metadata\n */\nexport const compareTestMetricsMetadata = {\n name: 'compare_test_metrics',\n title: 'Compare Test Metrics',\n description: `Compare test metrics between two commits or test runs.\n\nUseful for measuring the impact of code changes on test performance or reliability.\n\nParameters:\n- projectId (required): Project ID\n- testName (required): The test name to compare (short name or full name)\n- Option 1 - Compare by commit:\n - beforeCommit: Commit SHA for \"before\" measurement\n - afterCommit: Commit SHA for \"after\" measurement\n- Option 2 - Compare by test run:\n - beforeRunId: Test run ID for \"before\" measurement\n - afterRunId: Test run ID for \"after\" measurement\n\nReturns:\n- testName: The test that was compared\n- before: Metrics from the before commit/run\n - testRunId, commit, branch, status, durationMs, createdAt\n- after: Metrics from the after commit/run\n - testRunId, commit, branch, status, durationMs, createdAt\n- change: Calculated changes\n - durationMs: Duration difference (negative = faster)\n - percentChange: Percentage change (negative = improvement)\n - statusChanged: Whether pass/fail status changed\n\nUse cases:\n- \"Did my fix make this test faster?\"\n- \"Compare test performance between these two commits\"\n- \"Did this test start failing after my changes?\"\n- \"Show me the before/after for the slow test I optimized\"\n\nTip: Use get_test_history first to find the commit SHAs or test run IDs you want to compare.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for find_uncovered_failure_areas tool\n */\nexport const findUncoveredFailureAreasInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n days: z\n .number()\n .int()\n .min(1)\n .max(365)\n .optional()\n .describe('Number of days to analyze for test failures (default: 30)'),\n coverageThreshold: z\n .number()\n .min(0)\n .max(100)\n .optional()\n .describe('Include files with coverage below this percentage (default: 80)'),\n}\n\n/**\n * Output schema for find_uncovered_failure_areas tool\n */\nexport const findUncoveredFailureAreasOutputSchema = {\n hasCoverage: z.boolean(),\n hasTestResults: z.boolean(),\n riskAreas: z.array(z.object({\n filePath: z.string(),\n coverage: z.number(),\n failureCount: z.number(),\n riskScore: z.number(),\n testNames: z.array(z.string()),\n })),\n message: z.string().optional(),\n}\n\nexport interface FindUncoveredFailureAreasInput {\n projectId?: string\n days?: number\n coverageThreshold?: number\n}\n\nexport interface FindUncoveredFailureAreasOutput {\n hasCoverage: boolean\n hasTestResults: boolean\n riskAreas: Array<{\n filePath: string\n coverage: number\n failureCount: number\n riskScore: number\n testNames: string[]\n }>\n message?: string\n}\n\n/**\n * Execute find_uncovered_failure_areas tool\n */\nexport async function executeFindUncoveredFailureAreas(\n client: GafferApiClient,\n input: FindUncoveredFailureAreasInput,\n): Promise<FindUncoveredFailureAreasOutput> {\n const response = await client.getCoverageRiskAreas({\n projectId: input.projectId,\n days: input.days,\n coverageThreshold: input.coverageThreshold,\n })\n\n return {\n hasCoverage: response.hasCoverage,\n hasTestResults: response.hasTestResults,\n riskAreas: response.riskAreas,\n message: response.message,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const findUncoveredFailureAreasMetadata = {\n name: 'find_uncovered_failure_areas',\n title: 'Find Uncovered Failure Areas',\n description: `Find areas of code that have both low coverage AND test failures.\n\nThis cross-references test failures with coverage data to identify high-risk\nareas in your codebase that need attention. Files are ranked by a \"risk score\"\ncalculated as: (100 - coverage%) × failureCount.\n\nParameters:\n- projectId: The project to analyze (required)\n- days: Analysis period for test failures (default: 30)\n- coverageThreshold: Include files below this coverage % (default: 80)\n\nReturns:\n- List of risk areas sorted by risk score (highest risk first)\n- Each area includes: file path, coverage %, failure count, risk score, test names\n\nUse this to prioritize which parts of your codebase need better test coverage.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_coverage_for_file tool\n */\nexport const getCoverageForFileInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n filePath: z\n .string()\n .describe('File path to get coverage for. Can be exact path or partial match.'),\n}\n\n/**\n * Output schema for get_coverage_for_file tool\n */\nexport const getCoverageForFileOutputSchema = {\n hasCoverage: z.boolean(),\n files: z.array(z.object({\n path: z.string(),\n lines: z.object({\n covered: z.number(),\n total: z.number(),\n percentage: z.number(),\n }),\n branches: z.object({\n covered: z.number(),\n total: z.number(),\n percentage: z.number(),\n }),\n functions: z.object({\n covered: z.number(),\n total: z.number(),\n percentage: z.number(),\n }),\n })),\n message: z.string().optional(),\n}\n\nexport interface GetCoverageForFileInput {\n projectId?: string\n filePath: string\n}\n\nexport interface GetCoverageForFileOutput {\n hasCoverage: boolean\n files: Array<{\n path: string\n lines: { covered: number, total: number, percentage: number }\n branches: { covered: number, total: number, percentage: number }\n functions: { covered: number, total: number, percentage: number }\n }>\n message?: string\n}\n\n/**\n * Execute get_coverage_for_file tool\n */\nexport async function executeGetCoverageForFile(\n client: GafferApiClient,\n input: GetCoverageForFileInput,\n): Promise<GetCoverageForFileOutput> {\n const response = await client.getCoverageFiles({\n projectId: input.projectId,\n filePath: input.filePath,\n limit: 10, // Return up to 10 matching files\n })\n\n return {\n hasCoverage: response.hasCoverage,\n files: response.files.map(f => ({\n path: f.path,\n lines: f.lines,\n branches: f.branches,\n functions: f.functions,\n })),\n message: response.message,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getCoverageForFileMetadata = {\n name: 'get_coverage_for_file',\n title: 'Get Coverage for File',\n description: `Get coverage metrics for a specific file or files matching a path pattern.\n\nParameters:\n- projectId: The project to query (required)\n- filePath: File path to search for (exact or partial match)\n\nReturns:\n- Line coverage (covered/total/percentage)\n- Branch coverage (covered/total/percentage)\n- Function coverage (covered/total/percentage)\n\nThis is the preferred tool for targeted coverage analysis. Use path prefixes to focus on\nspecific areas of the codebase:\n- \"server/services\" - Backend service layer\n- \"server/utils\" - Backend utilities\n- \"src/api\" - API routes\n- \"lib/core\" - Core business logic\n\nBefore querying, explore the codebase to identify critical paths - entry points,\nheavily-imported files, and code handling auth/payments/data mutations.\nPrioritize: high utilization + low coverage = highest impact.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_coverage_summary tool\n */\nexport const getCoverageSummaryInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n days: z\n .number()\n .int()\n .min(1)\n .max(365)\n .optional()\n .describe('Number of days to analyze for trends (default: 30)'),\n}\n\n/**\n * Output schema for get_coverage_summary tool\n */\nexport const getCoverageSummaryOutputSchema = {\n hasCoverage: z.boolean(),\n current: z.object({\n lines: z.number(),\n branches: z.number(),\n functions: z.number(),\n }).optional(),\n trend: z.object({\n direction: z.enum(['up', 'down', 'stable']),\n change: z.number(),\n }).optional(),\n totalReports: z.number(),\n latestReportDate: z.string().nullable().optional(),\n lowestCoverageFiles: z.array(z.object({\n path: z.string(),\n coverage: z.number(),\n })).optional(),\n message: z.string().optional(),\n}\n\nexport interface GetCoverageSummaryInput {\n projectId?: string\n days?: number\n}\n\nexport interface GetCoverageSummaryOutput {\n hasCoverage: boolean\n current?: {\n lines: number\n branches: number\n functions: number\n }\n trend?: {\n direction: 'up' | 'down' | 'stable'\n change: number\n }\n totalReports: number\n latestReportDate?: string | null\n lowestCoverageFiles?: Array<{\n path: string\n coverage: number\n }>\n message?: string\n}\n\n/**\n * Execute get_coverage_summary tool\n */\nexport async function executeGetCoverageSummary(\n client: GafferApiClient,\n input: GetCoverageSummaryInput,\n): Promise<GetCoverageSummaryOutput> {\n const response = await client.getCoverageSummary({\n projectId: input.projectId,\n days: input.days,\n })\n\n return {\n hasCoverage: response.hasCoverage,\n current: response.current,\n trend: response.trend,\n totalReports: response.totalReports,\n latestReportDate: response.latestReportDate,\n lowestCoverageFiles: response.lowestCoverageFiles,\n message: response.message,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getCoverageSummaryMetadata = {\n name: 'get_coverage_summary',\n title: 'Get Coverage Summary',\n description: `Get the coverage metrics summary for a project.\n\nReturns:\n- Current coverage percentages (lines, branches, functions)\n- Trend direction (up, down, stable) and change amount\n- Total number of coverage reports\n- Latest report date\n- Top 5 files with lowest coverage\n\nUse this to understand your project's overall test coverage health.\n\nAfter getting the summary, use get_coverage_for_file with path prefixes to drill into\nspecific areas (e.g., \"server/services\", \"src/api\", \"lib/core\"). This helps identify\nhigh-value targets in critical code paths rather than just the files with lowest coverage.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport type { FailureClustersResponse } from '../types.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_failure_clusters tool\n */\nexport const getFailureClustersInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n testRunId: z\n .string()\n .describe('Test run ID. Use list_test_runs to find test run IDs.'),\n}\n\n/**\n * Output schema for get_failure_clusters tool\n */\nexport const getFailureClustersOutputSchema = {\n clusters: z.array(z.object({\n representativeError: z.string(),\n count: z.number(),\n tests: z.array(z.object({\n name: z.string(),\n fullName: z.string(),\n errorMessage: z.string(),\n filePath: z.string().nullable(),\n })),\n similarity: z.number(),\n })),\n totalFailures: z.number(),\n}\n\nexport interface GetFailureClustersInput {\n projectId?: string\n testRunId: string\n}\n\n/**\n * Execute get_failure_clusters tool\n */\nexport async function executeGetFailureClusters(\n client: GafferApiClient,\n input: GetFailureClustersInput,\n): Promise<FailureClustersResponse> {\n return client.getFailureClusters({\n projectId: input.projectId,\n testRunId: input.testRunId,\n })\n}\n\n/**\n * Tool metadata\n */\nexport const getFailureClustersMetadata = {\n name: 'get_failure_clusters',\n title: 'Get Failure Clusters',\n description: `Group failed tests by root cause using error message similarity.\n\nParameters:\n- projectId (required): The project ID\n- testRunId (required): The test run ID to analyze\n\nReturns:\n- clusters: Array of failure clusters, each containing:\n - representativeError: The error message representing this cluster\n - count: Number of tests with this same root cause\n - tests: Array of individual failed tests in this cluster\n - name: Short test name\n - fullName: Full test name including describe blocks\n - errorMessage: The specific error message\n - filePath: Test file path (null if not recorded)\n - similarity: Similarity threshold used for clustering (0-1)\n- totalFailures: Total number of failed tests across all clusters\n\nUse cases:\n- \"Group these 15 failures by root cause\" — often reveals 2-3 distinct bugs\n- \"Which error affects the most tests?\" — fix the largest cluster first\n- \"Are these failures related?\" — check if they land in the same cluster\n\nTip: Use get_test_run_details with status='failed' first to see raw failures,\nthen use this tool to understand which failures share the same root cause.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_flaky_tests tool\n */\nexport const getFlakyTestsInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n threshold: z\n .number()\n .min(0)\n .max(1)\n .optional()\n .describe('Minimum flip rate to be considered flaky (0-1, default: 0.1 = 10%)'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum number of flaky tests to return (default: 50)'),\n days: z\n .number()\n .int()\n .min(1)\n .max(365)\n .optional()\n .describe('Analysis period in days (default: 30)'),\n}\n\n/**\n * Output schema for get_flaky_tests tool\n */\nexport const getFlakyTestsOutputSchema = {\n flakyTests: z.array(z.object({\n name: z.string(),\n flipRate: z.number(),\n flipCount: z.number(),\n totalRuns: z.number(),\n lastSeen: z.string(),\n flakinessScore: z.number(),\n })),\n summary: z.object({\n threshold: z.number(),\n totalFlaky: z.number(),\n period: z.number(),\n }),\n}\n\nexport interface GetFlakyTestsInput {\n projectId?: string\n threshold?: number\n limit?: number\n days?: number\n}\n\nexport interface GetFlakyTestsOutput {\n flakyTests: Array<{\n name: string\n flipRate: number\n flipCount: number\n totalRuns: number\n lastSeen: string\n flakinessScore: number\n }>\n summary: {\n threshold: number\n totalFlaky: number\n period: number\n }\n}\n\n/**\n * Execute get_flaky_tests tool\n */\nexport async function executeGetFlakyTests(\n client: GafferApiClient,\n input: GetFlakyTestsInput,\n): Promise<GetFlakyTestsOutput> {\n const response = await client.getFlakyTests({\n projectId: input.projectId,\n threshold: input.threshold,\n limit: input.limit,\n days: input.days,\n })\n\n return {\n flakyTests: response.flakyTests,\n summary: response.summary,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getFlakyTestsMetadata = {\n name: 'get_flaky_tests',\n title: 'Get Flaky Tests',\n description: `Get the list of flaky tests in a project.\n\nA test is considered flaky if it frequently switches between pass and fail states.\nTests are ranked by a composite flakinessScore that factors in flip behavior,\nfailure rate, and duration variability.\n\nReturns:\n- List of flaky tests sorted by flakinessScore (most flaky first), with:\n - name: Test name\n - flipRate: How often the test flips between pass/fail (0-1)\n - flipCount: Number of status transitions\n - totalRuns: Total test executions analyzed\n - lastSeen: When the test last ran\n - flakinessScore: Composite score (0-1) combining flip proximity, failure rate, and duration variability\n- Summary with threshold used and total count\n\nUse this after get_project_health shows flaky tests exist, to identify which\nspecific tests are flaky and need investigation.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_project_health tool\n */\nexport const getProjectHealthInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n days: z\n .number()\n .int()\n .min(1)\n .max(365)\n .optional()\n .describe('Number of days to analyze (default: 30)'),\n}\n\n/**\n * Output schema for get_project_health tool\n */\nexport const getProjectHealthOutputSchema = {\n projectName: z.string(),\n healthScore: z.number(),\n passRate: z.number().nullable(),\n testRunCount: z.number(),\n flakyTestCount: z.number(),\n trend: z.enum(['up', 'down', 'stable']),\n period: z.object({\n days: z.number(),\n start: z.string(),\n end: z.string(),\n }),\n}\n\nexport interface GetProjectHealthInput {\n projectId?: string\n days?: number\n}\n\nexport interface GetProjectHealthOutput {\n projectName: string\n healthScore: number\n passRate: number | null\n testRunCount: number\n flakyTestCount: number\n trend: 'up' | 'down' | 'stable'\n period: {\n days: number\n start: string\n end: string\n }\n}\n\n/**\n * Execute get_project_health tool\n */\nexport async function executeGetProjectHealth(\n client: GafferApiClient,\n input: GetProjectHealthInput,\n): Promise<GetProjectHealthOutput> {\n const response = await client.getProjectHealth({\n projectId: input.projectId,\n days: input.days,\n })\n\n return {\n projectName: response.analytics.projectName,\n healthScore: response.analytics.healthScore,\n passRate: response.analytics.passRate,\n testRunCount: response.analytics.testRunCount,\n flakyTestCount: response.analytics.flakyTestCount,\n trend: response.analytics.trend,\n period: response.analytics.period,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getProjectHealthMetadata = {\n name: 'get_project_health',\n title: 'Get Project Health',\n description: `Get the health metrics for a project.\n\nReturns:\n- Health score (0-100): Overall project health based on pass rate and trend\n- Pass rate: Percentage of tests passing\n- Test run count: Number of test runs in the period\n- Flaky test count: Number of tests with inconsistent results\n- Trend: Whether test health is improving (up), declining (down), or stable\n\nUse this to understand the current state of your test suite.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_report_browser_url tool\n */\nexport const getReportBrowserUrlInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n testRunId: z\n .string()\n .describe('The test run ID to get the report URL for. Use list_test_runs to find test run IDs.'),\n filename: z\n .string()\n .optional()\n .describe('Specific file to open (default: index.html or first HTML file)'),\n}\n\n/**\n * Output schema for get_report_browser_url tool\n */\nexport const getReportBrowserUrlOutputSchema = {\n url: z.string(),\n filename: z.string(),\n testRunId: z.string(),\n expiresAt: z.string(),\n expiresInSeconds: z.number(),\n}\n\nexport interface GetReportBrowserUrlInput {\n projectId?: string\n testRunId: string\n filename?: string\n}\n\nexport interface GetReportBrowserUrlOutput {\n url: string\n filename: string\n testRunId: string\n expiresAt: string\n expiresInSeconds: number\n}\n\n/**\n * Execute get_report_browser_url tool\n */\nexport async function executeGetReportBrowserUrl(\n client: GafferApiClient,\n input: GetReportBrowserUrlInput,\n): Promise<GetReportBrowserUrlOutput> {\n const response = await client.getReportBrowserUrl({\n projectId: input.projectId,\n testRunId: input.testRunId,\n filename: input.filename,\n })\n\n return {\n url: response.url,\n filename: response.filename,\n testRunId: response.testRunId,\n expiresAt: response.expiresAt,\n expiresInSeconds: response.expiresInSeconds,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getReportBrowserUrlMetadata = {\n name: 'get_report_browser_url',\n title: 'Get Report Browser URL',\n description: `Get a browser-navigable URL for viewing a test report (Playwright, Vitest, etc.).\n\nReturns a signed URL that can be opened directly in a browser without requiring\nthe user to log in. The URL expires after 30 minutes for security.\n\nParameters:\n- projectId: The project the test run belongs to (required)\n- testRunId: The test run to view (required)\n- filename: Specific file to open (optional, defaults to index.html)\n\nReturns:\n- url: Browser-navigable URL with signed token\n- filename: The file being accessed\n- expiresAt: ISO timestamp when the URL expires\n- expiresInSeconds: Time until expiration\n\nThe returned URL can be shared with users who need to view the report.\nNote: URLs expire after 30 minutes for security.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport type { ReportFile, ReportResponse } from '../types.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_report tool\n */\nexport const getReportInputSchema = {\n testRunId: z\n .string()\n .describe('The test run ID to get report files for. Use list_test_runs to find test run IDs.'),\n}\n\n/**\n * Output schema for get_report tool\n */\nexport const getReportOutputSchema = {\n testRunId: z.string(),\n projectId: z.string(),\n projectName: z.string(),\n resultSchema: z.string().optional(),\n files: z.array(z.object({\n filename: z.string(),\n size: z.number(),\n contentType: z.string(),\n downloadUrl: z.string(),\n })),\n urlExpiresInSeconds: z.number().optional(),\n}\n\nexport interface GetReportInput {\n testRunId: string\n}\n\n// Re-export types from types.ts for convenience\nexport type { ReportFile, ReportResponse }\n\n// Output type matches ReportResponse\nexport type GetReportOutput = ReportResponse\n\n/**\n * Execute get_report tool\n */\nexport async function executeGetReport(\n client: GafferApiClient,\n input: GetReportInput,\n): Promise<GetReportOutput> {\n const response = await client.getReport(input.testRunId)\n\n return {\n testRunId: response.testRunId,\n projectId: response.projectId,\n projectName: response.projectName,\n resultSchema: response.resultSchema,\n files: response.files.map(file => ({\n filename: file.filename,\n size: file.size,\n contentType: file.contentType,\n downloadUrl: file.downloadUrl,\n })),\n urlExpiresInSeconds: response.urlExpiresInSeconds,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getReportMetadata = {\n name: 'get_report',\n title: 'Get Report Files',\n description: `Get URLs for report files uploaded with a test run.\n\nIMPORTANT: This tool returns download URLs, not file content. You must fetch the URLs separately.\n\nReturns for each file:\n- filename: The file name (e.g., \"report.html\", \"results.json\", \"junit.xml\")\n- size: File size in bytes\n- contentType: MIME type (e.g., \"text/html\", \"application/json\", \"application/xml\")\n- downloadUrl: Presigned URL to download the file (valid for ~5 minutes)\n\nHow to use the returned URLs:\n\n1. **JSON files** (results.json, coverage.json):\n Use WebFetch with the downloadUrl to retrieve and parse the JSON content.\n Example: WebFetch(url=downloadUrl, prompt=\"Extract test results from this JSON\")\n\n2. **XML files** (junit.xml, xunit.xml):\n Use WebFetch with the downloadUrl to retrieve and parse the XML content.\n Example: WebFetch(url=downloadUrl, prompt=\"Parse the test results from this JUnit XML\")\n\n3. **HTML reports** (Playwright, pytest-html, Vitest):\n These are typically bundled React/JavaScript applications that require a browser.\n They cannot be meaningfully parsed by WebFetch.\n For programmatic analysis, use get_test_run_details instead.\n\nRecommendations:\n- For analyzing test results programmatically: Use get_test_run_details (returns parsed test data)\n- For JSON/XML files: Use this tool + WebFetch on the downloadUrl\n- For HTML reports: Direct users to view in browser, or use get_test_run_details\n\nUse cases:\n- \"What files are in this test run?\" (list available reports)\n- \"Get the coverage data from this run\" (then WebFetch the JSON URL)\n- \"Parse the JUnit XML results\" (then WebFetch the XML URL)`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport type { SlowestTestEntry, SlowestTestsResponse } from '../types.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_slowest_tests tool\n */\nexport const getSlowestTestsInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n days: z\n .number()\n .int()\n .min(1)\n .max(365)\n .optional()\n .describe('Analysis period in days (default: 30)'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum number of tests to return (default: 20)'),\n framework: z\n .string()\n .optional()\n .describe('Filter by test framework (e.g., \"playwright\", \"vitest\", \"jest\")'),\n branch: z\n .string()\n .optional()\n .describe('Filter by git branch name (e.g., \"main\", \"develop\")'),\n}\n\n/**\n * Output schema for get_slowest_tests tool\n */\nexport const getSlowestTestsOutputSchema = {\n slowestTests: z.array(z.object({\n name: z.string(),\n fullName: z.string(),\n filePath: z.string().optional(),\n framework: z.string().optional(),\n avgDurationMs: z.number(),\n p95DurationMs: z.number(),\n runCount: z.number(),\n })),\n summary: z.object({\n projectId: z.string(),\n projectName: z.string(),\n period: z.number(),\n totalReturned: z.number(),\n }),\n}\n\nexport interface GetSlowestTestsInput {\n projectId?: string\n days?: number\n limit?: number\n framework?: string\n branch?: string\n}\n\n// Re-export types from types.ts for convenience\nexport type { SlowestTestEntry, SlowestTestsResponse }\n\n// Output type matches SlowestTestsResponse\nexport type GetSlowestTestsOutput = SlowestTestsResponse\n\n/**\n * Execute get_slowest_tests tool\n */\nexport async function executeGetSlowestTests(\n client: GafferApiClient,\n input: GetSlowestTestsInput,\n): Promise<GetSlowestTestsOutput> {\n const response = await client.getSlowestTests({\n projectId: input.projectId,\n days: input.days,\n limit: input.limit,\n framework: input.framework,\n branch: input.branch,\n })\n\n return {\n slowestTests: response.slowestTests.map(test => ({\n name: test.name,\n fullName: test.fullName,\n filePath: test.filePath,\n framework: test.framework,\n avgDurationMs: test.avgDurationMs,\n p95DurationMs: test.p95DurationMs,\n runCount: test.runCount,\n })),\n summary: response.summary,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getSlowestTestsMetadata = {\n name: 'get_slowest_tests',\n title: 'Get Slowest Tests',\n description: `Get the slowest tests in a project, sorted by P95 duration.\n\nParameters:\n- projectId (required): Project ID to analyze\n- days (optional): Analysis period in days (default: 30, max: 365)\n- limit (optional): Max tests to return (default: 20, max: 100)\n- framework (optional): Filter by framework (e.g., \"playwright\", \"vitest\")\n- branch (optional): Filter by git branch (e.g., \"main\", \"develop\")\n\nReturns:\n- List of slowest tests with:\n - name: Short test name\n - fullName: Full test name including describe blocks\n - filePath: Test file path (if available)\n - framework: Test framework used\n - avgDurationMs: Average test duration in milliseconds\n - p95DurationMs: 95th percentile duration (used for sorting)\n - runCount: Number of times the test ran in the period\n- Summary with project info and period\n\nUse cases:\n- \"Which tests are slowing down my CI pipeline?\"\n- \"Find the slowest Playwright tests to optimize\"\n- \"Show me e2e tests taking over 30 seconds\"\n- \"What are the slowest tests on the main branch?\"`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_test_history tool\n */\nexport const getTestHistoryInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n testName: z\n .string()\n .optional()\n .describe('Exact test name to search for'),\n filePath: z\n .string()\n .optional()\n .describe('File path containing the test'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum number of results (default: 20)'),\n}\n\n/**\n * Output schema for get_test_history tool\n */\nexport const getTestHistoryOutputSchema = {\n history: z.array(z.object({\n testRunId: z.string(),\n createdAt: z.string(),\n branch: z.string().optional(),\n commitSha: z.string().optional(),\n status: z.enum(['passed', 'failed', 'skipped', 'pending']),\n durationMs: z.number(),\n message: z.string().optional(),\n })),\n summary: z.object({\n totalRuns: z.number(),\n passedRuns: z.number(),\n failedRuns: z.number(),\n passRate: z.number().nullable(),\n }),\n}\n\nexport interface GetTestHistoryInput {\n projectId?: string\n testName?: string\n filePath?: string\n limit?: number\n}\n\nexport interface GetTestHistoryOutput {\n history: Array<{\n testRunId: string\n createdAt: string\n branch?: string\n commitSha?: string\n status: 'passed' | 'failed' | 'skipped' | 'pending'\n durationMs: number\n message?: string\n }>\n summary: {\n totalRuns: number\n passedRuns: number\n failedRuns: number\n passRate: number | null\n }\n}\n\n/**\n * Execute get_test_history tool\n */\nexport async function executeGetTestHistory(\n client: GafferApiClient,\n input: GetTestHistoryInput,\n): Promise<GetTestHistoryOutput> {\n if (!input.testName && !input.filePath) {\n throw new Error('Either testName or filePath is required')\n }\n\n const response = await client.getTestHistory({\n projectId: input.projectId,\n testName: input.testName,\n filePath: input.filePath,\n limit: input.limit || 20,\n })\n\n return {\n history: response.history.map(entry => ({\n testRunId: entry.testRunId,\n createdAt: entry.createdAt,\n branch: entry.branch,\n commitSha: entry.commitSha,\n status: entry.test.status,\n durationMs: entry.test.durationMs,\n message: entry.test.message || undefined, // Convert null to undefined for schema compliance\n })),\n summary: {\n totalRuns: response.summary.totalRuns,\n passedRuns: response.summary.passedRuns,\n failedRuns: response.summary.failedRuns,\n passRate: response.summary.passRate,\n },\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getTestHistoryMetadata = {\n name: 'get_test_history',\n title: 'Get Test History',\n description: `Get the pass/fail history for a specific test.\n\nSearch by either:\n- testName: The exact name of the test (e.g., \"should handle user login\")\n- filePath: The file path containing the test (e.g., \"tests/auth.test.ts\")\n\nReturns:\n- History of test runs showing pass/fail status over time\n- Duration of each run\n- Branch and commit information\n- Error messages for failed runs\n- Summary statistics (pass rate, total runs)\n\nUse this to investigate flaky tests or understand test stability.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_test_run_details tool\n */\nexport const getTestRunDetailsInputSchema = {\n testRunId: z\n .string()\n .describe('The test run ID to get details for. Use list_test_runs to find test run IDs.'),\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n status: z\n .enum(['passed', 'failed', 'skipped'])\n .optional()\n .describe('Filter tests by status. Returns only tests matching this status.'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe('Maximum number of tests to return (default: 100, max: 500)'),\n offset: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe('Number of tests to skip for pagination (default: 0)'),\n}\n\n/**\n * Output schema for get_test_run_details tool\n */\nexport const getTestRunDetailsOutputSchema = {\n testRunId: z.string(),\n commitSha: z.string().nullable(),\n branch: z.string().nullable(),\n framework: z.string().nullable(),\n createdAt: z.string(),\n summary: z.object({\n passed: z.number(),\n failed: z.number(),\n skipped: z.number(),\n total: z.number(),\n }),\n tests: z.array(z.object({\n name: z.string(),\n fullName: z.string(),\n status: z.enum(['passed', 'failed', 'skipped']),\n durationMs: z.number().nullable(),\n filePath: z.string().nullable(),\n error: z.string().nullable(),\n errorStack: z.string().nullable(),\n })),\n pagination: z.object({\n total: z.number(),\n limit: z.number(),\n offset: z.number(),\n hasMore: z.boolean(),\n }),\n}\n\nexport interface GetTestRunDetailsInput {\n testRunId: string\n projectId?: string\n status?: 'passed' | 'failed' | 'skipped'\n limit?: number\n offset?: number\n}\n\nexport interface GetTestRunDetailsOutput {\n testRunId: string\n commitSha: string | null\n branch: string | null\n framework: string | null\n createdAt: string\n summary: {\n passed: number\n failed: number\n skipped: number\n total: number\n }\n tests: Array<{\n name: string\n fullName: string\n status: 'passed' | 'failed' | 'skipped'\n durationMs: number | null\n filePath: string | null\n error: string | null\n errorStack: string | null\n }>\n pagination: {\n total: number\n limit: number\n offset: number\n hasMore: boolean\n }\n}\n\n/**\n * Execute get_test_run_details tool\n */\nexport async function executeGetTestRunDetails(\n client: GafferApiClient,\n input: GetTestRunDetailsInput,\n): Promise<GetTestRunDetailsOutput> {\n const response = await client.getTestRunDetails({\n projectId: input.projectId,\n testRunId: input.testRunId,\n status: input.status,\n limit: input.limit,\n offset: input.offset,\n })\n\n return {\n testRunId: response.testRunId,\n commitSha: response.commitSha,\n branch: response.branch,\n framework: response.framework,\n createdAt: response.createdAt,\n summary: response.summary,\n tests: response.tests,\n pagination: response.pagination,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getTestRunDetailsMetadata = {\n name: 'get_test_run_details',\n title: 'Get Test Run Details',\n description: `Get parsed test results for a specific test run.\n\nParameters:\n- testRunId (required): The test run ID to get details for\n- projectId (required): Project ID the test run belongs to\n- status (optional): Filter by test status: \"passed\", \"failed\", or \"skipped\"\n- limit (optional): Max tests to return (default: 100, max: 500)\n- offset (optional): Pagination offset (default: 0)\n\nReturns:\n- testRunId: The test run ID\n- commitSha: Git commit SHA (null if not recorded)\n- branch: Git branch name (null if not recorded)\n- framework: Test framework (e.g., \"playwright\", \"vitest\")\n- createdAt: When the test run was created (ISO 8601)\n- summary: Overall counts (passed, failed, skipped, total)\n- tests: Array of individual test results with:\n - name: Short test name\n - fullName: Full test name including describe blocks\n - status: Test status (passed, failed, skipped)\n - durationMs: Test duration in milliseconds (null if not recorded)\n - filePath: Test file path (null if not recorded)\n - error: Error message for failed tests (null otherwise)\n - errorStack: Full stack trace for failed tests (null otherwise)\n- pagination: Pagination info (total, limit, offset, hasMore)\n\nUse cases:\n- \"Show me all failed tests from this test run\"\n- \"Get the test results from commit abc123\"\n- \"List tests that took the longest in this run\"\n- \"Find tests with errors in the auth module\"\n\nNote: For aggregate analytics like flaky test detection or duration trends,\nuse get_test_history, get_flaky_tests, or get_slowest_tests instead.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_untested_files tool\n */\nexport const getUntestedFilesInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n maxCoverage: z\n .number()\n .min(0)\n .max(100)\n .optional()\n .describe('Maximum coverage percentage to include (default: 10 for \"untested\")'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum number of files to return (default: 20)'),\n}\n\n/**\n * Output schema for get_untested_files tool\n */\nexport const getUntestedFilesOutputSchema = {\n hasCoverage: z.boolean(),\n files: z.array(z.object({\n path: z.string(),\n lines: z.object({\n covered: z.number(),\n total: z.number(),\n percentage: z.number(),\n }),\n branches: z.object({\n covered: z.number(),\n total: z.number(),\n percentage: z.number(),\n }),\n functions: z.object({\n covered: z.number(),\n total: z.number(),\n percentage: z.number(),\n }),\n })),\n totalCount: z.number(),\n message: z.string().optional(),\n}\n\nexport interface GetUntestedFilesInput {\n projectId?: string\n maxCoverage?: number\n limit?: number\n}\n\nexport interface GetUntestedFilesOutput {\n hasCoverage: boolean\n files: Array<{\n path: string\n lines: { covered: number, total: number, percentage: number }\n branches: { covered: number, total: number, percentage: number }\n functions: { covered: number, total: number, percentage: number }\n }>\n totalCount: number\n message?: string\n}\n\n/**\n * Execute get_untested_files tool\n */\nexport async function executeGetUntestedFiles(\n client: GafferApiClient,\n input: GetUntestedFilesInput,\n): Promise<GetUntestedFilesOutput> {\n const maxCoverage = input.maxCoverage ?? 10\n const limit = input.limit ?? 20\n\n const response = await client.getCoverageFiles({\n projectId: input.projectId,\n maxCoverage,\n limit,\n sortBy: 'coverage',\n sortOrder: 'asc', // Lowest coverage first\n })\n\n return {\n hasCoverage: response.hasCoverage,\n files: response.files.map(f => ({\n path: f.path,\n lines: f.lines,\n branches: f.branches,\n functions: f.functions,\n })),\n totalCount: response.pagination.total,\n message: response.message,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const getUntestedFilesMetadata = {\n name: 'get_untested_files',\n title: 'Get Untested Files',\n description: `Get files with little or no test coverage.\n\nReturns files sorted by coverage percentage (lowest first), filtered\nto only include files below a coverage threshold.\n\nParameters:\n- projectId: The project to analyze (required)\n- maxCoverage: Include files with coverage at or below this % (default: 10)\n- limit: Maximum number of files to return (default: 20, max: 100)\n\nReturns:\n- List of files sorted by coverage (lowest first)\n- Each file includes line/branch/function coverage metrics\n- Total count of files matching the criteria\n\nIMPORTANT: Results may be dominated by certain file types (e.g., UI components) that are\nnumerous but not necessarily the highest priority. For targeted analysis of specific code\nareas (backend, services, utilities), use get_coverage_for_file with path prefixes instead.\n\nTo prioritize effectively, explore the codebase to understand which code is heavily utilized\n(entry points, frequently-imported files, critical business logic) and then query coverage\nfor those specific paths.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport type { UploadSessionDetailResponse, UploadSessionsResponse } from '../types.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for get_upload_status tool\n */\nexport const getUploadStatusInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n sessionId: z\n .string()\n .optional()\n .describe('Specific upload session ID. If provided, returns detailed status for that session. Otherwise, lists recent sessions.'),\n commitSha: z\n .string()\n .optional()\n .describe('Filter sessions by commit SHA. Useful for checking if results for a specific commit are ready.'),\n branch: z\n .string()\n .optional()\n .describe('Filter sessions by branch name.'),\n}\n\n/**\n * Output schema for get_upload_status tool\n */\nexport const getUploadStatusOutputSchema = {\n sessions: z.array(z.object({\n id: z.string(),\n processingStatus: z.string(),\n commitSha: z.string().nullable(),\n branch: z.string().nullable(),\n pendingFileCount: z.number(),\n failedFileCount: z.number(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })).optional(),\n session: z.object({\n id: z.string(),\n processingStatus: z.string(),\n commitSha: z.string().nullable(),\n branch: z.string().nullable(),\n createdAt: z.string(),\n }).optional(),\n testRuns: z.array(z.object({\n id: z.string(),\n framework: z.string().nullable(),\n summary: z.object({\n passed: z.number(),\n failed: z.number(),\n skipped: z.number(),\n total: z.number(),\n }),\n createdAt: z.string(),\n })).optional(),\n coverageReports: z.array(z.object({\n id: z.string(),\n format: z.string(),\n createdAt: z.string(),\n })).optional(),\n pagination: z.object({\n total: z.number(),\n limit: z.number(),\n offset: z.number(),\n hasMore: z.boolean(),\n }).optional(),\n}\n\nexport interface GetUploadStatusInput {\n projectId?: string\n sessionId?: string\n commitSha?: string\n branch?: string\n}\n\nexport type GetUploadStatusOutput = UploadSessionsResponse | UploadSessionDetailResponse\n\n/**\n * Execute get_upload_status tool\n */\nexport async function executeGetUploadStatus(\n client: GafferApiClient,\n input: GetUploadStatusInput,\n): Promise<GetUploadStatusOutput> {\n if (input.sessionId) {\n return client.getUploadSessionDetail({\n projectId: input.projectId,\n sessionId: input.sessionId,\n })\n }\n\n return client.listUploadSessions({\n projectId: input.projectId,\n commitSha: input.commitSha,\n branch: input.branch,\n })\n}\n\n/**\n * Tool metadata\n */\nexport const getUploadStatusMetadata = {\n name: 'get_upload_status',\n title: 'Get Upload Status',\n description: `Check if CI results have been uploaded and processed.\n\nUse this tool to answer \"are my test results ready?\" after pushing code.\n\nParameters:\n- projectId (required): The project ID\n- sessionId (optional): Specific upload session ID for detailed status\n- commitSha (optional): Filter by commit SHA to find uploads for a specific commit\n- branch (optional): Filter by branch name\n\nBehavior:\n- If sessionId is provided: returns detailed status with linked test runs and coverage reports\n- Otherwise: returns a list of recent upload sessions (filtered by commitSha/branch if provided)\n\nProcessing statuses:\n- \"pending\" — upload received, processing not started\n- \"processing\" — files are being parsed\n- \"completed\" — all files processed successfully, results are ready\n- \"error\" — some files failed to process\n\nWorkflow:\n1. After pushing code, call with commitSha to find the upload session\n2. Check processingStatus — if \"completed\", results are ready\n3. If \"processing\" or \"pending\", wait and check again\n4. Once completed, use the linked testRunIds with get_test_run_details\n\nReturns (list mode):\n- sessions: Array of upload sessions with processing status\n- pagination: Pagination info\n\nReturns (detail mode):\n- session: Upload session details\n- testRuns: Linked test run summaries (id, framework, pass/fail counts)\n- coverageReports: Linked coverage report summaries (id, format)`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for list_projects tool\n */\nexport const listProjectsInputSchema = {\n organizationId: z\n .string()\n .optional()\n .describe('Filter by organization ID (optional)'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum number of projects to return (default: 50)'),\n}\n\n/**\n * Output schema for list_projects tool\n */\nexport const listProjectsOutputSchema = {\n projects: z.array(z.object({\n id: z.string(),\n name: z.string(),\n description: z.string().nullable().optional(),\n organization: z.object({\n id: z.string(),\n name: z.string(),\n slug: z.string(),\n }),\n })),\n total: z.number(),\n}\n\nexport interface ListProjectsInput {\n organizationId?: string\n limit?: number\n}\n\nexport interface ListProjectsOutput {\n projects: Array<{\n id: string\n name: string\n description?: string | null\n organization: {\n id: string\n name: string\n slug: string\n }\n }>\n total: number\n}\n\n/**\n * Execute list_projects tool\n */\nexport async function executeListProjects(\n client: GafferApiClient,\n input: ListProjectsInput,\n): Promise<ListProjectsOutput> {\n const response = await client.listProjects({\n organizationId: input.organizationId,\n limit: input.limit,\n })\n\n return {\n projects: response.projects.map(p => ({\n id: p.id,\n name: p.name,\n description: p.description,\n organization: p.organization,\n })),\n total: response.pagination.total,\n }\n}\n\n/**\n * Tool metadata\n */\nexport const listProjectsMetadata = {\n name: 'list_projects',\n title: 'List Projects',\n description: `List all projects you have access to.\n\nReturns a list of projects with their IDs, names, and organization info.\nUse this to find project IDs for other tools like get_project_health.\n\nRequires a user API Key (gaf_). Get one from Account Settings in the Gaffer dashboard.`,\n}\n","import type { GafferApiClient } from '../api-client.js'\nimport { z } from 'zod'\n\n/**\n * Input schema for list_test_runs tool\n */\nexport const listTestRunsInputSchema = {\n projectId: z\n .string()\n .optional()\n .describe('Project ID. Required for user API keys (gaf_). Not needed for project tokens — omit and it resolves automatically.'),\n commitSha: z\n .string()\n .optional()\n .describe('Filter by commit SHA (exact or prefix match)'),\n branch: z\n .string()\n .optional()\n .describe('Filter by branch name'),\n status: z\n .enum(['passed', 'failed'])\n .optional()\n .describe('Filter by status: \"passed\" (no failures) or \"failed\" (has failures)'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum number of test runs to return (default: 20)'),\n}\n\n/**\n * Output schema for list_test_runs tool\n */\nexport const listTestRunsOutputSchema = {\n testRuns: z.array(z.object({\n id: z.string(),\n commitSha: z.string().optional(),\n branch: z.string().optional(),\n passedCount: z.number(),\n failedCount: z.number(),\n skippedCount: z.number(),\n totalCount: z.number(),\n createdAt: z.string(),\n })),\n pagination: z.object({\n total: z.number(),\n hasMore: z.boolean(),\n }),\n}\n\nexport interface ListTestRunsInput {\n projectId?: string\n commitSha?: string\n branch?: string\n status?: 'passed' | 'failed'\n limit?: number\n}\n\nexport interface ListTestRunsOutput {\n testRuns: Array<{\n id: string\n commitSha?: string\n branch?: string\n passedCount: number\n failedCount: number\n skippedCount: number\n totalCount: number\n createdAt: string\n }>\n pagination: {\n total: number\n hasMore: boolean\n }\n}\n\n/**\n * Execute list_test_runs tool\n */\nexport async function executeListTestRuns(\n client: GafferApiClient,\n input: ListTestRunsInput,\n): Promise<ListTestRunsOutput> {\n const response = await client.getTestRuns({\n projectId: input.projectId,\n commitSha: input.commitSha,\n branch: input.branch,\n status: input.status,\n limit: input.limit || 20,\n })\n\n return {\n testRuns: response.testRuns.map(run => ({\n id: run.id,\n commitSha: run.commitSha || undefined,\n branch: run.branch || undefined,\n passedCount: run.summary.passed,\n failedCount: run.summary.failed,\n skippedCount: run.summary.skipped,\n totalCount: run.summary.total,\n createdAt: run.createdAt,\n })),\n pagination: {\n total: response.pagination.total,\n hasMore: response.pagination.hasMore,\n },\n }\n}\n\n/**\n * Tool metadata\n */\nexport const listTestRunsMetadata = {\n name: 'list_test_runs',\n title: 'List Test Runs',\n description: `List recent test runs for a project with optional filtering.\n\nFilter by:\n- commitSha: Filter by commit SHA (supports prefix matching)\n- branch: Filter by branch name\n- status: Filter by \"passed\" (no failures) or \"failed\" (has failures)\n\nReturns:\n- List of test runs with:\n - id: Test run ID (can be used with get_test_run for details)\n - commitSha: Git commit SHA\n - branch: Git branch name\n - passedCount/failedCount/skippedCount: Test counts\n - createdAt: When the test run was created\n- Pagination info (total count, hasMore flag)\n\nUse cases:\n- \"What tests failed in commit abc123?\"\n- \"Show me recent test runs on main branch\"\n- \"What's the status of tests on my feature branch?\"`,\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { GafferApiClient } from './api-client.js'\nimport {\n compareTestMetricsInputSchema,\n compareTestMetricsMetadata,\n compareTestMetricsOutputSchema,\n executeCompareTestMetrics,\n} from './tools/compare-test-metrics.js'\nimport {\n executeFindUncoveredFailureAreas,\n findUncoveredFailureAreasInputSchema,\n findUncoveredFailureAreasMetadata,\n findUncoveredFailureAreasOutputSchema,\n} from './tools/find-uncovered-failure-areas.js'\nimport {\n executeGetCoverageForFile,\n getCoverageForFileInputSchema,\n getCoverageForFileMetadata,\n getCoverageForFileOutputSchema,\n} from './tools/get-coverage-for-file.js'\nimport {\n executeGetCoverageSummary,\n getCoverageSummaryInputSchema,\n getCoverageSummaryMetadata,\n getCoverageSummaryOutputSchema,\n} from './tools/get-coverage-summary.js'\nimport {\n executeGetFailureClusters,\n getFailureClustersInputSchema,\n getFailureClustersMetadata,\n getFailureClustersOutputSchema,\n} from './tools/get-failure-clusters.js'\nimport {\n executeGetFlakyTests,\n getFlakyTestsInputSchema,\n getFlakyTestsMetadata,\n getFlakyTestsOutputSchema,\n} from './tools/get-flaky-tests.js'\nimport {\n executeGetProjectHealth,\n getProjectHealthInputSchema,\n getProjectHealthMetadata,\n getProjectHealthOutputSchema,\n} from './tools/get-project-health.js'\nimport {\n executeGetReportBrowserUrl,\n getReportBrowserUrlInputSchema,\n getReportBrowserUrlMetadata,\n getReportBrowserUrlOutputSchema,\n} from './tools/get-report-browser-url.js'\nimport {\n executeGetReport,\n getReportInputSchema,\n getReportMetadata,\n getReportOutputSchema,\n} from './tools/get-report.js'\nimport {\n executeGetSlowestTests,\n getSlowestTestsInputSchema,\n getSlowestTestsMetadata,\n getSlowestTestsOutputSchema,\n} from './tools/get-slowest-tests.js'\nimport {\n executeGetTestHistory,\n getTestHistoryInputSchema,\n getTestHistoryMetadata,\n getTestHistoryOutputSchema,\n} from './tools/get-test-history.js'\nimport {\n executeGetTestRunDetails,\n getTestRunDetailsInputSchema,\n getTestRunDetailsMetadata,\n getTestRunDetailsOutputSchema,\n} from './tools/get-test-run-details.js'\nimport {\n executeGetUntestedFiles,\n getUntestedFilesInputSchema,\n getUntestedFilesMetadata,\n getUntestedFilesOutputSchema,\n} from './tools/get-untested-files.js'\nimport {\n executeGetUploadStatus,\n getUploadStatusInputSchema,\n getUploadStatusMetadata,\n getUploadStatusOutputSchema,\n} from './tools/get-upload-status.js'\nimport {\n executeListProjects,\n listProjectsInputSchema,\n listProjectsMetadata,\n listProjectsOutputSchema,\n} from './tools/list-projects.js'\nimport {\n executeListTestRuns,\n listTestRunsInputSchema,\n listTestRunsMetadata,\n listTestRunsOutputSchema,\n} from './tools/list-test-runs.js'\n\n/**\n * Log error to stderr for observability\n * MCP uses stdout for communication, so stderr is safe for logging\n */\nfunction logError(toolName: string, error: unknown): void {\n const timestamp = new Date().toISOString()\n const message = error instanceof Error ? error.message : 'Unknown error'\n const stack = error instanceof Error ? error.stack : undefined\n console.error(`[${timestamp}] [gaffer-mcp] ${toolName} failed: ${message}`)\n if (stack) {\n console.error(stack)\n }\n}\n\n/**\n * Handle tool error: log it and return MCP error response\n */\nfunction handleToolError(toolName: string, error: unknown): { content: { type: 'text', text: string }[], isError: true } {\n logError(toolName, error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return {\n content: [{ type: 'text' as const, text: `Error: ${message}` }],\n isError: true,\n }\n}\n\n/**\n * Tool definition for registration helper.\n * Schema types use `any` to accommodate the MCP SDK's complex Zod type requirements.\n */\ninterface ToolDefinition<TInput, TOutput> {\n metadata: { name: string, title: string, description: string }\n inputSchema: any\n outputSchema: any\n execute: (client: GafferApiClient, input: TInput) => Promise<TOutput>\n}\n\n/**\n * Register a tool with the MCP server using a consistent pattern.\n * Reduces boilerplate by handling error wrapping and response formatting.\n */\nfunction registerTool<TInput, TOutput>(\n server: McpServer,\n client: GafferApiClient,\n tool: ToolDefinition<TInput, TOutput>,\n): void {\n server.registerTool(\n tool.metadata.name,\n {\n title: tool.metadata.title,\n description: tool.metadata.description,\n inputSchema: tool.inputSchema,\n outputSchema: tool.outputSchema,\n },\n async (input: TInput) => {\n try {\n const output = await tool.execute(client, input)\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(output, null, 2) }],\n structuredContent: output as unknown as Record<string, unknown>,\n }\n }\n catch (error) {\n return handleToolError(tool.metadata.name, error)\n }\n },\n )\n}\n\n/**\n * Gaffer MCP Server\n *\n * Provides AI assistants with access to test history and health metrics.\n *\n * Supports two authentication modes:\n * 1. User API Keys (gaf_) - Read-only access to all user's projects\n * Set via GAFFER_API_KEY environment variable\n * 2. Project Upload Tokens (gfr_) - Legacy, single project access\n */\nasync function main() {\n // Validate API key is present\n const apiKey = process.env.GAFFER_API_KEY\n if (!apiKey) {\n console.error('Error: GAFFER_API_KEY environment variable is required')\n console.error('')\n console.error('Get your API Key from: https://app.gaffer.sh/account/api-keys')\n console.error('')\n console.error('Then configure Claude Code or Cursor with:')\n console.error(JSON.stringify({\n mcpServers: {\n gaffer: {\n command: 'npx',\n args: ['-y', '@gaffer-sh/mcp'],\n env: {\n GAFFER_API_KEY: 'gaf_your-api-key-here',\n },\n },\n },\n }, null, 2))\n process.exit(1)\n }\n\n // Create API client\n const client = GafferApiClient.fromEnv()\n\n // Create MCP server\n const server = new McpServer(\n {\n name: 'gaffer',\n version: '0.1.0',\n },\n {\n instructions: `Gaffer provides test analytics and coverage data for your projects.\n\n## Authentication\n\n${client.isUserToken()\n ? 'You have access to multiple projects. Use `list_projects` to find project IDs, then pass `projectId` to all tools.'\n : 'Your token is scoped to a single project. Do NOT call `list_projects`. Do NOT pass `projectId` — it resolves automatically. All tools are available.'}\n\n## Coverage Analysis Best Practices\n\nWhen helping users improve test coverage, combine coverage data with codebase exploration:\n\n1. **Understand code utilization first**: Before targeting files by coverage percentage, explore which code is critical:\n - Find entry points (route definitions, event handlers, exported functions)\n - Find heavily-imported files (files imported by many others are high-value targets)\n - Identify critical business logic (auth, payments, data mutations)\n\n2. **Prioritize by impact**: Low coverage alone doesn't indicate priority. Consider:\n - High utilization + low coverage = highest priority\n - Large files with 0% coverage have bigger impact than small files\n - Use find_uncovered_failure_areas for files with both low coverage AND test failures\n\n3. **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.\n\n4. **Iterate**: Get baseline → identify targets → write tests → re-check coverage after CI uploads new results.\n\n## Finding Invisible Files\n\nCoverage 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.\n\nTo find invisible files:\n1. Use get_coverage_for_file with a path prefix (e.g., \"server/\") to see what Gaffer tracks\n2. Use the local Glob tool to list all source files in that path\n3. Compare the lists - files in local but NOT in Gaffer are invisible\n4. These files need tests that actually import them\n\nExample: 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.\n\n## Agentic CI / Test Failure Diagnosis\n\nWhen helping diagnose CI failures or fix failing tests:\n\n1. **Check flakiness first**: Use get_flaky_tests to identify non-deterministic tests.\n Skip flaky tests unless the user specifically wants to stabilize them.\n\n2. **Get failure details**: Use get_test_run_details with status='failed'\n to see error messages and stack traces for failing tests.\n\n3. **Group by root cause**: Use get_failure_clusters to see which failures\n share the same underlying error — fix the root cause, not individual tests.\n\n4. **Check history**: Use get_test_history to understand if the failure is new\n (regression) or recurring (existing bug).\n\n5. **Verify fixes**: After code changes, use compare_test_metrics to confirm\n the specific test now passes.\n\n6. **Prioritize by risk**: Use find_uncovered_failure_areas to identify\n which failing code has the lowest test coverage — fix those first.\n\n## Checking Upload Status\n\nWhen an agent needs to know if CI results are ready:\n\n1. Use get_upload_status with commitSha or branch to find upload sessions\n2. Check processingStatus: \"completed\" means results are ready, \"processing\" means wait\n3. Once completed, use the linked testRunIds to get test results`,\n },\n )\n\n // Register all tools using the helper\n registerTool(server, client, {\n metadata: getProjectHealthMetadata,\n inputSchema: getProjectHealthInputSchema,\n outputSchema: getProjectHealthOutputSchema,\n execute: executeGetProjectHealth,\n })\n\n registerTool(server, client, {\n metadata: getTestHistoryMetadata,\n inputSchema: getTestHistoryInputSchema,\n outputSchema: getTestHistoryOutputSchema,\n execute: executeGetTestHistory,\n })\n\n registerTool(server, client, {\n metadata: getFlakyTestsMetadata,\n inputSchema: getFlakyTestsInputSchema,\n outputSchema: getFlakyTestsOutputSchema,\n execute: executeGetFlakyTests,\n })\n\n registerTool(server, client, {\n metadata: listTestRunsMetadata,\n inputSchema: listTestRunsInputSchema,\n outputSchema: listTestRunsOutputSchema,\n execute: executeListTestRuns,\n })\n\n if (client.isUserToken()) {\n registerTool(server, client, {\n metadata: listProjectsMetadata,\n inputSchema: listProjectsInputSchema,\n outputSchema: listProjectsOutputSchema,\n execute: executeListProjects,\n })\n }\n\n registerTool(server, client, {\n metadata: getReportMetadata,\n inputSchema: getReportInputSchema,\n outputSchema: getReportOutputSchema,\n execute: executeGetReport,\n })\n\n registerTool(server, client, {\n metadata: getSlowestTestsMetadata,\n inputSchema: getSlowestTestsInputSchema,\n outputSchema: getSlowestTestsOutputSchema,\n execute: executeGetSlowestTests,\n })\n\n registerTool(server, client, {\n metadata: getTestRunDetailsMetadata,\n inputSchema: getTestRunDetailsInputSchema,\n outputSchema: getTestRunDetailsOutputSchema,\n execute: executeGetTestRunDetails,\n })\n\n registerTool(server, client, {\n metadata: getFailureClustersMetadata,\n inputSchema: getFailureClustersInputSchema,\n outputSchema: getFailureClustersOutputSchema,\n execute: executeGetFailureClusters,\n })\n\n registerTool(server, client, {\n metadata: compareTestMetricsMetadata,\n inputSchema: compareTestMetricsInputSchema,\n outputSchema: compareTestMetricsOutputSchema,\n execute: executeCompareTestMetrics,\n })\n\n registerTool(server, client, {\n metadata: getCoverageSummaryMetadata,\n inputSchema: getCoverageSummaryInputSchema,\n outputSchema: getCoverageSummaryOutputSchema,\n execute: executeGetCoverageSummary,\n })\n\n registerTool(server, client, {\n metadata: getCoverageForFileMetadata,\n inputSchema: getCoverageForFileInputSchema,\n outputSchema: getCoverageForFileOutputSchema,\n execute: executeGetCoverageForFile,\n })\n\n registerTool(server, client, {\n metadata: findUncoveredFailureAreasMetadata,\n inputSchema: findUncoveredFailureAreasInputSchema,\n outputSchema: findUncoveredFailureAreasOutputSchema,\n execute: executeFindUncoveredFailureAreas,\n })\n\n registerTool(server, client, {\n metadata: getUntestedFilesMetadata,\n inputSchema: getUntestedFilesInputSchema,\n outputSchema: getUntestedFilesOutputSchema,\n execute: executeGetUntestedFiles,\n })\n\n registerTool(server, client, {\n metadata: getReportBrowserUrlMetadata,\n inputSchema: getReportBrowserUrlInputSchema,\n outputSchema: getReportBrowserUrlOutputSchema,\n execute: executeGetReportBrowserUrl,\n })\n\n registerTool(server, client, {\n metadata: getUploadStatusMetadata,\n inputSchema: getUploadStatusInputSchema,\n outputSchema: getUploadStatusOutputSchema,\n execute: executeGetUploadStatus,\n })\n\n // Connect via stdio transport\n const transport = new StdioServerTransport()\n await server.connect(transport)\n}\n\n// Run the server\nmain().catch((error) => {\n console.error('Fatal error:', error)\n process.exit(1)\n})\n"],"mappings":"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"}
|
package/package.json
CHANGED