@gaffer-sh/mcp 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +96 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1625 -1365
- package/dist/index.js.map +1 -0
- package/package.json +6 -7
package/dist/index.js
CHANGED
|
@@ -1,518 +1,519 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/index.ts
|
|
2
|
+
import { createRequire } from "node:module";
|
|
4
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
//#region src/api-client.ts
|
|
8
|
+
const pkg = createRequire(import.meta.url)("../package.json");
|
|
9
|
+
const REQUEST_TIMEOUT_MS = 3e4;
|
|
10
|
+
const MAX_RETRIES = 3;
|
|
11
|
+
const INITIAL_RETRY_DELAY_MS = 1e3;
|
|
12
|
+
const RETRYABLE_STATUS_CODES = [
|
|
13
|
+
401,
|
|
14
|
+
429,
|
|
15
|
+
500,
|
|
16
|
+
502,
|
|
17
|
+
503,
|
|
18
|
+
504
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Sleep for a given number of milliseconds
|
|
22
|
+
*/
|
|
12
23
|
function sleep(ms) {
|
|
13
|
-
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Detect token type from prefix
|
|
28
|
+
* - gaf_ = user API Key (read-only, cross-project)
|
|
29
|
+
* - gfr_ = Project Upload Token (legacy, single project)
|
|
30
|
+
*/
|
|
15
31
|
function detectTokenType(token) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
return "project";
|
|
32
|
+
if (token.startsWith("gaf_")) return "user";
|
|
33
|
+
return "project";
|
|
20
34
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Get a browser-navigable URL for viewing a test report
|
|
426
|
-
*
|
|
427
|
-
* @param options - Query options
|
|
428
|
-
* @param options.projectId - The project ID (required)
|
|
429
|
-
* @param options.testRunId - The test run ID (required)
|
|
430
|
-
* @param options.filename - Specific file to open (default: index.html)
|
|
431
|
-
* @returns URL with signed token for browser access
|
|
432
|
-
*/
|
|
433
|
-
async getReportBrowserUrl(options) {
|
|
434
|
-
if (!this.isUserToken()) {
|
|
435
|
-
throw new Error("getReportBrowserUrl requires a user API Key (gaf_).");
|
|
436
|
-
}
|
|
437
|
-
if (!options.projectId) {
|
|
438
|
-
throw new Error("projectId is required");
|
|
439
|
-
}
|
|
440
|
-
if (!options.testRunId) {
|
|
441
|
-
throw new Error("testRunId is required");
|
|
442
|
-
}
|
|
443
|
-
return this.request(
|
|
444
|
-
`/user/projects/${options.projectId}/reports/${options.testRunId}/browser-url`,
|
|
445
|
-
{
|
|
446
|
-
...options.filename && { filename: options.filename }
|
|
447
|
-
}
|
|
448
|
-
);
|
|
449
|
-
}
|
|
35
|
+
/**
|
|
36
|
+
* Gaffer API v1 client for MCP server
|
|
37
|
+
*
|
|
38
|
+
* Supports two authentication modes:
|
|
39
|
+
* 1. User API Keys (gaf_) - Read-only access to all user's projects
|
|
40
|
+
* 2. Project Upload Tokens (gfr_) - Legacy, single project access
|
|
41
|
+
*/
|
|
42
|
+
var GafferApiClient = class GafferApiClient {
|
|
43
|
+
apiKey;
|
|
44
|
+
baseUrl;
|
|
45
|
+
tokenType;
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.apiKey = config.apiKey;
|
|
48
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
49
|
+
this.tokenType = detectTokenType(config.apiKey);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create client from environment variables
|
|
53
|
+
*
|
|
54
|
+
* Supports:
|
|
55
|
+
* - GAFFER_API_KEY (for user API Keys gaf_)
|
|
56
|
+
*/
|
|
57
|
+
static fromEnv() {
|
|
58
|
+
const apiKey = process.env.GAFFER_API_KEY;
|
|
59
|
+
if (!apiKey) throw new Error("GAFFER_API_KEY environment variable is required");
|
|
60
|
+
return new GafferApiClient({
|
|
61
|
+
apiKey,
|
|
62
|
+
baseUrl: process.env.GAFFER_API_URL || "https://app.gaffer.sh"
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check if using a user API Key (enables cross-project features)
|
|
67
|
+
*/
|
|
68
|
+
isUserToken() {
|
|
69
|
+
return this.tokenType === "user";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Make authenticated request to Gaffer API with retry logic
|
|
73
|
+
*/
|
|
74
|
+
async request(endpoint, params) {
|
|
75
|
+
const url = new URL(`/api/v1${endpoint}`, this.baseUrl);
|
|
76
|
+
if (params) {
|
|
77
|
+
for (const [key, value] of Object.entries(params)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
78
|
+
}
|
|
79
|
+
let lastError = null;
|
|
80
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(url.toString(), {
|
|
85
|
+
method: "GET",
|
|
86
|
+
headers: {
|
|
87
|
+
"X-API-Key": this.apiKey,
|
|
88
|
+
"Accept": "application/json",
|
|
89
|
+
"User-Agent": `gaffer-mcp/${pkg.version}`
|
|
90
|
+
},
|
|
91
|
+
signal: controller.signal
|
|
92
|
+
});
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const errorData = await response.json().catch(() => ({}));
|
|
95
|
+
if (RETRYABLE_STATUS_CODES.includes(response.status) && attempt < MAX_RETRIES) {
|
|
96
|
+
let delayMs = INITIAL_RETRY_DELAY_MS * 2 ** attempt;
|
|
97
|
+
if (response.status === 429) {
|
|
98
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
99
|
+
if (retryAfter) delayMs = Math.max(delayMs, Number.parseInt(retryAfter, 10) * 1e3);
|
|
100
|
+
}
|
|
101
|
+
lastError = new Error(errorData.error?.message || `API request failed: ${response.status}`);
|
|
102
|
+
await sleep(delayMs);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const errorMessage = errorData.error?.message || `API request failed: ${response.status}`;
|
|
106
|
+
throw new Error(errorMessage);
|
|
107
|
+
}
|
|
108
|
+
return response.json();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
clearTimeout(timeoutId);
|
|
111
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
112
|
+
lastError = /* @__PURE__ */ new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`);
|
|
113
|
+
if (attempt < MAX_RETRIES) {
|
|
114
|
+
await sleep(INITIAL_RETRY_DELAY_MS * 2 ** attempt);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
throw lastError;
|
|
118
|
+
}
|
|
119
|
+
if (error instanceof TypeError && attempt < MAX_RETRIES) {
|
|
120
|
+
lastError = error;
|
|
121
|
+
await sleep(INITIAL_RETRY_DELAY_MS * 2 ** attempt);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
} finally {
|
|
126
|
+
clearTimeout(timeoutId);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw lastError || /* @__PURE__ */ new Error("Request failed after retries");
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
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
|
|
139
|
+
*/
|
|
140
|
+
async listProjects(options = {}) {
|
|
141
|
+
if (!this.isUserToken()) throw new Error("listProjects requires a user API Key (gaf_). Upload Tokens (gfr_) can only access a single project.");
|
|
142
|
+
return this.request("/user/projects", {
|
|
143
|
+
...options.organizationId && { organizationId: options.organizationId },
|
|
144
|
+
...options.limit && { limit: options.limit },
|
|
145
|
+
...options.offset && { offset: options.offset }
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 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
|
+
*/
|
|
155
|
+
async getProjectHealth(options = {}) {
|
|
156
|
+
if (this.isUserToken()) {
|
|
157
|
+
if (!options.projectId) throw new Error("projectId is required when using a user API Key");
|
|
158
|
+
return this.request(`/user/projects/${options.projectId}/health`, { days: options.days || 30 });
|
|
159
|
+
}
|
|
160
|
+
return this.request("/project/analytics", { days: options.days || 30 });
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 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
|
+
*/
|
|
171
|
+
async getTestHistory(options) {
|
|
172
|
+
const testName = options.testName?.trim();
|
|
173
|
+
const filePath = options.filePath?.trim();
|
|
174
|
+
if (!testName && !filePath) throw new Error("Either testName or filePath is required (and must not be empty)");
|
|
175
|
+
if (this.isUserToken()) {
|
|
176
|
+
if (!options.projectId) throw new Error("projectId is required when using a user API Key");
|
|
177
|
+
return this.request(`/user/projects/${options.projectId}/test-history`, {
|
|
178
|
+
...testName && { testName },
|
|
179
|
+
...filePath && { filePath },
|
|
180
|
+
...options.limit && { limit: options.limit }
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return this.request("/project/test-history", {
|
|
184
|
+
...testName && { testName },
|
|
185
|
+
...filePath && { filePath },
|
|
186
|
+
...options.limit && { limit: options.limit }
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 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
|
+
*/
|
|
198
|
+
async getFlakyTests(options = {}) {
|
|
199
|
+
if (this.isUserToken()) {
|
|
200
|
+
if (!options.projectId) throw new Error("projectId is required when using a user API Key");
|
|
201
|
+
return this.request(`/user/projects/${options.projectId}/flaky-tests`, {
|
|
202
|
+
...options.threshold && { threshold: options.threshold },
|
|
203
|
+
...options.limit && { limit: options.limit },
|
|
204
|
+
...options.days && { days: options.days }
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return this.request("/project/flaky-tests", {
|
|
208
|
+
...options.threshold && { threshold: options.threshold },
|
|
209
|
+
...options.limit && { limit: options.limit },
|
|
210
|
+
...options.days && { days: options.days }
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 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
|
+
*/
|
|
223
|
+
async getTestRuns(options = {}) {
|
|
224
|
+
if (this.isUserToken()) {
|
|
225
|
+
if (!options.projectId) throw new Error("projectId is required when using a user API Key");
|
|
226
|
+
return this.request(`/user/projects/${options.projectId}/test-runs`, {
|
|
227
|
+
...options.commitSha && { commitSha: options.commitSha },
|
|
228
|
+
...options.branch && { branch: options.branch },
|
|
229
|
+
...options.status && { status: options.status },
|
|
230
|
+
...options.limit && { limit: options.limit }
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return this.request("/project/test-runs", {
|
|
234
|
+
...options.commitSha && { commitSha: options.commitSha },
|
|
235
|
+
...options.branch && { branch: options.branch },
|
|
236
|
+
...options.status && { status: options.status },
|
|
237
|
+
...options.limit && { limit: options.limit }
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get report files for a test run
|
|
242
|
+
*
|
|
243
|
+
* @param testRunId - The test run ID
|
|
244
|
+
* @returns Report metadata with download URLs for each file
|
|
245
|
+
*/
|
|
246
|
+
async getReport(testRunId) {
|
|
247
|
+
if (!this.isUserToken()) throw new Error("getReport requires a user API Key (gaf_). Upload Tokens (gfr_) cannot access reports via API.");
|
|
248
|
+
if (!testRunId) throw new Error("testRunId is required");
|
|
249
|
+
return this.request(`/user/test-runs/${testRunId}/report`);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* 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
|
+
*/
|
|
262
|
+
async getSlowestTests(options) {
|
|
263
|
+
if (!this.isUserToken()) throw new Error("getSlowestTests requires a user API Key (gaf_).");
|
|
264
|
+
if (!options.projectId) throw new Error("projectId is required");
|
|
265
|
+
return this.request(`/user/projects/${options.projectId}/slowest-tests`, {
|
|
266
|
+
...options.days && { days: options.days },
|
|
267
|
+
...options.limit && { limit: options.limit },
|
|
268
|
+
...options.framework && { framework: options.framework },
|
|
269
|
+
...options.branch && { branch: options.branch }
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* 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
|
+
*/
|
|
283
|
+
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
|
+
if (!options.testRunId) throw new Error("testRunId is required");
|
|
287
|
+
return this.request(`/user/projects/${options.projectId}/test-runs/${options.testRunId}/details`, {
|
|
288
|
+
...options.status && { status: options.status },
|
|
289
|
+
...options.limit && { limit: options.limit },
|
|
290
|
+
...options.offset && { offset: options.offset }
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* 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
|
+
*/
|
|
305
|
+
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
|
+
if (!options.testName) throw new Error("testName is required");
|
|
309
|
+
return this.request(`/user/projects/${options.projectId}/compare-test`, {
|
|
310
|
+
testName: options.testName,
|
|
311
|
+
...options.beforeCommit && { beforeCommit: options.beforeCommit },
|
|
312
|
+
...options.afterCommit && { afterCommit: options.afterCommit },
|
|
313
|
+
...options.beforeRunId && { beforeRunId: options.beforeRunId },
|
|
314
|
+
...options.afterRunId && { afterRunId: options.afterRunId }
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* 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
|
+
*/
|
|
325
|
+
async getCoverageSummary(options) {
|
|
326
|
+
if (!this.isUserToken()) throw new Error("getCoverageSummary requires a user API Key (gaf_).");
|
|
327
|
+
if (!options.projectId) throw new Error("projectId is required");
|
|
328
|
+
return this.request(`/user/projects/${options.projectId}/coverage-summary`, { ...options.days && { days: options.days } });
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* 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
|
+
*/
|
|
344
|
+
async getCoverageFiles(options) {
|
|
345
|
+
if (!this.isUserToken()) throw new Error("getCoverageFiles requires a user API Key (gaf_).");
|
|
346
|
+
if (!options.projectId) throw new Error("projectId is required");
|
|
347
|
+
return this.request(`/user/projects/${options.projectId}/coverage/files`, {
|
|
348
|
+
...options.filePath && { filePath: options.filePath },
|
|
349
|
+
...options.minCoverage !== void 0 && { minCoverage: options.minCoverage },
|
|
350
|
+
...options.maxCoverage !== void 0 && { maxCoverage: options.maxCoverage },
|
|
351
|
+
...options.limit && { limit: options.limit },
|
|
352
|
+
...options.offset && { offset: options.offset },
|
|
353
|
+
...options.sortBy && { sortBy: options.sortBy },
|
|
354
|
+
...options.sortOrder && { sortOrder: options.sortOrder }
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* 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
|
+
*/
|
|
366
|
+
async getCoverageRiskAreas(options) {
|
|
367
|
+
if (!this.isUserToken()) throw new Error("getCoverageRiskAreas requires a user API Key (gaf_).");
|
|
368
|
+
if (!options.projectId) throw new Error("projectId is required");
|
|
369
|
+
return this.request(`/user/projects/${options.projectId}/coverage/risk-areas`, {
|
|
370
|
+
...options.days && { days: options.days },
|
|
371
|
+
...options.coverageThreshold !== void 0 && { coverageThreshold: options.coverageThreshold }
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* 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
|
+
*/
|
|
383
|
+
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
|
+
if (!options.testRunId) throw new Error("testRunId is required");
|
|
387
|
+
return this.request(`/user/projects/${options.projectId}/reports/${options.testRunId}/browser-url`, { ...options.filename && { filename: options.filename } });
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* 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
|
+
*/
|
|
397
|
+
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
|
+
if (!options.testRunId) throw new Error("testRunId is required");
|
|
401
|
+
return this.request(`/user/projects/${options.projectId}/test-runs/${options.testRunId}/failure-clusters`);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 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
|
+
*/
|
|
414
|
+
async listUploadSessions(options) {
|
|
415
|
+
if (!this.isUserToken()) throw new Error("listUploadSessions requires a user API Key (gaf_).");
|
|
416
|
+
if (!options.projectId) throw new Error("projectId is required");
|
|
417
|
+
return this.request(`/user/projects/${options.projectId}/upload-sessions`, {
|
|
418
|
+
...options.commitSha && { commitSha: options.commitSha },
|
|
419
|
+
...options.branch && { branch: options.branch },
|
|
420
|
+
...options.limit && { limit: options.limit },
|
|
421
|
+
...options.offset && { offset: options.offset }
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* 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
|
+
*/
|
|
432
|
+
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
|
+
if (!options.sessionId) throw new Error("sessionId is required");
|
|
436
|
+
return this.request(`/user/projects/${options.projectId}/upload-sessions/${options.sessionId}`);
|
|
437
|
+
}
|
|
450
438
|
};
|
|
451
439
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region src/tools/compare-test-metrics.ts
|
|
442
|
+
/**
|
|
443
|
+
* Input schema for compare_test_metrics tool
|
|
444
|
+
*/
|
|
445
|
+
const compareTestMetricsInputSchema = {
|
|
446
|
+
projectId: z.string().describe("Project ID. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
|
|
447
|
+
testName: z.string().describe("The test name to compare. Can be the short name or full name including describe blocks."),
|
|
448
|
+
beforeCommit: z.string().optional().describe("Commit SHA for the \"before\" measurement. Use with afterCommit."),
|
|
449
|
+
afterCommit: z.string().optional().describe("Commit SHA for the \"after\" measurement. Use with beforeCommit."),
|
|
450
|
+
beforeRunId: z.string().optional().describe("Test run ID for the \"before\" measurement. Use with afterRunId."),
|
|
451
|
+
afterRunId: z.string().optional().describe("Test run ID for the \"after\" measurement. Use with beforeRunId.")
|
|
461
452
|
};
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Output schema for compare_test_metrics tool
|
|
455
|
+
*/
|
|
456
|
+
const compareTestMetricsOutputSchema = {
|
|
457
|
+
testName: z.string(),
|
|
458
|
+
before: z.object({
|
|
459
|
+
testRunId: z.string(),
|
|
460
|
+
commit: z.string().nullable(),
|
|
461
|
+
branch: z.string().nullable(),
|
|
462
|
+
status: z.enum([
|
|
463
|
+
"passed",
|
|
464
|
+
"failed",
|
|
465
|
+
"skipped"
|
|
466
|
+
]),
|
|
467
|
+
durationMs: z.number().nullable(),
|
|
468
|
+
createdAt: z.string()
|
|
469
|
+
}),
|
|
470
|
+
after: z.object({
|
|
471
|
+
testRunId: z.string(),
|
|
472
|
+
commit: z.string().nullable(),
|
|
473
|
+
branch: z.string().nullable(),
|
|
474
|
+
status: z.enum([
|
|
475
|
+
"passed",
|
|
476
|
+
"failed",
|
|
477
|
+
"skipped"
|
|
478
|
+
]),
|
|
479
|
+
durationMs: z.number().nullable(),
|
|
480
|
+
createdAt: z.string()
|
|
481
|
+
}),
|
|
482
|
+
change: z.object({
|
|
483
|
+
durationMs: z.number().nullable(),
|
|
484
|
+
percentChange: z.number().nullable(),
|
|
485
|
+
statusChanged: z.boolean()
|
|
486
|
+
})
|
|
485
487
|
};
|
|
488
|
+
/**
|
|
489
|
+
* Execute compare_test_metrics tool
|
|
490
|
+
*/
|
|
486
491
|
async function executeCompareTestMetrics(client, input) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
testName: input.testName,
|
|
505
|
-
beforeCommit: input.beforeCommit,
|
|
506
|
-
afterCommit: input.afterCommit,
|
|
507
|
-
beforeRunId: input.beforeRunId,
|
|
508
|
-
afterRunId: input.afterRunId
|
|
509
|
-
});
|
|
510
|
-
return response;
|
|
492
|
+
const hasCommits = input.beforeCommit && input.afterCommit;
|
|
493
|
+
const hasRunIds = input.beforeRunId && input.afterRunId;
|
|
494
|
+
if (!hasCommits && !hasRunIds) throw new Error("Must provide either (beforeCommit + afterCommit) or (beforeRunId + afterRunId)");
|
|
495
|
+
if (hasCommits) {
|
|
496
|
+
if (input.beforeCommit.trim().length === 0 || input.afterCommit.trim().length === 0) throw new Error("beforeCommit and afterCommit must not be empty strings");
|
|
497
|
+
}
|
|
498
|
+
if (hasRunIds) {
|
|
499
|
+
if (input.beforeRunId.trim().length === 0 || input.afterRunId.trim().length === 0) throw new Error("beforeRunId and afterRunId must not be empty strings");
|
|
500
|
+
}
|
|
501
|
+
return await client.compareTestMetrics({
|
|
502
|
+
projectId: input.projectId,
|
|
503
|
+
testName: input.testName,
|
|
504
|
+
beforeCommit: input.beforeCommit,
|
|
505
|
+
afterCommit: input.afterCommit,
|
|
506
|
+
beforeRunId: input.beforeRunId,
|
|
507
|
+
afterRunId: input.afterRunId
|
|
508
|
+
});
|
|
511
509
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
510
|
+
/**
|
|
511
|
+
* Tool metadata
|
|
512
|
+
*/
|
|
513
|
+
const compareTestMetricsMetadata = {
|
|
514
|
+
name: "compare_test_metrics",
|
|
515
|
+
title: "Compare Test Metrics",
|
|
516
|
+
description: `Compare test metrics between two commits or test runs.
|
|
516
517
|
|
|
517
518
|
Useful for measuring the impact of code changes on test performance or reliability.
|
|
518
519
|
|
|
@@ -549,46 +550,58 @@ Use cases:
|
|
|
549
550
|
Tip: Use get_test_history first to find the commit SHAs or test run IDs you want to compare.`
|
|
550
551
|
};
|
|
551
552
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
553
|
+
//#endregion
|
|
554
|
+
//#region src/tools/find-uncovered-failure-areas.ts
|
|
555
|
+
/**
|
|
556
|
+
* Input schema for find_uncovered_failure_areas tool
|
|
557
|
+
*/
|
|
558
|
+
const findUncoveredFailureAreasInputSchema = {
|
|
559
|
+
projectId: z.string().describe("Project ID to analyze. Required. Use list_projects to find project IDs."),
|
|
560
|
+
days: z.number().int().min(1).max(365).optional().describe("Number of days to analyze for test failures (default: 30)"),
|
|
561
|
+
coverageThreshold: z.number().min(0).max(100).optional().describe("Include files with coverage below this percentage (default: 80)")
|
|
558
562
|
};
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
563
|
+
/**
|
|
564
|
+
* Output schema for find_uncovered_failure_areas tool
|
|
565
|
+
*/
|
|
566
|
+
const findUncoveredFailureAreasOutputSchema = {
|
|
567
|
+
hasCoverage: z.boolean(),
|
|
568
|
+
hasTestResults: z.boolean(),
|
|
569
|
+
riskAreas: z.array(z.object({
|
|
570
|
+
filePath: z.string(),
|
|
571
|
+
coverage: z.number(),
|
|
572
|
+
failureCount: z.number(),
|
|
573
|
+
riskScore: z.number(),
|
|
574
|
+
testNames: z.array(z.string())
|
|
575
|
+
})),
|
|
576
|
+
message: z.string().optional()
|
|
570
577
|
};
|
|
578
|
+
/**
|
|
579
|
+
* Execute find_uncovered_failure_areas tool
|
|
580
|
+
*/
|
|
571
581
|
async function executeFindUncoveredFailureAreas(client, input) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
582
|
+
const response = await client.getCoverageRiskAreas({
|
|
583
|
+
projectId: input.projectId,
|
|
584
|
+
days: input.days,
|
|
585
|
+
coverageThreshold: input.coverageThreshold
|
|
586
|
+
});
|
|
587
|
+
return {
|
|
588
|
+
hasCoverage: response.hasCoverage,
|
|
589
|
+
hasTestResults: response.hasTestResults,
|
|
590
|
+
riskAreas: response.riskAreas,
|
|
591
|
+
message: response.message
|
|
592
|
+
};
|
|
583
593
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
594
|
+
/**
|
|
595
|
+
* Tool metadata
|
|
596
|
+
*/
|
|
597
|
+
const findUncoveredFailureAreasMetadata = {
|
|
598
|
+
name: "find_uncovered_failure_areas",
|
|
599
|
+
title: "Find Uncovered Failure Areas",
|
|
600
|
+
description: `Find areas of code that have both low coverage AND test failures.
|
|
588
601
|
|
|
589
602
|
This cross-references test failures with coverage data to identify high-risk
|
|
590
603
|
areas in your codebase that need attention. Files are ranked by a "risk score"
|
|
591
|
-
calculated as: (100 - coverage%)
|
|
604
|
+
calculated as: (100 - coverage%) × failureCount.
|
|
592
605
|
|
|
593
606
|
When using a user API Key (gaf_), you must provide a projectId.
|
|
594
607
|
Use list_projects first to find available project IDs.
|
|
@@ -605,56 +618,67 @@ Returns:
|
|
|
605
618
|
Use this to prioritize which parts of your codebase need better test coverage.`
|
|
606
619
|
};
|
|
607
620
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
621
|
+
//#endregion
|
|
622
|
+
//#region src/tools/get-coverage-for-file.ts
|
|
623
|
+
/**
|
|
624
|
+
* Input schema for get_coverage_for_file tool
|
|
625
|
+
*/
|
|
626
|
+
const getCoverageForFileInputSchema = {
|
|
627
|
+
projectId: z.string().describe("Project ID to get coverage for. Required. Use list_projects to find project IDs."),
|
|
628
|
+
filePath: z.string().describe("File path to get coverage for. Can be exact path or partial match.")
|
|
613
629
|
};
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
630
|
+
/**
|
|
631
|
+
* Output schema for get_coverage_for_file tool
|
|
632
|
+
*/
|
|
633
|
+
const getCoverageForFileOutputSchema = {
|
|
634
|
+
hasCoverage: z.boolean(),
|
|
635
|
+
files: z.array(z.object({
|
|
636
|
+
path: z.string(),
|
|
637
|
+
lines: z.object({
|
|
638
|
+
covered: z.number(),
|
|
639
|
+
total: z.number(),
|
|
640
|
+
percentage: z.number()
|
|
641
|
+
}),
|
|
642
|
+
branches: z.object({
|
|
643
|
+
covered: z.number(),
|
|
644
|
+
total: z.number(),
|
|
645
|
+
percentage: z.number()
|
|
646
|
+
}),
|
|
647
|
+
functions: z.object({
|
|
648
|
+
covered: z.number(),
|
|
649
|
+
total: z.number(),
|
|
650
|
+
percentage: z.number()
|
|
651
|
+
})
|
|
652
|
+
})),
|
|
653
|
+
message: z.string().optional()
|
|
635
654
|
};
|
|
655
|
+
/**
|
|
656
|
+
* Execute get_coverage_for_file tool
|
|
657
|
+
*/
|
|
636
658
|
async function executeGetCoverageForFile(client, input) {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
};
|
|
659
|
+
const response = await client.getCoverageFiles({
|
|
660
|
+
projectId: input.projectId,
|
|
661
|
+
filePath: input.filePath,
|
|
662
|
+
limit: 10
|
|
663
|
+
});
|
|
664
|
+
return {
|
|
665
|
+
hasCoverage: response.hasCoverage,
|
|
666
|
+
files: response.files.map((f) => ({
|
|
667
|
+
path: f.path,
|
|
668
|
+
lines: f.lines,
|
|
669
|
+
branches: f.branches,
|
|
670
|
+
functions: f.functions
|
|
671
|
+
})),
|
|
672
|
+
message: response.message
|
|
673
|
+
};
|
|
653
674
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
675
|
+
/**
|
|
676
|
+
* Tool metadata
|
|
677
|
+
*/
|
|
678
|
+
const getCoverageForFileMetadata = {
|
|
679
|
+
name: "get_coverage_for_file",
|
|
680
|
+
title: "Get Coverage for File",
|
|
681
|
+
description: `Get coverage metrics for a specific file or files matching a path pattern.
|
|
658
682
|
|
|
659
683
|
When using a user API Key (gaf_), you must provide a projectId.
|
|
660
684
|
Use list_projects first to find available project IDs.
|
|
@@ -680,50 +704,66 @@ heavily-imported files, and code handling auth/payments/data mutations.
|
|
|
680
704
|
Prioritize: high utilization + low coverage = highest impact.`
|
|
681
705
|
};
|
|
682
706
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
707
|
+
//#endregion
|
|
708
|
+
//#region src/tools/get-coverage-summary.ts
|
|
709
|
+
/**
|
|
710
|
+
* Input schema for get_coverage_summary tool
|
|
711
|
+
*/
|
|
712
|
+
const getCoverageSummaryInputSchema = {
|
|
713
|
+
projectId: z.string().describe("Project ID to get coverage for. Required. Use list_projects to find project IDs."),
|
|
714
|
+
days: z.number().int().min(1).max(365).optional().describe("Number of days to analyze for trends (default: 30)")
|
|
688
715
|
};
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
716
|
+
/**
|
|
717
|
+
* Output schema for get_coverage_summary tool
|
|
718
|
+
*/
|
|
719
|
+
const getCoverageSummaryOutputSchema = {
|
|
720
|
+
hasCoverage: z.boolean(),
|
|
721
|
+
current: z.object({
|
|
722
|
+
lines: z.number(),
|
|
723
|
+
branches: z.number(),
|
|
724
|
+
functions: z.number()
|
|
725
|
+
}).optional(),
|
|
726
|
+
trend: z.object({
|
|
727
|
+
direction: z.enum([
|
|
728
|
+
"up",
|
|
729
|
+
"down",
|
|
730
|
+
"stable"
|
|
731
|
+
]),
|
|
732
|
+
change: z.number()
|
|
733
|
+
}).optional(),
|
|
734
|
+
totalReports: z.number(),
|
|
735
|
+
latestReportDate: z.string().nullable().optional(),
|
|
736
|
+
lowestCoverageFiles: z.array(z.object({
|
|
737
|
+
path: z.string(),
|
|
738
|
+
coverage: z.number()
|
|
739
|
+
})).optional(),
|
|
740
|
+
message: z.string().optional()
|
|
707
741
|
};
|
|
742
|
+
/**
|
|
743
|
+
* Execute get_coverage_summary tool
|
|
744
|
+
*/
|
|
708
745
|
async function executeGetCoverageSummary(client, input) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
746
|
+
const response = await client.getCoverageSummary({
|
|
747
|
+
projectId: input.projectId,
|
|
748
|
+
days: input.days
|
|
749
|
+
});
|
|
750
|
+
return {
|
|
751
|
+
hasCoverage: response.hasCoverage,
|
|
752
|
+
current: response.current,
|
|
753
|
+
trend: response.trend,
|
|
754
|
+
totalReports: response.totalReports,
|
|
755
|
+
latestReportDate: response.latestReportDate,
|
|
756
|
+
lowestCoverageFiles: response.lowestCoverageFiles,
|
|
757
|
+
message: response.message
|
|
758
|
+
};
|
|
722
759
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
760
|
+
/**
|
|
761
|
+
* Tool metadata
|
|
762
|
+
*/
|
|
763
|
+
const getCoverageSummaryMetadata = {
|
|
764
|
+
name: "get_coverage_summary",
|
|
765
|
+
title: "Get Coverage Summary",
|
|
766
|
+
description: `Get the coverage metrics summary for a project.
|
|
727
767
|
|
|
728
768
|
When using a user API Key (gaf_), you must provide a projectId.
|
|
729
769
|
Use list_projects first to find available project IDs.
|
|
@@ -742,44 +782,127 @@ specific areas (e.g., "server/services", "src/api", "lib/core"). This helps iden
|
|
|
742
782
|
high-value targets in critical code paths rather than just the files with lowest coverage.`
|
|
743
783
|
};
|
|
744
784
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/tools/get-failure-clusters.ts
|
|
787
|
+
/**
|
|
788
|
+
* Input schema for get_failure_clusters tool
|
|
789
|
+
*/
|
|
790
|
+
const getFailureClustersInputSchema = {
|
|
791
|
+
projectId: z.string().describe("Project ID. Use list_projects to find project IDs."),
|
|
792
|
+
testRunId: z.string().describe("Test run ID to get failure clusters for. Use list_test_runs to find test run IDs.")
|
|
793
|
+
};
|
|
794
|
+
/**
|
|
795
|
+
* Output schema for get_failure_clusters tool
|
|
796
|
+
*/
|
|
797
|
+
const getFailureClustersOutputSchema = {
|
|
798
|
+
clusters: z.array(z.object({
|
|
799
|
+
representativeError: z.string(),
|
|
800
|
+
count: z.number(),
|
|
801
|
+
tests: z.array(z.object({
|
|
802
|
+
name: z.string(),
|
|
803
|
+
fullName: z.string(),
|
|
804
|
+
errorMessage: z.string(),
|
|
805
|
+
filePath: z.string().nullable()
|
|
806
|
+
})),
|
|
807
|
+
similarity: z.number()
|
|
808
|
+
})),
|
|
809
|
+
totalFailures: z.number()
|
|
752
810
|
};
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
811
|
+
/**
|
|
812
|
+
* Execute get_failure_clusters tool
|
|
813
|
+
*/
|
|
814
|
+
async function executeGetFailureClusters(client, input) {
|
|
815
|
+
return client.getFailureClusters({
|
|
816
|
+
projectId: input.projectId,
|
|
817
|
+
testRunId: input.testRunId
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Tool metadata
|
|
822
|
+
*/
|
|
823
|
+
const getFailureClustersMetadata = {
|
|
824
|
+
name: "get_failure_clusters",
|
|
825
|
+
title: "Get Failure Clusters",
|
|
826
|
+
description: `Group failed tests by root cause using error message similarity.
|
|
827
|
+
|
|
828
|
+
When using a user API Key (gaf_), you must provide a projectId.
|
|
829
|
+
Use list_projects to find available project IDs, and list_test_runs to find test run IDs.
|
|
830
|
+
|
|
831
|
+
Parameters:
|
|
832
|
+
- projectId (required): The project ID
|
|
833
|
+
- testRunId (required): The test run ID to analyze
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
- clusters: Array of failure clusters, each containing:
|
|
837
|
+
- representativeError: The error message representing this cluster
|
|
838
|
+
- count: Number of tests with this same root cause
|
|
839
|
+
- tests: Array of individual failed tests in this cluster
|
|
840
|
+
- name: Short test name
|
|
841
|
+
- fullName: Full test name including describe blocks
|
|
842
|
+
- errorMessage: The specific error message
|
|
843
|
+
- filePath: Test file path (null if not recorded)
|
|
844
|
+
- similarity: Similarity threshold used for clustering (0-1)
|
|
845
|
+
- totalFailures: Total number of failed tests across all clusters
|
|
846
|
+
|
|
847
|
+
Use cases:
|
|
848
|
+
- "Group these 15 failures by root cause" — often reveals 2-3 distinct bugs
|
|
849
|
+
- "Which error affects the most tests?" — fix the largest cluster first
|
|
850
|
+
- "Are these failures related?" — check if they land in the same cluster
|
|
851
|
+
|
|
852
|
+
Tip: Use get_test_run_details with status='failed' first to see raw failures,
|
|
853
|
+
then use this tool to understand which failures share the same root cause.`
|
|
766
854
|
};
|
|
855
|
+
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/tools/get-flaky-tests.ts
|
|
858
|
+
/**
|
|
859
|
+
* Input schema for get_flaky_tests tool
|
|
860
|
+
*/
|
|
861
|
+
const getFlakyTestsInputSchema = {
|
|
862
|
+
projectId: z.string().optional().describe("Project ID to get flaky tests for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
|
|
863
|
+
threshold: z.number().min(0).max(1).optional().describe("Minimum flip rate to be considered flaky (0-1, default: 0.1 = 10%)"),
|
|
864
|
+
limit: z.number().int().min(1).max(100).optional().describe("Maximum number of flaky tests to return (default: 50)"),
|
|
865
|
+
days: z.number().int().min(1).max(365).optional().describe("Analysis period in days (default: 30)")
|
|
866
|
+
};
|
|
867
|
+
/**
|
|
868
|
+
* Output schema for get_flaky_tests tool
|
|
869
|
+
*/
|
|
870
|
+
const getFlakyTestsOutputSchema = {
|
|
871
|
+
flakyTests: z.array(z.object({
|
|
872
|
+
name: z.string(),
|
|
873
|
+
flipRate: z.number(),
|
|
874
|
+
flipCount: z.number(),
|
|
875
|
+
totalRuns: z.number(),
|
|
876
|
+
lastSeen: z.string()
|
|
877
|
+
})),
|
|
878
|
+
summary: z.object({
|
|
879
|
+
threshold: z.number(),
|
|
880
|
+
totalFlaky: z.number(),
|
|
881
|
+
period: z.number()
|
|
882
|
+
})
|
|
883
|
+
};
|
|
884
|
+
/**
|
|
885
|
+
* Execute get_flaky_tests tool
|
|
886
|
+
*/
|
|
767
887
|
async function executeGetFlakyTests(client, input) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
888
|
+
const response = await client.getFlakyTests({
|
|
889
|
+
projectId: input.projectId,
|
|
890
|
+
threshold: input.threshold,
|
|
891
|
+
limit: input.limit,
|
|
892
|
+
days: input.days
|
|
893
|
+
});
|
|
894
|
+
return {
|
|
895
|
+
flakyTests: response.flakyTests,
|
|
896
|
+
summary: response.summary
|
|
897
|
+
};
|
|
778
898
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
899
|
+
/**
|
|
900
|
+
* Tool metadata
|
|
901
|
+
*/
|
|
902
|
+
const getFlakyTestsMetadata = {
|
|
903
|
+
name: "get_flaky_tests",
|
|
904
|
+
title: "Get Flaky Tests",
|
|
905
|
+
description: `Get the list of flaky tests in a project.
|
|
783
906
|
|
|
784
907
|
When using a user API Key (gaf_), you must provide a projectId.
|
|
785
908
|
Use list_projects first to find available project IDs.
|
|
@@ -800,44 +923,60 @@ Use this after get_project_health shows flaky tests exist, to identify which
|
|
|
800
923
|
specific tests are flaky and need investigation.`
|
|
801
924
|
};
|
|
802
925
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/tools/get-project-health.ts
|
|
928
|
+
/**
|
|
929
|
+
* Input schema for get_project_health tool
|
|
930
|
+
*/
|
|
931
|
+
const getProjectHealthInputSchema = {
|
|
932
|
+
projectId: z.string().optional().describe("Project ID to get health for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
|
|
933
|
+
days: z.number().int().min(1).max(365).optional().describe("Number of days to analyze (default: 30)")
|
|
808
934
|
};
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
935
|
+
/**
|
|
936
|
+
* Output schema for get_project_health tool
|
|
937
|
+
*/
|
|
938
|
+
const getProjectHealthOutputSchema = {
|
|
939
|
+
projectName: z.string(),
|
|
940
|
+
healthScore: z.number(),
|
|
941
|
+
passRate: z.number().nullable(),
|
|
942
|
+
testRunCount: z.number(),
|
|
943
|
+
flakyTestCount: z.number(),
|
|
944
|
+
trend: z.enum([
|
|
945
|
+
"up",
|
|
946
|
+
"down",
|
|
947
|
+
"stable"
|
|
948
|
+
]),
|
|
949
|
+
period: z.object({
|
|
950
|
+
days: z.number(),
|
|
951
|
+
start: z.string(),
|
|
952
|
+
end: z.string()
|
|
953
|
+
})
|
|
821
954
|
};
|
|
955
|
+
/**
|
|
956
|
+
* Execute get_project_health tool
|
|
957
|
+
*/
|
|
822
958
|
async function executeGetProjectHealth(client, input) {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
959
|
+
const response = await client.getProjectHealth({
|
|
960
|
+
projectId: input.projectId,
|
|
961
|
+
days: input.days
|
|
962
|
+
});
|
|
963
|
+
return {
|
|
964
|
+
projectName: response.analytics.projectName,
|
|
965
|
+
healthScore: response.analytics.healthScore,
|
|
966
|
+
passRate: response.analytics.passRate,
|
|
967
|
+
testRunCount: response.analytics.testRunCount,
|
|
968
|
+
flakyTestCount: response.analytics.flakyTestCount,
|
|
969
|
+
trend: response.analytics.trend,
|
|
970
|
+
period: response.analytics.period
|
|
971
|
+
};
|
|
836
972
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
973
|
+
/**
|
|
974
|
+
* Tool metadata
|
|
975
|
+
*/
|
|
976
|
+
const getProjectHealthMetadata = {
|
|
977
|
+
name: "get_project_health",
|
|
978
|
+
title: "Get Project Health",
|
|
979
|
+
description: `Get the health metrics for a project.
|
|
841
980
|
|
|
842
981
|
When using a user API Key (gaf_), you must provide a projectId.
|
|
843
982
|
Use list_projects first to find available project IDs.
|
|
@@ -852,38 +991,50 @@ Returns:
|
|
|
852
991
|
Use this to understand the current state of your test suite.`
|
|
853
992
|
};
|
|
854
993
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
994
|
+
//#endregion
|
|
995
|
+
//#region src/tools/get-report-browser-url.ts
|
|
996
|
+
/**
|
|
997
|
+
* Input schema for get_report_browser_url tool
|
|
998
|
+
*/
|
|
999
|
+
const getReportBrowserUrlInputSchema = {
|
|
1000
|
+
projectId: z.string().describe("Project ID the test run belongs to. Required. Use list_projects to find project IDs."),
|
|
1001
|
+
testRunId: z.string().describe("The test run ID to get the report URL for. Use list_test_runs to find test run IDs."),
|
|
1002
|
+
filename: z.string().optional().describe("Specific file to open (default: index.html or first HTML file)")
|
|
861
1003
|
};
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1004
|
+
/**
|
|
1005
|
+
* Output schema for get_report_browser_url tool
|
|
1006
|
+
*/
|
|
1007
|
+
const getReportBrowserUrlOutputSchema = {
|
|
1008
|
+
url: z.string(),
|
|
1009
|
+
filename: z.string(),
|
|
1010
|
+
testRunId: z.string(),
|
|
1011
|
+
expiresAt: z.string(),
|
|
1012
|
+
expiresInSeconds: z.number()
|
|
868
1013
|
};
|
|
1014
|
+
/**
|
|
1015
|
+
* Execute get_report_browser_url tool
|
|
1016
|
+
*/
|
|
869
1017
|
async function executeGetReportBrowserUrl(client, input) {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1018
|
+
const response = await client.getReportBrowserUrl({
|
|
1019
|
+
projectId: input.projectId,
|
|
1020
|
+
testRunId: input.testRunId,
|
|
1021
|
+
filename: input.filename
|
|
1022
|
+
});
|
|
1023
|
+
return {
|
|
1024
|
+
url: response.url,
|
|
1025
|
+
filename: response.filename,
|
|
1026
|
+
testRunId: response.testRunId,
|
|
1027
|
+
expiresAt: response.expiresAt,
|
|
1028
|
+
expiresInSeconds: response.expiresInSeconds
|
|
1029
|
+
};
|
|
882
1030
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1031
|
+
/**
|
|
1032
|
+
* Tool metadata
|
|
1033
|
+
*/
|
|
1034
|
+
const getReportBrowserUrlMetadata = {
|
|
1035
|
+
name: "get_report_browser_url",
|
|
1036
|
+
title: "Get Report Browser URL",
|
|
1037
|
+
description: `Get a browser-navigable URL for viewing a test report (Playwright, Vitest, etc.).
|
|
887
1038
|
|
|
888
1039
|
Returns a signed URL that can be opened directly in a browser without requiring
|
|
889
1040
|
the user to log in. The URL expires after 30 minutes for security.
|
|
@@ -906,44 +1057,54 @@ The returned URL can be shared with users who need to view the report.
|
|
|
906
1057
|
Note: URLs expire after 30 minutes for security.`
|
|
907
1058
|
};
|
|
908
1059
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1060
|
+
//#endregion
|
|
1061
|
+
//#region src/tools/get-report.ts
|
|
1062
|
+
/**
|
|
1063
|
+
* Input schema for get_report tool
|
|
1064
|
+
*/
|
|
1065
|
+
const getReportInputSchema = { testRunId: z.string().describe("The test run ID to get report files for. Use list_test_runs to find test run IDs.") };
|
|
1066
|
+
/**
|
|
1067
|
+
* Output schema for get_report tool
|
|
1068
|
+
*/
|
|
1069
|
+
const getReportOutputSchema = {
|
|
1070
|
+
testRunId: z.string(),
|
|
1071
|
+
projectId: z.string(),
|
|
1072
|
+
projectName: z.string(),
|
|
1073
|
+
resultSchema: z.string().optional(),
|
|
1074
|
+
files: z.array(z.object({
|
|
1075
|
+
filename: z.string(),
|
|
1076
|
+
size: z.number(),
|
|
1077
|
+
contentType: z.string(),
|
|
1078
|
+
downloadUrl: z.string()
|
|
1079
|
+
})),
|
|
1080
|
+
urlExpiresInSeconds: z.number().optional()
|
|
926
1081
|
};
|
|
1082
|
+
/**
|
|
1083
|
+
* Execute get_report tool
|
|
1084
|
+
*/
|
|
927
1085
|
async function executeGetReport(client, input) {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1086
|
+
const response = await client.getReport(input.testRunId);
|
|
1087
|
+
return {
|
|
1088
|
+
testRunId: response.testRunId,
|
|
1089
|
+
projectId: response.projectId,
|
|
1090
|
+
projectName: response.projectName,
|
|
1091
|
+
resultSchema: response.resultSchema,
|
|
1092
|
+
files: response.files.map((file) => ({
|
|
1093
|
+
filename: file.filename,
|
|
1094
|
+
size: file.size,
|
|
1095
|
+
contentType: file.contentType,
|
|
1096
|
+
downloadUrl: file.downloadUrl
|
|
1097
|
+
})),
|
|
1098
|
+
urlExpiresInSeconds: response.urlExpiresInSeconds
|
|
1099
|
+
};
|
|
942
1100
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1101
|
+
/**
|
|
1102
|
+
* Tool metadata
|
|
1103
|
+
*/
|
|
1104
|
+
const getReportMetadata = {
|
|
1105
|
+
name: "get_report",
|
|
1106
|
+
title: "Get Report Files",
|
|
1107
|
+
description: `Get URLs for report files uploaded with a test run.
|
|
947
1108
|
|
|
948
1109
|
IMPORTANT: This tool returns download URLs, not file content. You must fetch the URLs separately.
|
|
949
1110
|
|
|
@@ -979,57 +1140,69 @@ Use cases:
|
|
|
979
1140
|
- "Parse the JUnit XML results" (then WebFetch the XML URL)`
|
|
980
1141
|
};
|
|
981
1142
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1143
|
+
//#endregion
|
|
1144
|
+
//#region src/tools/get-slowest-tests.ts
|
|
1145
|
+
/**
|
|
1146
|
+
* Input schema for get_slowest_tests tool
|
|
1147
|
+
*/
|
|
1148
|
+
const getSlowestTestsInputSchema = {
|
|
1149
|
+
projectId: z.string().describe("Project ID to get slowest tests for. Required. Use list_projects to find project IDs."),
|
|
1150
|
+
days: z.number().int().min(1).max(365).optional().describe("Analysis period in days (default: 30)"),
|
|
1151
|
+
limit: z.number().int().min(1).max(100).optional().describe("Maximum number of tests to return (default: 20)"),
|
|
1152
|
+
framework: z.string().optional().describe("Filter by test framework (e.g., \"playwright\", \"vitest\", \"jest\")"),
|
|
1153
|
+
branch: z.string().optional().describe("Filter by git branch name (e.g., \"main\", \"develop\")")
|
|
990
1154
|
};
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1155
|
+
/**
|
|
1156
|
+
* Output schema for get_slowest_tests tool
|
|
1157
|
+
*/
|
|
1158
|
+
const getSlowestTestsOutputSchema = {
|
|
1159
|
+
slowestTests: z.array(z.object({
|
|
1160
|
+
name: z.string(),
|
|
1161
|
+
fullName: z.string(),
|
|
1162
|
+
filePath: z.string().optional(),
|
|
1163
|
+
framework: z.string().optional(),
|
|
1164
|
+
avgDurationMs: z.number(),
|
|
1165
|
+
p95DurationMs: z.number(),
|
|
1166
|
+
runCount: z.number()
|
|
1167
|
+
})),
|
|
1168
|
+
summary: z.object({
|
|
1169
|
+
projectId: z.string(),
|
|
1170
|
+
projectName: z.string(),
|
|
1171
|
+
period: z.number(),
|
|
1172
|
+
totalReturned: z.number()
|
|
1173
|
+
})
|
|
1007
1174
|
};
|
|
1175
|
+
/**
|
|
1176
|
+
* Execute get_slowest_tests tool
|
|
1177
|
+
*/
|
|
1008
1178
|
async function executeGetSlowestTests(client, input) {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1179
|
+
const response = await client.getSlowestTests({
|
|
1180
|
+
projectId: input.projectId,
|
|
1181
|
+
days: input.days,
|
|
1182
|
+
limit: input.limit,
|
|
1183
|
+
framework: input.framework,
|
|
1184
|
+
branch: input.branch
|
|
1185
|
+
});
|
|
1186
|
+
return {
|
|
1187
|
+
slowestTests: response.slowestTests.map((test) => ({
|
|
1188
|
+
name: test.name,
|
|
1189
|
+
fullName: test.fullName,
|
|
1190
|
+
filePath: test.filePath,
|
|
1191
|
+
framework: test.framework,
|
|
1192
|
+
avgDurationMs: test.avgDurationMs,
|
|
1193
|
+
p95DurationMs: test.p95DurationMs,
|
|
1194
|
+
runCount: test.runCount
|
|
1195
|
+
})),
|
|
1196
|
+
summary: response.summary
|
|
1197
|
+
};
|
|
1028
1198
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1199
|
+
/**
|
|
1200
|
+
* Tool metadata
|
|
1201
|
+
*/
|
|
1202
|
+
const getSlowestTestsMetadata = {
|
|
1203
|
+
name: "get_slowest_tests",
|
|
1204
|
+
title: "Get Slowest Tests",
|
|
1205
|
+
description: `Get the slowest tests in a project, sorted by P95 duration.
|
|
1033
1206
|
|
|
1034
1207
|
When using a user API Key (gaf_), you must provide a projectId.
|
|
1035
1208
|
Use list_projects first to find available project IDs.
|
|
@@ -1059,64 +1232,78 @@ Use cases:
|
|
|
1059
1232
|
- "What are the slowest tests on the main branch?"`
|
|
1060
1233
|
};
|
|
1061
1234
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1235
|
+
//#endregion
|
|
1236
|
+
//#region src/tools/get-test-history.ts
|
|
1237
|
+
/**
|
|
1238
|
+
* Input schema for get_test_history tool
|
|
1239
|
+
*/
|
|
1240
|
+
const getTestHistoryInputSchema = {
|
|
1241
|
+
projectId: z.string().optional().describe("Project ID to get test history for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
|
|
1242
|
+
testName: z.string().optional().describe("Exact test name to search for"),
|
|
1243
|
+
filePath: z.string().optional().describe("File path containing the test"),
|
|
1244
|
+
limit: z.number().int().min(1).max(100).optional().describe("Maximum number of results (default: 20)")
|
|
1069
1245
|
};
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1246
|
+
/**
|
|
1247
|
+
* Output schema for get_test_history tool
|
|
1248
|
+
*/
|
|
1249
|
+
const getTestHistoryOutputSchema = {
|
|
1250
|
+
history: z.array(z.object({
|
|
1251
|
+
testRunId: z.string(),
|
|
1252
|
+
createdAt: z.string(),
|
|
1253
|
+
branch: z.string().optional(),
|
|
1254
|
+
commitSha: z.string().optional(),
|
|
1255
|
+
status: z.enum([
|
|
1256
|
+
"passed",
|
|
1257
|
+
"failed",
|
|
1258
|
+
"skipped",
|
|
1259
|
+
"pending"
|
|
1260
|
+
]),
|
|
1261
|
+
durationMs: z.number(),
|
|
1262
|
+
message: z.string().optional()
|
|
1263
|
+
})),
|
|
1264
|
+
summary: z.object({
|
|
1265
|
+
totalRuns: z.number(),
|
|
1266
|
+
passedRuns: z.number(),
|
|
1267
|
+
failedRuns: z.number(),
|
|
1268
|
+
passRate: z.number().nullable()
|
|
1269
|
+
})
|
|
1086
1270
|
};
|
|
1271
|
+
/**
|
|
1272
|
+
* Execute get_test_history tool
|
|
1273
|
+
*/
|
|
1087
1274
|
async function executeGetTestHistory(client, input) {
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
passRate: response.summary.passRate
|
|
1113
|
-
}
|
|
1114
|
-
};
|
|
1275
|
+
if (!input.testName && !input.filePath) throw new Error("Either testName or filePath is required");
|
|
1276
|
+
const response = await client.getTestHistory({
|
|
1277
|
+
projectId: input.projectId,
|
|
1278
|
+
testName: input.testName,
|
|
1279
|
+
filePath: input.filePath,
|
|
1280
|
+
limit: input.limit || 20
|
|
1281
|
+
});
|
|
1282
|
+
return {
|
|
1283
|
+
history: response.history.map((entry) => ({
|
|
1284
|
+
testRunId: entry.testRunId,
|
|
1285
|
+
createdAt: entry.createdAt,
|
|
1286
|
+
branch: entry.branch,
|
|
1287
|
+
commitSha: entry.commitSha,
|
|
1288
|
+
status: entry.test.status,
|
|
1289
|
+
durationMs: entry.test.durationMs,
|
|
1290
|
+
message: entry.test.message || void 0
|
|
1291
|
+
})),
|
|
1292
|
+
summary: {
|
|
1293
|
+
totalRuns: response.summary.totalRuns,
|
|
1294
|
+
passedRuns: response.summary.passedRuns,
|
|
1295
|
+
failedRuns: response.summary.failedRuns,
|
|
1296
|
+
passRate: response.summary.passRate
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1115
1299
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1300
|
+
/**
|
|
1301
|
+
* Tool metadata
|
|
1302
|
+
*/
|
|
1303
|
+
const getTestHistoryMetadata = {
|
|
1304
|
+
name: "get_test_history",
|
|
1305
|
+
title: "Get Test History",
|
|
1306
|
+
description: `Get the pass/fail history for a specific test.
|
|
1120
1307
|
|
|
1121
1308
|
When using a user API Key (gaf_), you must provide a projectId.
|
|
1122
1309
|
Use list_projects first to find available project IDs.
|
|
@@ -1135,57 +1322,86 @@ Returns:
|
|
|
1135
1322
|
Use this to investigate flaky tests or understand test stability.`
|
|
1136
1323
|
};
|
|
1137
1324
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1325
|
+
//#endregion
|
|
1326
|
+
//#region src/tools/get-test-run-details.ts
|
|
1327
|
+
/**
|
|
1328
|
+
* Input schema for get_test_run_details tool
|
|
1329
|
+
*/
|
|
1330
|
+
const getTestRunDetailsInputSchema = {
|
|
1331
|
+
testRunId: z.string().describe("The test run ID to get details for. Use list_test_runs to find test run IDs."),
|
|
1332
|
+
projectId: z.string().describe("Project ID the test run belongs to. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
|
|
1333
|
+
status: z.enum([
|
|
1334
|
+
"passed",
|
|
1335
|
+
"failed",
|
|
1336
|
+
"skipped"
|
|
1337
|
+
]).optional().describe("Filter tests by status. Returns only tests matching this status."),
|
|
1338
|
+
limit: z.number().int().min(1).max(500).optional().describe("Maximum number of tests to return (default: 100, max: 500)"),
|
|
1339
|
+
offset: z.number().int().min(0).optional().describe("Number of tests to skip for pagination (default: 0)")
|
|
1146
1340
|
};
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1341
|
+
/**
|
|
1342
|
+
* Output schema for get_test_run_details tool
|
|
1343
|
+
*/
|
|
1344
|
+
const getTestRunDetailsOutputSchema = {
|
|
1345
|
+
testRunId: z.string(),
|
|
1346
|
+
commitSha: z.string().nullable(),
|
|
1347
|
+
branch: z.string().nullable(),
|
|
1348
|
+
framework: z.string().nullable(),
|
|
1349
|
+
createdAt: z.string(),
|
|
1350
|
+
summary: z.object({
|
|
1351
|
+
passed: z.number(),
|
|
1352
|
+
failed: z.number(),
|
|
1353
|
+
skipped: z.number(),
|
|
1354
|
+
total: z.number()
|
|
1355
|
+
}),
|
|
1356
|
+
tests: z.array(z.object({
|
|
1357
|
+
name: z.string(),
|
|
1358
|
+
fullName: z.string(),
|
|
1359
|
+
status: z.enum([
|
|
1360
|
+
"passed",
|
|
1361
|
+
"failed",
|
|
1362
|
+
"skipped"
|
|
1363
|
+
]),
|
|
1364
|
+
durationMs: z.number().nullable(),
|
|
1365
|
+
filePath: z.string().nullable(),
|
|
1366
|
+
error: z.string().nullable(),
|
|
1367
|
+
errorStack: z.string().nullable()
|
|
1368
|
+
})),
|
|
1369
|
+
pagination: z.object({
|
|
1370
|
+
total: z.number(),
|
|
1371
|
+
limit: z.number(),
|
|
1372
|
+
offset: z.number(),
|
|
1373
|
+
hasMore: z.boolean()
|
|
1374
|
+
})
|
|
1169
1375
|
};
|
|
1376
|
+
/**
|
|
1377
|
+
* Execute get_test_run_details tool
|
|
1378
|
+
*/
|
|
1170
1379
|
async function executeGetTestRunDetails(client, input) {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1380
|
+
const response = await client.getTestRunDetails({
|
|
1381
|
+
projectId: input.projectId,
|
|
1382
|
+
testRunId: input.testRunId,
|
|
1383
|
+
status: input.status,
|
|
1384
|
+
limit: input.limit,
|
|
1385
|
+
offset: input.offset
|
|
1386
|
+
});
|
|
1387
|
+
return {
|
|
1388
|
+
testRunId: response.testRunId,
|
|
1389
|
+
commitSha: response.commitSha,
|
|
1390
|
+
branch: response.branch,
|
|
1391
|
+
framework: response.framework,
|
|
1392
|
+
createdAt: response.createdAt,
|
|
1393
|
+
summary: response.summary,
|
|
1394
|
+
tests: response.tests,
|
|
1395
|
+
pagination: response.pagination
|
|
1396
|
+
};
|
|
1184
1397
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1398
|
+
/**
|
|
1399
|
+
* Tool metadata
|
|
1400
|
+
*/
|
|
1401
|
+
const getTestRunDetailsMetadata = {
|
|
1402
|
+
name: "get_test_run_details",
|
|
1403
|
+
title: "Get Test Run Details",
|
|
1404
|
+
description: `Get parsed test results for a specific test run.
|
|
1189
1405
|
|
|
1190
1406
|
When using a user API Key (gaf_), you must provide a projectId.
|
|
1191
1407
|
Use list_projects to find available project IDs, and list_test_runs to find test run IDs.
|
|
@@ -1199,6 +1415,10 @@ Parameters:
|
|
|
1199
1415
|
|
|
1200
1416
|
Returns:
|
|
1201
1417
|
- testRunId: The test run ID
|
|
1418
|
+
- commitSha: Git commit SHA (null if not recorded)
|
|
1419
|
+
- branch: Git branch name (null if not recorded)
|
|
1420
|
+
- framework: Test framework (e.g., "playwright", "vitest")
|
|
1421
|
+
- createdAt: When the test run was created (ISO 8601)
|
|
1202
1422
|
- summary: Overall counts (passed, failed, skipped, total)
|
|
1203
1423
|
- tests: Array of individual test results with:
|
|
1204
1424
|
- name: Short test name
|
|
@@ -1207,6 +1427,7 @@ Returns:
|
|
|
1207
1427
|
- durationMs: Test duration in milliseconds (null if not recorded)
|
|
1208
1428
|
- filePath: Test file path (null if not recorded)
|
|
1209
1429
|
- error: Error message for failed tests (null otherwise)
|
|
1430
|
+
- errorStack: Full stack trace for failed tests (null otherwise)
|
|
1210
1431
|
- pagination: Pagination info (total, limit, offset, hasMore)
|
|
1211
1432
|
|
|
1212
1433
|
Use cases:
|
|
@@ -1219,63 +1440,74 @@ Note: For aggregate analytics like flaky test detection or duration trends,
|
|
|
1219
1440
|
use get_test_history, get_flaky_tests, or get_slowest_tests instead.`
|
|
1220
1441
|
};
|
|
1221
1442
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1443
|
+
//#endregion
|
|
1444
|
+
//#region src/tools/get-untested-files.ts
|
|
1445
|
+
/**
|
|
1446
|
+
* Input schema for get_untested_files tool
|
|
1447
|
+
*/
|
|
1448
|
+
const getUntestedFilesInputSchema = {
|
|
1449
|
+
projectId: z.string().describe("Project ID to analyze. Required. Use list_projects to find project IDs."),
|
|
1450
|
+
maxCoverage: z.number().min(0).max(100).optional().describe("Maximum coverage percentage to include (default: 10 for \"untested\")"),
|
|
1451
|
+
limit: z.number().int().min(1).max(100).optional().describe("Maximum number of files to return (default: 20)")
|
|
1228
1452
|
};
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1453
|
+
/**
|
|
1454
|
+
* Output schema for get_untested_files tool
|
|
1455
|
+
*/
|
|
1456
|
+
const getUntestedFilesOutputSchema = {
|
|
1457
|
+
hasCoverage: z.boolean(),
|
|
1458
|
+
files: z.array(z.object({
|
|
1459
|
+
path: z.string(),
|
|
1460
|
+
lines: z.object({
|
|
1461
|
+
covered: z.number(),
|
|
1462
|
+
total: z.number(),
|
|
1463
|
+
percentage: z.number()
|
|
1464
|
+
}),
|
|
1465
|
+
branches: z.object({
|
|
1466
|
+
covered: z.number(),
|
|
1467
|
+
total: z.number(),
|
|
1468
|
+
percentage: z.number()
|
|
1469
|
+
}),
|
|
1470
|
+
functions: z.object({
|
|
1471
|
+
covered: z.number(),
|
|
1472
|
+
total: z.number(),
|
|
1473
|
+
percentage: z.number()
|
|
1474
|
+
})
|
|
1475
|
+
})),
|
|
1476
|
+
totalCount: z.number(),
|
|
1477
|
+
message: z.string().optional()
|
|
1251
1478
|
};
|
|
1479
|
+
/**
|
|
1480
|
+
* Execute get_untested_files tool
|
|
1481
|
+
*/
|
|
1252
1482
|
async function executeGetUntestedFiles(client, input) {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
};
|
|
1483
|
+
const maxCoverage = input.maxCoverage ?? 10;
|
|
1484
|
+
const limit = input.limit ?? 20;
|
|
1485
|
+
const response = await client.getCoverageFiles({
|
|
1486
|
+
projectId: input.projectId,
|
|
1487
|
+
maxCoverage,
|
|
1488
|
+
limit,
|
|
1489
|
+
sortBy: "coverage",
|
|
1490
|
+
sortOrder: "asc"
|
|
1491
|
+
});
|
|
1492
|
+
return {
|
|
1493
|
+
hasCoverage: response.hasCoverage,
|
|
1494
|
+
files: response.files.map((f) => ({
|
|
1495
|
+
path: f.path,
|
|
1496
|
+
lines: f.lines,
|
|
1497
|
+
branches: f.branches,
|
|
1498
|
+
functions: f.functions
|
|
1499
|
+
})),
|
|
1500
|
+
totalCount: response.pagination.total,
|
|
1501
|
+
message: response.message
|
|
1502
|
+
};
|
|
1274
1503
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1504
|
+
/**
|
|
1505
|
+
* Tool metadata
|
|
1506
|
+
*/
|
|
1507
|
+
const getUntestedFilesMetadata = {
|
|
1508
|
+
name: "get_untested_files",
|
|
1509
|
+
title: "Get Untested Files",
|
|
1510
|
+
description: `Get files with little or no test coverage.
|
|
1279
1511
|
|
|
1280
1512
|
Returns files sorted by coverage percentage (lowest first), filtered
|
|
1281
1513
|
to only include files below a coverage threshold.
|
|
@@ -1302,44 +1534,167 @@ To prioritize effectively, explore the codebase to understand which code is heav
|
|
|
1302
1534
|
for those specific paths.`
|
|
1303
1535
|
};
|
|
1304
1536
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1537
|
+
//#endregion
|
|
1538
|
+
//#region src/tools/get-upload-status.ts
|
|
1539
|
+
/**
|
|
1540
|
+
* Input schema for get_upload_status tool
|
|
1541
|
+
*/
|
|
1542
|
+
const getUploadStatusInputSchema = {
|
|
1543
|
+
projectId: z.string().describe("Project ID. Use list_projects to find project IDs."),
|
|
1544
|
+
sessionId: z.string().optional().describe("Specific upload session ID. If provided, returns detailed status for that session. Otherwise, lists recent sessions."),
|
|
1545
|
+
commitSha: z.string().optional().describe("Filter sessions by commit SHA. Useful for checking if results for a specific commit are ready."),
|
|
1546
|
+
branch: z.string().optional().describe("Filter sessions by branch name.")
|
|
1547
|
+
};
|
|
1548
|
+
/**
|
|
1549
|
+
* Output schema for get_upload_status tool
|
|
1550
|
+
*/
|
|
1551
|
+
const getUploadStatusOutputSchema = {
|
|
1552
|
+
sessions: z.array(z.object({
|
|
1553
|
+
id: z.string(),
|
|
1554
|
+
processingStatus: z.string(),
|
|
1555
|
+
commitSha: z.string().nullable(),
|
|
1556
|
+
branch: z.string().nullable(),
|
|
1557
|
+
pendingFileCount: z.number(),
|
|
1558
|
+
failedFileCount: z.number(),
|
|
1559
|
+
createdAt: z.string(),
|
|
1560
|
+
updatedAt: z.string()
|
|
1561
|
+
})).optional(),
|
|
1562
|
+
session: z.object({
|
|
1563
|
+
id: z.string(),
|
|
1564
|
+
processingStatus: z.string(),
|
|
1565
|
+
commitSha: z.string().nullable(),
|
|
1566
|
+
branch: z.string().nullable(),
|
|
1567
|
+
createdAt: z.string()
|
|
1568
|
+
}).optional(),
|
|
1569
|
+
testRuns: z.array(z.object({
|
|
1570
|
+
id: z.string(),
|
|
1571
|
+
framework: z.string().nullable(),
|
|
1572
|
+
summary: z.object({
|
|
1573
|
+
passed: z.number(),
|
|
1574
|
+
failed: z.number(),
|
|
1575
|
+
skipped: z.number(),
|
|
1576
|
+
total: z.number()
|
|
1577
|
+
}),
|
|
1578
|
+
createdAt: z.string()
|
|
1579
|
+
})).optional(),
|
|
1580
|
+
coverageReports: z.array(z.object({
|
|
1581
|
+
id: z.string(),
|
|
1582
|
+
format: z.string(),
|
|
1583
|
+
createdAt: z.string()
|
|
1584
|
+
})).optional(),
|
|
1585
|
+
pagination: z.object({
|
|
1586
|
+
total: z.number(),
|
|
1587
|
+
limit: z.number(),
|
|
1588
|
+
offset: z.number(),
|
|
1589
|
+
hasMore: z.boolean()
|
|
1590
|
+
}).optional()
|
|
1591
|
+
};
|
|
1592
|
+
/**
|
|
1593
|
+
* Execute get_upload_status tool
|
|
1594
|
+
*/
|
|
1595
|
+
async function executeGetUploadStatus(client, input) {
|
|
1596
|
+
if (input.sessionId) return client.getUploadSessionDetail({
|
|
1597
|
+
projectId: input.projectId,
|
|
1598
|
+
sessionId: input.sessionId
|
|
1599
|
+
});
|
|
1600
|
+
return client.listUploadSessions({
|
|
1601
|
+
projectId: input.projectId,
|
|
1602
|
+
commitSha: input.commitSha,
|
|
1603
|
+
branch: input.branch
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Tool metadata
|
|
1608
|
+
*/
|
|
1609
|
+
const getUploadStatusMetadata = {
|
|
1610
|
+
name: "get_upload_status",
|
|
1611
|
+
title: "Get Upload Status",
|
|
1612
|
+
description: `Check if CI results have been uploaded and processed.
|
|
1613
|
+
|
|
1614
|
+
Use this tool to answer "are my test results ready?" after pushing code.
|
|
1615
|
+
|
|
1616
|
+
Parameters:
|
|
1617
|
+
- projectId (required): The project ID
|
|
1618
|
+
- sessionId (optional): Specific upload session ID for detailed status
|
|
1619
|
+
- commitSha (optional): Filter by commit SHA to find uploads for a specific commit
|
|
1620
|
+
- branch (optional): Filter by branch name
|
|
1621
|
+
|
|
1622
|
+
Behavior:
|
|
1623
|
+
- If sessionId is provided: returns detailed status with linked test runs and coverage reports
|
|
1624
|
+
- Otherwise: returns a list of recent upload sessions (filtered by commitSha/branch if provided)
|
|
1625
|
+
|
|
1626
|
+
Processing statuses:
|
|
1627
|
+
- "pending" — upload received, processing not started
|
|
1628
|
+
- "processing" — files are being parsed
|
|
1629
|
+
- "completed" — all files processed successfully, results are ready
|
|
1630
|
+
- "error" — some files failed to process
|
|
1631
|
+
|
|
1632
|
+
Workflow:
|
|
1633
|
+
1. After pushing code, call with commitSha to find the upload session
|
|
1634
|
+
2. Check processingStatus — if "completed", results are ready
|
|
1635
|
+
3. If "processing" or "pending", wait and check again
|
|
1636
|
+
4. Once completed, use the linked testRunIds with get_test_run_details
|
|
1637
|
+
|
|
1638
|
+
Returns (list mode):
|
|
1639
|
+
- sessions: Array of upload sessions with processing status
|
|
1640
|
+
- pagination: Pagination info
|
|
1641
|
+
|
|
1642
|
+
Returns (detail mode):
|
|
1643
|
+
- session: Upload session details
|
|
1644
|
+
- testRuns: Linked test run summaries (id, framework, pass/fail counts)
|
|
1645
|
+
- coverageReports: Linked coverage report summaries (id, format)`
|
|
1310
1646
|
};
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
})
|
|
1321
|
-
})),
|
|
1322
|
-
total: z13.number()
|
|
1647
|
+
|
|
1648
|
+
//#endregion
|
|
1649
|
+
//#region src/tools/list-projects.ts
|
|
1650
|
+
/**
|
|
1651
|
+
* Input schema for list_projects tool
|
|
1652
|
+
*/
|
|
1653
|
+
const listProjectsInputSchema = {
|
|
1654
|
+
organizationId: z.string().optional().describe("Filter by organization ID (optional)"),
|
|
1655
|
+
limit: z.number().int().min(1).max(100).optional().describe("Maximum number of projects to return (default: 50)")
|
|
1323
1656
|
};
|
|
1657
|
+
/**
|
|
1658
|
+
* Output schema for list_projects tool
|
|
1659
|
+
*/
|
|
1660
|
+
const listProjectsOutputSchema = {
|
|
1661
|
+
projects: z.array(z.object({
|
|
1662
|
+
id: z.string(),
|
|
1663
|
+
name: z.string(),
|
|
1664
|
+
description: z.string().nullable().optional(),
|
|
1665
|
+
organization: z.object({
|
|
1666
|
+
id: z.string(),
|
|
1667
|
+
name: z.string(),
|
|
1668
|
+
slug: z.string()
|
|
1669
|
+
})
|
|
1670
|
+
})),
|
|
1671
|
+
total: z.number()
|
|
1672
|
+
};
|
|
1673
|
+
/**
|
|
1674
|
+
* Execute list_projects tool
|
|
1675
|
+
*/
|
|
1324
1676
|
async function executeListProjects(client, input) {
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1677
|
+
const response = await client.listProjects({
|
|
1678
|
+
organizationId: input.organizationId,
|
|
1679
|
+
limit: input.limit
|
|
1680
|
+
});
|
|
1681
|
+
return {
|
|
1682
|
+
projects: response.projects.map((p) => ({
|
|
1683
|
+
id: p.id,
|
|
1684
|
+
name: p.name,
|
|
1685
|
+
description: p.description,
|
|
1686
|
+
organization: p.organization
|
|
1687
|
+
})),
|
|
1688
|
+
total: response.pagination.total
|
|
1689
|
+
};
|
|
1338
1690
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1691
|
+
/**
|
|
1692
|
+
* Tool metadata
|
|
1693
|
+
*/
|
|
1694
|
+
const listProjectsMetadata = {
|
|
1695
|
+
name: "list_projects",
|
|
1696
|
+
title: "List Projects",
|
|
1697
|
+
description: `List all projects you have access to.
|
|
1343
1698
|
|
|
1344
1699
|
Returns a list of projects with their IDs, names, and organization info.
|
|
1345
1700
|
Use this to find project IDs for other tools like get_project_health.
|
|
@@ -1347,60 +1702,72 @@ Use this to find project IDs for other tools like get_project_health.
|
|
|
1347
1702
|
Requires a user API Key (gaf_). Get one from Account Settings in the Gaffer dashboard.`
|
|
1348
1703
|
};
|
|
1349
1704
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1705
|
+
//#endregion
|
|
1706
|
+
//#region src/tools/list-test-runs.ts
|
|
1707
|
+
/**
|
|
1708
|
+
* Input schema for list_test_runs tool
|
|
1709
|
+
*/
|
|
1710
|
+
const listTestRunsInputSchema = {
|
|
1711
|
+
projectId: z.string().optional().describe("Project ID to list test runs for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
|
|
1712
|
+
commitSha: z.string().optional().describe("Filter by commit SHA (exact or prefix match)"),
|
|
1713
|
+
branch: z.string().optional().describe("Filter by branch name"),
|
|
1714
|
+
status: z.enum(["passed", "failed"]).optional().describe("Filter by status: \"passed\" (no failures) or \"failed\" (has failures)"),
|
|
1715
|
+
limit: z.number().int().min(1).max(100).optional().describe("Maximum number of test runs to return (default: 20)")
|
|
1358
1716
|
};
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1717
|
+
/**
|
|
1718
|
+
* Output schema for list_test_runs tool
|
|
1719
|
+
*/
|
|
1720
|
+
const listTestRunsOutputSchema = {
|
|
1721
|
+
testRuns: z.array(z.object({
|
|
1722
|
+
id: z.string(),
|
|
1723
|
+
commitSha: z.string().optional(),
|
|
1724
|
+
branch: z.string().optional(),
|
|
1725
|
+
passedCount: z.number(),
|
|
1726
|
+
failedCount: z.number(),
|
|
1727
|
+
skippedCount: z.number(),
|
|
1728
|
+
totalCount: z.number(),
|
|
1729
|
+
createdAt: z.string()
|
|
1730
|
+
})),
|
|
1731
|
+
pagination: z.object({
|
|
1732
|
+
total: z.number(),
|
|
1733
|
+
hasMore: z.boolean()
|
|
1734
|
+
})
|
|
1374
1735
|
};
|
|
1736
|
+
/**
|
|
1737
|
+
* Execute list_test_runs tool
|
|
1738
|
+
*/
|
|
1375
1739
|
async function executeListTestRuns(client, input) {
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1740
|
+
const response = await client.getTestRuns({
|
|
1741
|
+
projectId: input.projectId,
|
|
1742
|
+
commitSha: input.commitSha,
|
|
1743
|
+
branch: input.branch,
|
|
1744
|
+
status: input.status,
|
|
1745
|
+
limit: input.limit || 20
|
|
1746
|
+
});
|
|
1747
|
+
return {
|
|
1748
|
+
testRuns: response.testRuns.map((run) => ({
|
|
1749
|
+
id: run.id,
|
|
1750
|
+
commitSha: run.commitSha || void 0,
|
|
1751
|
+
branch: run.branch || void 0,
|
|
1752
|
+
passedCount: run.summary.passed,
|
|
1753
|
+
failedCount: run.summary.failed,
|
|
1754
|
+
skippedCount: run.summary.skipped,
|
|
1755
|
+
totalCount: run.summary.total,
|
|
1756
|
+
createdAt: run.createdAt
|
|
1757
|
+
})),
|
|
1758
|
+
pagination: {
|
|
1759
|
+
total: response.pagination.total,
|
|
1760
|
+
hasMore: response.pagination.hasMore
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1399
1763
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1764
|
+
/**
|
|
1765
|
+
* Tool metadata
|
|
1766
|
+
*/
|
|
1767
|
+
const listTestRunsMetadata = {
|
|
1768
|
+
name: "list_test_runs",
|
|
1769
|
+
title: "List Test Runs",
|
|
1770
|
+
description: `List recent test runs for a project with optional filtering.
|
|
1404
1771
|
|
|
1405
1772
|
When using a user API Key (gaf_), you must provide a projectId.
|
|
1406
1773
|
Use list_projects first to find available project IDs.
|
|
@@ -1425,53 +1792,86 @@ Use cases:
|
|
|
1425
1792
|
- "What's the status of tests on my feature branch?"`
|
|
1426
1793
|
};
|
|
1427
1794
|
|
|
1428
|
-
|
|
1795
|
+
//#endregion
|
|
1796
|
+
//#region src/index.ts
|
|
1797
|
+
/**
|
|
1798
|
+
* Log error to stderr for observability
|
|
1799
|
+
* MCP uses stdout for communication, so stderr is safe for logging
|
|
1800
|
+
*/
|
|
1429
1801
|
function logError(toolName, error) {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
console.error(stack);
|
|
1436
|
-
}
|
|
1802
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1803
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1804
|
+
const stack = error instanceof Error ? error.stack : void 0;
|
|
1805
|
+
console.error(`[${timestamp}] [gaffer-mcp] ${toolName} failed: ${message}`);
|
|
1806
|
+
if (stack) console.error(stack);
|
|
1437
1807
|
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Handle tool error: log it and return MCP error response
|
|
1810
|
+
*/
|
|
1438
1811
|
function handleToolError(toolName, error) {
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1812
|
+
logError(toolName, error);
|
|
1813
|
+
return {
|
|
1814
|
+
content: [{
|
|
1815
|
+
type: "text",
|
|
1816
|
+
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1817
|
+
}],
|
|
1818
|
+
isError: true
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Register a tool with the MCP server using a consistent pattern.
|
|
1823
|
+
* Reduces boilerplate by handling error wrapping and response formatting.
|
|
1824
|
+
*/
|
|
1825
|
+
function registerTool(server, client, tool) {
|
|
1826
|
+
server.registerTool(tool.metadata.name, {
|
|
1827
|
+
title: tool.metadata.title,
|
|
1828
|
+
description: tool.metadata.description,
|
|
1829
|
+
inputSchema: tool.inputSchema,
|
|
1830
|
+
outputSchema: tool.outputSchema
|
|
1831
|
+
}, async (input) => {
|
|
1832
|
+
try {
|
|
1833
|
+
const output = await tool.execute(client, input);
|
|
1834
|
+
return {
|
|
1835
|
+
content: [{
|
|
1836
|
+
type: "text",
|
|
1837
|
+
text: JSON.stringify(output, null, 2)
|
|
1838
|
+
}],
|
|
1839
|
+
structuredContent: output
|
|
1840
|
+
};
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
return handleToolError(tool.metadata.name, error);
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1445
1845
|
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Gaffer MCP Server
|
|
1848
|
+
*
|
|
1849
|
+
* Provides AI assistants with access to test history and health metrics.
|
|
1850
|
+
*
|
|
1851
|
+
* Supports two authentication modes:
|
|
1852
|
+
* 1. User API Keys (gaf_) - Read-only access to all user's projects
|
|
1853
|
+
* Set via GAFFER_API_KEY environment variable
|
|
1854
|
+
* 2. Project Upload Tokens (gfr_) - Legacy, single project access
|
|
1855
|
+
*/
|
|
1446
1856
|
async function main() {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
process.exit(1);
|
|
1466
|
-
}
|
|
1467
|
-
const client = GafferApiClient.fromEnv();
|
|
1468
|
-
const server = new McpServer(
|
|
1469
|
-
{
|
|
1470
|
-
name: "gaffer",
|
|
1471
|
-
version: "0.1.0"
|
|
1472
|
-
},
|
|
1473
|
-
{
|
|
1474
|
-
instructions: `Gaffer provides test analytics and coverage data for your projects.
|
|
1857
|
+
if (!process.env.GAFFER_API_KEY) {
|
|
1858
|
+
console.error("Error: GAFFER_API_KEY environment variable is required");
|
|
1859
|
+
console.error("");
|
|
1860
|
+
console.error("Get your API Key from: https://app.gaffer.sh/account/api-keys");
|
|
1861
|
+
console.error("");
|
|
1862
|
+
console.error("Then configure Claude Code or Cursor with:");
|
|
1863
|
+
console.error(JSON.stringify({ mcpServers: { gaffer: {
|
|
1864
|
+
command: "npx",
|
|
1865
|
+
args: ["-y", "@gaffer-sh/mcp"],
|
|
1866
|
+
env: { GAFFER_API_KEY: "gaf_your-api-key-here" }
|
|
1867
|
+
} } }, null, 2));
|
|
1868
|
+
process.exit(1);
|
|
1869
|
+
}
|
|
1870
|
+
const client = GafferApiClient.fromEnv();
|
|
1871
|
+
const server = new McpServer({
|
|
1872
|
+
name: "gaffer",
|
|
1873
|
+
version: "0.1.0"
|
|
1874
|
+
}, { instructions: `Gaffer provides test analytics and coverage data for your projects.
|
|
1475
1875
|
|
|
1476
1876
|
## Coverage Analysis Best Practices
|
|
1477
1877
|
|
|
@@ -1489,293 +1889,153 @@ When helping users improve test coverage, combine coverage data with codebase ex
|
|
|
1489
1889
|
|
|
1490
1890
|
3. **Use path-based queries**: The get_untested_files tool may return many files of a certain type (e.g., UI components). For targeted analysis, use get_coverage_for_file with path prefixes to focus on specific areas of the codebase.
|
|
1491
1891
|
|
|
1492
|
-
4. **Iterate**: Get baseline
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
}
|
|
1634
|
-
);
|
|
1635
|
-
server.registerTool(
|
|
1636
|
-
getTestRunDetailsMetadata.name,
|
|
1637
|
-
{
|
|
1638
|
-
title: getTestRunDetailsMetadata.title,
|
|
1639
|
-
description: getTestRunDetailsMetadata.description,
|
|
1640
|
-
inputSchema: getTestRunDetailsInputSchema,
|
|
1641
|
-
outputSchema: getTestRunDetailsOutputSchema
|
|
1642
|
-
},
|
|
1643
|
-
async (input) => {
|
|
1644
|
-
try {
|
|
1645
|
-
const output = await executeGetTestRunDetails(client, input);
|
|
1646
|
-
return {
|
|
1647
|
-
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
1648
|
-
structuredContent: output
|
|
1649
|
-
};
|
|
1650
|
-
} catch (error) {
|
|
1651
|
-
return handleToolError(getTestRunDetailsMetadata.name, error);
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
);
|
|
1655
|
-
server.registerTool(
|
|
1656
|
-
compareTestMetricsMetadata.name,
|
|
1657
|
-
{
|
|
1658
|
-
title: compareTestMetricsMetadata.title,
|
|
1659
|
-
description: compareTestMetricsMetadata.description,
|
|
1660
|
-
inputSchema: compareTestMetricsInputSchema,
|
|
1661
|
-
outputSchema: compareTestMetricsOutputSchema
|
|
1662
|
-
},
|
|
1663
|
-
async (input) => {
|
|
1664
|
-
try {
|
|
1665
|
-
const output = await executeCompareTestMetrics(client, input);
|
|
1666
|
-
return {
|
|
1667
|
-
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
1668
|
-
structuredContent: output
|
|
1669
|
-
};
|
|
1670
|
-
} catch (error) {
|
|
1671
|
-
return handleToolError(compareTestMetricsMetadata.name, error);
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
);
|
|
1675
|
-
server.registerTool(
|
|
1676
|
-
getCoverageSummaryMetadata.name,
|
|
1677
|
-
{
|
|
1678
|
-
title: getCoverageSummaryMetadata.title,
|
|
1679
|
-
description: getCoverageSummaryMetadata.description,
|
|
1680
|
-
inputSchema: getCoverageSummaryInputSchema,
|
|
1681
|
-
outputSchema: getCoverageSummaryOutputSchema
|
|
1682
|
-
},
|
|
1683
|
-
async (input) => {
|
|
1684
|
-
try {
|
|
1685
|
-
const output = await executeGetCoverageSummary(client, input);
|
|
1686
|
-
return {
|
|
1687
|
-
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
1688
|
-
structuredContent: output
|
|
1689
|
-
};
|
|
1690
|
-
} catch (error) {
|
|
1691
|
-
return handleToolError(getCoverageSummaryMetadata.name, error);
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
);
|
|
1695
|
-
server.registerTool(
|
|
1696
|
-
getCoverageForFileMetadata.name,
|
|
1697
|
-
{
|
|
1698
|
-
title: getCoverageForFileMetadata.title,
|
|
1699
|
-
description: getCoverageForFileMetadata.description,
|
|
1700
|
-
inputSchema: getCoverageForFileInputSchema,
|
|
1701
|
-
outputSchema: getCoverageForFileOutputSchema
|
|
1702
|
-
},
|
|
1703
|
-
async (input) => {
|
|
1704
|
-
try {
|
|
1705
|
-
const output = await executeGetCoverageForFile(client, input);
|
|
1706
|
-
return {
|
|
1707
|
-
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
1708
|
-
structuredContent: output
|
|
1709
|
-
};
|
|
1710
|
-
} catch (error) {
|
|
1711
|
-
return handleToolError(getCoverageForFileMetadata.name, error);
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
);
|
|
1715
|
-
server.registerTool(
|
|
1716
|
-
findUncoveredFailureAreasMetadata.name,
|
|
1717
|
-
{
|
|
1718
|
-
title: findUncoveredFailureAreasMetadata.title,
|
|
1719
|
-
description: findUncoveredFailureAreasMetadata.description,
|
|
1720
|
-
inputSchema: findUncoveredFailureAreasInputSchema,
|
|
1721
|
-
outputSchema: findUncoveredFailureAreasOutputSchema
|
|
1722
|
-
},
|
|
1723
|
-
async (input) => {
|
|
1724
|
-
try {
|
|
1725
|
-
const output = await executeFindUncoveredFailureAreas(client, input);
|
|
1726
|
-
return {
|
|
1727
|
-
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
1728
|
-
structuredContent: output
|
|
1729
|
-
};
|
|
1730
|
-
} catch (error) {
|
|
1731
|
-
return handleToolError(findUncoveredFailureAreasMetadata.name, error);
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
);
|
|
1735
|
-
server.registerTool(
|
|
1736
|
-
getUntestedFilesMetadata.name,
|
|
1737
|
-
{
|
|
1738
|
-
title: getUntestedFilesMetadata.title,
|
|
1739
|
-
description: getUntestedFilesMetadata.description,
|
|
1740
|
-
inputSchema: getUntestedFilesInputSchema,
|
|
1741
|
-
outputSchema: getUntestedFilesOutputSchema
|
|
1742
|
-
},
|
|
1743
|
-
async (input) => {
|
|
1744
|
-
try {
|
|
1745
|
-
const output = await executeGetUntestedFiles(client, input);
|
|
1746
|
-
return {
|
|
1747
|
-
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
1748
|
-
structuredContent: output
|
|
1749
|
-
};
|
|
1750
|
-
} catch (error) {
|
|
1751
|
-
return handleToolError(getUntestedFilesMetadata.name, error);
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
);
|
|
1755
|
-
server.registerTool(
|
|
1756
|
-
getReportBrowserUrlMetadata.name,
|
|
1757
|
-
{
|
|
1758
|
-
title: getReportBrowserUrlMetadata.title,
|
|
1759
|
-
description: getReportBrowserUrlMetadata.description,
|
|
1760
|
-
inputSchema: getReportBrowserUrlInputSchema,
|
|
1761
|
-
outputSchema: getReportBrowserUrlOutputSchema
|
|
1762
|
-
},
|
|
1763
|
-
async (input) => {
|
|
1764
|
-
try {
|
|
1765
|
-
const output = await executeGetReportBrowserUrl(client, input);
|
|
1766
|
-
return {
|
|
1767
|
-
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
1768
|
-
structuredContent: output
|
|
1769
|
-
};
|
|
1770
|
-
} catch (error) {
|
|
1771
|
-
return handleToolError(getReportBrowserUrlMetadata.name, error);
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
);
|
|
1775
|
-
const transport = new StdioServerTransport();
|
|
1776
|
-
await server.connect(transport);
|
|
1892
|
+
4. **Iterate**: Get baseline → identify targets → write tests → re-check coverage after CI uploads new results.
|
|
1893
|
+
|
|
1894
|
+
## Finding Invisible Files
|
|
1895
|
+
|
|
1896
|
+
Coverage tools can only report on files that were loaded during test execution. Some files have 0% coverage but don't appear in reports at all - these are "invisible" files that were never imported.
|
|
1897
|
+
|
|
1898
|
+
To find invisible files:
|
|
1899
|
+
1. Use get_coverage_for_file with a path prefix (e.g., "server/") to see what Gaffer tracks
|
|
1900
|
+
2. Use the local Glob tool to list all source files in that path
|
|
1901
|
+
3. Compare the lists - files in local but NOT in Gaffer are invisible
|
|
1902
|
+
4. These files need tests that actually import them
|
|
1903
|
+
|
|
1904
|
+
Example: If get_coverage_for_file("server/api") returns user.ts, auth.ts, but Glob finds user.ts, auth.ts, billing.ts - then billing.ts is invisible and needs tests that import it.
|
|
1905
|
+
|
|
1906
|
+
## Agentic CI / Test Failure Diagnosis
|
|
1907
|
+
|
|
1908
|
+
When helping diagnose CI failures or fix failing tests:
|
|
1909
|
+
|
|
1910
|
+
1. **Check flakiness first**: Use get_flaky_tests to identify non-deterministic tests.
|
|
1911
|
+
Skip flaky tests unless the user specifically wants to stabilize them.
|
|
1912
|
+
|
|
1913
|
+
2. **Get failure details**: Use get_test_run_details with status='failed'
|
|
1914
|
+
to see error messages and stack traces for failing tests.
|
|
1915
|
+
|
|
1916
|
+
3. **Group by root cause**: Use get_failure_clusters to see which failures
|
|
1917
|
+
share the same underlying error — fix the root cause, not individual tests.
|
|
1918
|
+
|
|
1919
|
+
4. **Check history**: Use get_test_history to understand if the failure is new
|
|
1920
|
+
(regression) or recurring (existing bug).
|
|
1921
|
+
|
|
1922
|
+
5. **Verify fixes**: After code changes, use compare_test_metrics to confirm
|
|
1923
|
+
the specific test now passes.
|
|
1924
|
+
|
|
1925
|
+
6. **Prioritize by risk**: Use find_uncovered_failure_areas to identify
|
|
1926
|
+
which failing code has the lowest test coverage — fix those first.
|
|
1927
|
+
|
|
1928
|
+
## Checking Upload Status
|
|
1929
|
+
|
|
1930
|
+
When an agent needs to know if CI results are ready:
|
|
1931
|
+
|
|
1932
|
+
1. Use get_upload_status with commitSha or branch to find upload sessions
|
|
1933
|
+
2. Check processingStatus: "completed" means results are ready, "processing" means wait
|
|
1934
|
+
3. Once completed, use the linked testRunIds to get test results` });
|
|
1935
|
+
registerTool(server, client, {
|
|
1936
|
+
metadata: getProjectHealthMetadata,
|
|
1937
|
+
inputSchema: getProjectHealthInputSchema,
|
|
1938
|
+
outputSchema: getProjectHealthOutputSchema,
|
|
1939
|
+
execute: executeGetProjectHealth
|
|
1940
|
+
});
|
|
1941
|
+
registerTool(server, client, {
|
|
1942
|
+
metadata: getTestHistoryMetadata,
|
|
1943
|
+
inputSchema: getTestHistoryInputSchema,
|
|
1944
|
+
outputSchema: getTestHistoryOutputSchema,
|
|
1945
|
+
execute: executeGetTestHistory
|
|
1946
|
+
});
|
|
1947
|
+
registerTool(server, client, {
|
|
1948
|
+
metadata: getFlakyTestsMetadata,
|
|
1949
|
+
inputSchema: getFlakyTestsInputSchema,
|
|
1950
|
+
outputSchema: getFlakyTestsOutputSchema,
|
|
1951
|
+
execute: executeGetFlakyTests
|
|
1952
|
+
});
|
|
1953
|
+
registerTool(server, client, {
|
|
1954
|
+
metadata: listTestRunsMetadata,
|
|
1955
|
+
inputSchema: listTestRunsInputSchema,
|
|
1956
|
+
outputSchema: listTestRunsOutputSchema,
|
|
1957
|
+
execute: executeListTestRuns
|
|
1958
|
+
});
|
|
1959
|
+
registerTool(server, client, {
|
|
1960
|
+
metadata: listProjectsMetadata,
|
|
1961
|
+
inputSchema: listProjectsInputSchema,
|
|
1962
|
+
outputSchema: listProjectsOutputSchema,
|
|
1963
|
+
execute: executeListProjects
|
|
1964
|
+
});
|
|
1965
|
+
registerTool(server, client, {
|
|
1966
|
+
metadata: getReportMetadata,
|
|
1967
|
+
inputSchema: getReportInputSchema,
|
|
1968
|
+
outputSchema: getReportOutputSchema,
|
|
1969
|
+
execute: executeGetReport
|
|
1970
|
+
});
|
|
1971
|
+
registerTool(server, client, {
|
|
1972
|
+
metadata: getSlowestTestsMetadata,
|
|
1973
|
+
inputSchema: getSlowestTestsInputSchema,
|
|
1974
|
+
outputSchema: getSlowestTestsOutputSchema,
|
|
1975
|
+
execute: executeGetSlowestTests
|
|
1976
|
+
});
|
|
1977
|
+
registerTool(server, client, {
|
|
1978
|
+
metadata: getTestRunDetailsMetadata,
|
|
1979
|
+
inputSchema: getTestRunDetailsInputSchema,
|
|
1980
|
+
outputSchema: getTestRunDetailsOutputSchema,
|
|
1981
|
+
execute: executeGetTestRunDetails
|
|
1982
|
+
});
|
|
1983
|
+
registerTool(server, client, {
|
|
1984
|
+
metadata: getFailureClustersMetadata,
|
|
1985
|
+
inputSchema: getFailureClustersInputSchema,
|
|
1986
|
+
outputSchema: getFailureClustersOutputSchema,
|
|
1987
|
+
execute: executeGetFailureClusters
|
|
1988
|
+
});
|
|
1989
|
+
registerTool(server, client, {
|
|
1990
|
+
metadata: compareTestMetricsMetadata,
|
|
1991
|
+
inputSchema: compareTestMetricsInputSchema,
|
|
1992
|
+
outputSchema: compareTestMetricsOutputSchema,
|
|
1993
|
+
execute: executeCompareTestMetrics
|
|
1994
|
+
});
|
|
1995
|
+
registerTool(server, client, {
|
|
1996
|
+
metadata: getCoverageSummaryMetadata,
|
|
1997
|
+
inputSchema: getCoverageSummaryInputSchema,
|
|
1998
|
+
outputSchema: getCoverageSummaryOutputSchema,
|
|
1999
|
+
execute: executeGetCoverageSummary
|
|
2000
|
+
});
|
|
2001
|
+
registerTool(server, client, {
|
|
2002
|
+
metadata: getCoverageForFileMetadata,
|
|
2003
|
+
inputSchema: getCoverageForFileInputSchema,
|
|
2004
|
+
outputSchema: getCoverageForFileOutputSchema,
|
|
2005
|
+
execute: executeGetCoverageForFile
|
|
2006
|
+
});
|
|
2007
|
+
registerTool(server, client, {
|
|
2008
|
+
metadata: findUncoveredFailureAreasMetadata,
|
|
2009
|
+
inputSchema: findUncoveredFailureAreasInputSchema,
|
|
2010
|
+
outputSchema: findUncoveredFailureAreasOutputSchema,
|
|
2011
|
+
execute: executeFindUncoveredFailureAreas
|
|
2012
|
+
});
|
|
2013
|
+
registerTool(server, client, {
|
|
2014
|
+
metadata: getUntestedFilesMetadata,
|
|
2015
|
+
inputSchema: getUntestedFilesInputSchema,
|
|
2016
|
+
outputSchema: getUntestedFilesOutputSchema,
|
|
2017
|
+
execute: executeGetUntestedFiles
|
|
2018
|
+
});
|
|
2019
|
+
registerTool(server, client, {
|
|
2020
|
+
metadata: getReportBrowserUrlMetadata,
|
|
2021
|
+
inputSchema: getReportBrowserUrlInputSchema,
|
|
2022
|
+
outputSchema: getReportBrowserUrlOutputSchema,
|
|
2023
|
+
execute: executeGetReportBrowserUrl
|
|
2024
|
+
});
|
|
2025
|
+
registerTool(server, client, {
|
|
2026
|
+
metadata: getUploadStatusMetadata,
|
|
2027
|
+
inputSchema: getUploadStatusInputSchema,
|
|
2028
|
+
outputSchema: getUploadStatusOutputSchema,
|
|
2029
|
+
execute: executeGetUploadStatus
|
|
2030
|
+
});
|
|
2031
|
+
const transport = new StdioServerTransport();
|
|
2032
|
+
await server.connect(transport);
|
|
1777
2033
|
}
|
|
1778
2034
|
main().catch((error) => {
|
|
1779
|
-
|
|
1780
|
-
|
|
2035
|
+
console.error("Fatal error:", error);
|
|
2036
|
+
process.exit(1);
|
|
1781
2037
|
});
|
|
2038
|
+
|
|
2039
|
+
//#endregion
|
|
2040
|
+
export { };
|
|
2041
|
+
//# sourceMappingURL=index.js.map
|