@gaffer-sh/mcp 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +511 -183
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6
6
|
|
|
7
7
|
// src/api-client.ts
|
|
8
8
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
9
|
+
var MAX_RETRIES = 3;
|
|
10
|
+
var INITIAL_RETRY_DELAY_MS = 1e3;
|
|
11
|
+
var RETRYABLE_STATUS_CODES = [401, 429, 500, 502, 503, 504];
|
|
12
|
+
function sleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
9
15
|
function detectTokenType(token) {
|
|
10
16
|
if (token.startsWith("gaf_")) {
|
|
11
17
|
return "user";
|
|
@@ -42,7 +48,7 @@ var GafferApiClient = class _GafferApiClient {
|
|
|
42
48
|
return this.tokenType === "user";
|
|
43
49
|
}
|
|
44
50
|
/**
|
|
45
|
-
* Make authenticated request to Gaffer API
|
|
51
|
+
* Make authenticated request to Gaffer API with retry logic
|
|
46
52
|
*/
|
|
47
53
|
async request(endpoint, params) {
|
|
48
54
|
const url = new URL(`/api/v1${endpoint}`, this.baseUrl);
|
|
@@ -53,32 +59,59 @@ var GafferApiClient = class _GafferApiClient {
|
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
let lastError = null;
|
|
63
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(url.toString(), {
|
|
68
|
+
method: "GET",
|
|
69
|
+
headers: {
|
|
70
|
+
"X-API-Key": this.apiKey,
|
|
71
|
+
"Accept": "application/json",
|
|
72
|
+
"User-Agent": "gaffer-mcp/0.2.1"
|
|
73
|
+
},
|
|
74
|
+
signal: controller.signal
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const errorData = await response.json().catch(() => ({}));
|
|
78
|
+
if (RETRYABLE_STATUS_CODES.includes(response.status) && attempt < MAX_RETRIES) {
|
|
79
|
+
let delayMs = INITIAL_RETRY_DELAY_MS * 2 ** attempt;
|
|
80
|
+
if (response.status === 429) {
|
|
81
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
82
|
+
if (retryAfter) {
|
|
83
|
+
delayMs = Math.max(delayMs, Number.parseInt(retryAfter, 10) * 1e3);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
lastError = new Error(errorData.error?.message || `API request failed: ${response.status}`);
|
|
87
|
+
await sleep(delayMs);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const errorMessage = errorData.error?.message || `API request failed: ${response.status}`;
|
|
91
|
+
throw new Error(errorMessage);
|
|
92
|
+
}
|
|
93
|
+
return response.json();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
clearTimeout(timeoutId);
|
|
96
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
97
|
+
lastError = new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`);
|
|
98
|
+
if (attempt < MAX_RETRIES) {
|
|
99
|
+
await sleep(INITIAL_RETRY_DELAY_MS * 2 ** attempt);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
throw lastError;
|
|
103
|
+
}
|
|
104
|
+
if (error instanceof TypeError && attempt < MAX_RETRIES) {
|
|
105
|
+
lastError = error;
|
|
106
|
+
await sleep(INITIAL_RETRY_DELAY_MS * 2 ** attempt);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
} finally {
|
|
111
|
+
clearTimeout(timeoutId);
|
|
77
112
|
}
|
|
78
|
-
throw error;
|
|
79
|
-
} finally {
|
|
80
|
-
clearTimeout(timeoutId);
|
|
81
113
|
}
|
|
114
|
+
throw lastError || new Error("Request failed after retries");
|
|
82
115
|
}
|
|
83
116
|
/**
|
|
84
117
|
* List all projects the user has access to
|
|
@@ -228,6 +261,7 @@ var GafferApiClient = class _GafferApiClient {
|
|
|
228
261
|
* @param options.days - Analysis period in days (default: 30)
|
|
229
262
|
* @param options.limit - Maximum number of results (default: 20)
|
|
230
263
|
* @param options.framework - Filter by test framework
|
|
264
|
+
* @param options.branch - Filter by git branch name
|
|
231
265
|
* @returns Slowest tests sorted by P95 duration
|
|
232
266
|
*/
|
|
233
267
|
async getSlowestTests(options) {
|
|
@@ -240,31 +274,195 @@ var GafferApiClient = class _GafferApiClient {
|
|
|
240
274
|
return this.request(`/user/projects/${options.projectId}/slowest-tests`, {
|
|
241
275
|
...options.days && { days: options.days },
|
|
242
276
|
...options.limit && { limit: options.limit },
|
|
243
|
-
...options.framework && { framework: options.framework }
|
|
277
|
+
...options.framework && { framework: options.framework },
|
|
278
|
+
...options.branch && { branch: options.branch }
|
|
244
279
|
});
|
|
245
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Get parsed test results for a specific test run
|
|
283
|
+
*
|
|
284
|
+
* @param options - Query options
|
|
285
|
+
* @param options.projectId - The project ID (required)
|
|
286
|
+
* @param options.testRunId - The test run ID (required)
|
|
287
|
+
* @param options.status - Filter by test status ('passed', 'failed', 'skipped')
|
|
288
|
+
* @param options.limit - Maximum number of results (default: 100)
|
|
289
|
+
* @param options.offset - Pagination offset (default: 0)
|
|
290
|
+
* @returns Parsed test cases with pagination
|
|
291
|
+
*/
|
|
292
|
+
async getTestRunDetails(options) {
|
|
293
|
+
if (!this.isUserToken()) {
|
|
294
|
+
throw new Error("getTestRunDetails requires a user API Key (gaf_).");
|
|
295
|
+
}
|
|
296
|
+
if (!options.projectId) {
|
|
297
|
+
throw new Error("projectId is required");
|
|
298
|
+
}
|
|
299
|
+
if (!options.testRunId) {
|
|
300
|
+
throw new Error("testRunId is required");
|
|
301
|
+
}
|
|
302
|
+
return this.request(
|
|
303
|
+
`/user/projects/${options.projectId}/test-runs/${options.testRunId}/details`,
|
|
304
|
+
{
|
|
305
|
+
...options.status && { status: options.status },
|
|
306
|
+
...options.limit && { limit: options.limit },
|
|
307
|
+
...options.offset && { offset: options.offset }
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Compare test metrics between two commits or test runs
|
|
313
|
+
*
|
|
314
|
+
* @param options - Query options
|
|
315
|
+
* @param options.projectId - The project ID (required)
|
|
316
|
+
* @param options.testName - The test name to compare (required)
|
|
317
|
+
* @param options.beforeCommit - Commit SHA for before (use with afterCommit)
|
|
318
|
+
* @param options.afterCommit - Commit SHA for after (use with beforeCommit)
|
|
319
|
+
* @param options.beforeRunId - Test run ID for before (use with afterRunId)
|
|
320
|
+
* @param options.afterRunId - Test run ID for after (use with beforeRunId)
|
|
321
|
+
* @returns Comparison of test metrics
|
|
322
|
+
*/
|
|
323
|
+
async compareTestMetrics(options) {
|
|
324
|
+
if (!this.isUserToken()) {
|
|
325
|
+
throw new Error("compareTestMetrics requires a user API Key (gaf_).");
|
|
326
|
+
}
|
|
327
|
+
if (!options.projectId) {
|
|
328
|
+
throw new Error("projectId is required");
|
|
329
|
+
}
|
|
330
|
+
if (!options.testName) {
|
|
331
|
+
throw new Error("testName is required");
|
|
332
|
+
}
|
|
333
|
+
return this.request(
|
|
334
|
+
`/user/projects/${options.projectId}/compare-test`,
|
|
335
|
+
{
|
|
336
|
+
testName: options.testName,
|
|
337
|
+
...options.beforeCommit && { beforeCommit: options.beforeCommit },
|
|
338
|
+
...options.afterCommit && { afterCommit: options.afterCommit },
|
|
339
|
+
...options.beforeRunId && { beforeRunId: options.beforeRunId },
|
|
340
|
+
...options.afterRunId && { afterRunId: options.afterRunId }
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
}
|
|
246
344
|
};
|
|
247
345
|
|
|
248
|
-
// src/tools/
|
|
346
|
+
// src/tools/compare-test-metrics.ts
|
|
249
347
|
import { z } from "zod";
|
|
348
|
+
var compareTestMetricsInputSchema = {
|
|
349
|
+
projectId: z.string().describe("Project ID. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
|
|
350
|
+
testName: z.string().describe("The test name to compare. Can be the short name or full name including describe blocks."),
|
|
351
|
+
beforeCommit: z.string().optional().describe('Commit SHA for the "before" measurement. Use with afterCommit.'),
|
|
352
|
+
afterCommit: z.string().optional().describe('Commit SHA for the "after" measurement. Use with beforeCommit.'),
|
|
353
|
+
beforeRunId: z.string().optional().describe('Test run ID for the "before" measurement. Use with afterRunId.'),
|
|
354
|
+
afterRunId: z.string().optional().describe('Test run ID for the "after" measurement. Use with beforeRunId.')
|
|
355
|
+
};
|
|
356
|
+
var compareTestMetricsOutputSchema = {
|
|
357
|
+
testName: z.string(),
|
|
358
|
+
before: z.object({
|
|
359
|
+
testRunId: z.string(),
|
|
360
|
+
commit: z.string().nullable(),
|
|
361
|
+
branch: z.string().nullable(),
|
|
362
|
+
status: z.enum(["passed", "failed", "skipped"]),
|
|
363
|
+
durationMs: z.number().nullable(),
|
|
364
|
+
createdAt: z.string()
|
|
365
|
+
}),
|
|
366
|
+
after: z.object({
|
|
367
|
+
testRunId: z.string(),
|
|
368
|
+
commit: z.string().nullable(),
|
|
369
|
+
branch: z.string().nullable(),
|
|
370
|
+
status: z.enum(["passed", "failed", "skipped"]),
|
|
371
|
+
durationMs: z.number().nullable(),
|
|
372
|
+
createdAt: z.string()
|
|
373
|
+
}),
|
|
374
|
+
change: z.object({
|
|
375
|
+
durationMs: z.number().nullable(),
|
|
376
|
+
percentChange: z.number().nullable(),
|
|
377
|
+
statusChanged: z.boolean()
|
|
378
|
+
})
|
|
379
|
+
};
|
|
380
|
+
async function executeCompareTestMetrics(client, input) {
|
|
381
|
+
const hasCommits = input.beforeCommit && input.afterCommit;
|
|
382
|
+
const hasRunIds = input.beforeRunId && input.afterRunId;
|
|
383
|
+
if (!hasCommits && !hasRunIds) {
|
|
384
|
+
throw new Error("Must provide either (beforeCommit + afterCommit) or (beforeRunId + afterRunId)");
|
|
385
|
+
}
|
|
386
|
+
if (hasCommits) {
|
|
387
|
+
if (input.beforeCommit.trim().length === 0 || input.afterCommit.trim().length === 0) {
|
|
388
|
+
throw new Error("beforeCommit and afterCommit must not be empty strings");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (hasRunIds) {
|
|
392
|
+
if (input.beforeRunId.trim().length === 0 || input.afterRunId.trim().length === 0) {
|
|
393
|
+
throw new Error("beforeRunId and afterRunId must not be empty strings");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const response = await client.compareTestMetrics({
|
|
397
|
+
projectId: input.projectId,
|
|
398
|
+
testName: input.testName,
|
|
399
|
+
beforeCommit: input.beforeCommit,
|
|
400
|
+
afterCommit: input.afterCommit,
|
|
401
|
+
beforeRunId: input.beforeRunId,
|
|
402
|
+
afterRunId: input.afterRunId
|
|
403
|
+
});
|
|
404
|
+
return response;
|
|
405
|
+
}
|
|
406
|
+
var compareTestMetricsMetadata = {
|
|
407
|
+
name: "compare_test_metrics",
|
|
408
|
+
title: "Compare Test Metrics",
|
|
409
|
+
description: `Compare test metrics between two commits or test runs.
|
|
410
|
+
|
|
411
|
+
Useful for measuring the impact of code changes on test performance or reliability.
|
|
412
|
+
|
|
413
|
+
When using a user API Key (gaf_), you must provide a projectId.
|
|
414
|
+
Use list_projects to find available project IDs.
|
|
415
|
+
|
|
416
|
+
Parameters:
|
|
417
|
+
- projectId (required): Project ID
|
|
418
|
+
- testName (required): The test name to compare (short name or full name)
|
|
419
|
+
- Option 1 - Compare by commit:
|
|
420
|
+
- beforeCommit: Commit SHA for "before" measurement
|
|
421
|
+
- afterCommit: Commit SHA for "after" measurement
|
|
422
|
+
- Option 2 - Compare by test run:
|
|
423
|
+
- beforeRunId: Test run ID for "before" measurement
|
|
424
|
+
- afterRunId: Test run ID for "after" measurement
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
- testName: The test that was compared
|
|
428
|
+
- before: Metrics from the before commit/run
|
|
429
|
+
- testRunId, commit, branch, status, durationMs, createdAt
|
|
430
|
+
- after: Metrics from the after commit/run
|
|
431
|
+
- testRunId, commit, branch, status, durationMs, createdAt
|
|
432
|
+
- change: Calculated changes
|
|
433
|
+
- durationMs: Duration difference (negative = faster)
|
|
434
|
+
- percentChange: Percentage change (negative = improvement)
|
|
435
|
+
- statusChanged: Whether pass/fail status changed
|
|
436
|
+
|
|
437
|
+
Use cases:
|
|
438
|
+
- "Did my fix make this test faster?"
|
|
439
|
+
- "Compare test performance between these two commits"
|
|
440
|
+
- "Did this test start failing after my changes?"
|
|
441
|
+
- "Show me the before/after for the slow test I optimized"
|
|
442
|
+
|
|
443
|
+
Tip: Use get_test_history first to find the commit SHAs or test run IDs you want to compare.`
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// src/tools/get-flaky-tests.ts
|
|
447
|
+
import { z as z2 } from "zod";
|
|
250
448
|
var getFlakyTestsInputSchema = {
|
|
251
|
-
projectId:
|
|
252
|
-
threshold:
|
|
253
|
-
limit:
|
|
254
|
-
days:
|
|
449
|
+
projectId: z2.string().optional().describe("Project ID to get flaky tests for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
|
|
450
|
+
threshold: z2.number().min(0).max(1).optional().describe("Minimum flip rate to be considered flaky (0-1, default: 0.1 = 10%)"),
|
|
451
|
+
limit: z2.number().int().min(1).max(100).optional().describe("Maximum number of flaky tests to return (default: 50)"),
|
|
452
|
+
days: z2.number().int().min(1).max(365).optional().describe("Analysis period in days (default: 30)")
|
|
255
453
|
};
|
|
256
454
|
var getFlakyTestsOutputSchema = {
|
|
257
|
-
flakyTests:
|
|
258
|
-
name:
|
|
259
|
-
flipRate:
|
|
260
|
-
flipCount:
|
|
261
|
-
totalRuns:
|
|
262
|
-
lastSeen:
|
|
455
|
+
flakyTests: z2.array(z2.object({
|
|
456
|
+
name: z2.string(),
|
|
457
|
+
flipRate: z2.number(),
|
|
458
|
+
flipCount: z2.number(),
|
|
459
|
+
totalRuns: z2.number(),
|
|
460
|
+
lastSeen: z2.string()
|
|
263
461
|
})),
|
|
264
|
-
summary:
|
|
265
|
-
threshold:
|
|
266
|
-
totalFlaky:
|
|
267
|
-
period:
|
|
462
|
+
summary: z2.object({
|
|
463
|
+
threshold: z2.number(),
|
|
464
|
+
totalFlaky: z2.number(),
|
|
465
|
+
period: z2.number()
|
|
268
466
|
})
|
|
269
467
|
};
|
|
270
468
|
async function executeGetFlakyTests(client, input) {
|
|
@@ -304,22 +502,22 @@ specific tests are flaky and need investigation.`
|
|
|
304
502
|
};
|
|
305
503
|
|
|
306
504
|
// src/tools/get-project-health.ts
|
|
307
|
-
import { z as
|
|
505
|
+
import { z as z3 } from "zod";
|
|
308
506
|
var getProjectHealthInputSchema = {
|
|
309
|
-
projectId:
|
|
310
|
-
days:
|
|
507
|
+
projectId: z3.string().optional().describe("Project ID to get health for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
|
|
508
|
+
days: z3.number().int().min(1).max(365).optional().describe("Number of days to analyze (default: 30)")
|
|
311
509
|
};
|
|
312
510
|
var getProjectHealthOutputSchema = {
|
|
313
|
-
projectName:
|
|
314
|
-
healthScore:
|
|
315
|
-
passRate:
|
|
316
|
-
testRunCount:
|
|
317
|
-
flakyTestCount:
|
|
318
|
-
trend:
|
|
319
|
-
period:
|
|
320
|
-
days:
|
|
321
|
-
start:
|
|
322
|
-
end:
|
|
511
|
+
projectName: z3.string(),
|
|
512
|
+
healthScore: z3.number(),
|
|
513
|
+
passRate: z3.number().nullable(),
|
|
514
|
+
testRunCount: z3.number(),
|
|
515
|
+
flakyTestCount: z3.number(),
|
|
516
|
+
trend: z3.enum(["up", "down", "stable"]),
|
|
517
|
+
period: z3.object({
|
|
518
|
+
days: z3.number(),
|
|
519
|
+
start: z3.string(),
|
|
520
|
+
end: z3.string()
|
|
323
521
|
})
|
|
324
522
|
};
|
|
325
523
|
async function executeGetProjectHealth(client, input) {
|
|
@@ -356,22 +554,22 @@ Use this to understand the current state of your test suite.`
|
|
|
356
554
|
};
|
|
357
555
|
|
|
358
556
|
// src/tools/get-report.ts
|
|
359
|
-
import { z as
|
|
557
|
+
import { z as z4 } from "zod";
|
|
360
558
|
var getReportInputSchema = {
|
|
361
|
-
testRunId:
|
|
559
|
+
testRunId: z4.string().describe("The test run ID to get report files for. Use list_test_runs to find test run IDs.")
|
|
362
560
|
};
|
|
363
561
|
var getReportOutputSchema = {
|
|
364
|
-
testRunId:
|
|
365
|
-
projectId:
|
|
366
|
-
projectName:
|
|
367
|
-
resultSchema:
|
|
368
|
-
files:
|
|
369
|
-
filename:
|
|
370
|
-
size:
|
|
371
|
-
contentType:
|
|
372
|
-
downloadUrl:
|
|
562
|
+
testRunId: z4.string(),
|
|
563
|
+
projectId: z4.string(),
|
|
564
|
+
projectName: z4.string(),
|
|
565
|
+
resultSchema: z4.string().optional(),
|
|
566
|
+
files: z4.array(z4.object({
|
|
567
|
+
filename: z4.string(),
|
|
568
|
+
size: z4.number(),
|
|
569
|
+
contentType: z4.string(),
|
|
570
|
+
downloadUrl: z4.string()
|
|
373
571
|
})),
|
|
374
|
-
urlExpiresInSeconds:
|
|
572
|
+
urlExpiresInSeconds: z4.number().optional()
|
|
375
573
|
};
|
|
376
574
|
async function executeGetReport(client, input) {
|
|
377
575
|
const response = await client.getReport(input.testRunId);
|
|
@@ -392,52 +590,66 @@ async function executeGetReport(client, input) {
|
|
|
392
590
|
var getReportMetadata = {
|
|
393
591
|
name: "get_report",
|
|
394
592
|
title: "Get Report Files",
|
|
395
|
-
description: `Get
|
|
593
|
+
description: `Get URLs for report files uploaded with a test run.
|
|
594
|
+
|
|
595
|
+
IMPORTANT: This tool returns download URLs, not file content. You must fetch the URLs separately.
|
|
396
596
|
|
|
397
|
-
Returns
|
|
398
|
-
- filename: The file name (e.g., "report.html", "
|
|
597
|
+
Returns for each file:
|
|
598
|
+
- filename: The file name (e.g., "report.html", "results.json", "junit.xml")
|
|
399
599
|
- size: File size in bytes
|
|
400
|
-
- contentType: MIME type (e.g., "text/html", "application/json")
|
|
401
|
-
- downloadUrl: Presigned URL to download the file
|
|
402
|
-
- urlExpiresInSeconds: How long the download URLs are valid (typically 5 minutes)
|
|
600
|
+
- contentType: MIME type (e.g., "text/html", "application/json", "application/xml")
|
|
601
|
+
- downloadUrl: Presigned URL to download the file (valid for ~5 minutes)
|
|
403
602
|
|
|
404
|
-
|
|
405
|
-
- HTML reports (Playwright, pytest-html, Vitest UI)
|
|
406
|
-
- JSON results (Jest, Vitest)
|
|
407
|
-
- JUnit XML files
|
|
408
|
-
- Coverage reports
|
|
603
|
+
How to use the returned URLs:
|
|
409
604
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
605
|
+
1. **JSON files** (results.json, coverage.json):
|
|
606
|
+
Use WebFetch with the downloadUrl to retrieve and parse the JSON content.
|
|
607
|
+
Example: WebFetch(url=downloadUrl, prompt="Extract test results from this JSON")
|
|
608
|
+
|
|
609
|
+
2. **XML files** (junit.xml, xunit.xml):
|
|
610
|
+
Use WebFetch with the downloadUrl to retrieve and parse the XML content.
|
|
611
|
+
Example: WebFetch(url=downloadUrl, prompt="Parse the test results from this JUnit XML")
|
|
414
612
|
|
|
415
|
-
|
|
613
|
+
3. **HTML reports** (Playwright, pytest-html, Vitest):
|
|
614
|
+
These are typically bundled React/JavaScript applications that require a browser.
|
|
615
|
+
They cannot be meaningfully parsed by WebFetch.
|
|
616
|
+
For programmatic analysis, use get_test_run_details instead.
|
|
617
|
+
|
|
618
|
+
Recommendations:
|
|
619
|
+
- For analyzing test results programmatically: Use get_test_run_details (returns parsed test data)
|
|
620
|
+
- For JSON/XML files: Use this tool + WebFetch on the downloadUrl
|
|
621
|
+
- For HTML reports: Direct users to view in browser, or use get_test_run_details
|
|
622
|
+
|
|
623
|
+
Use cases:
|
|
624
|
+
- "What files are in this test run?" (list available reports)
|
|
625
|
+
- "Get the coverage data from this run" (then WebFetch the JSON URL)
|
|
626
|
+
- "Parse the JUnit XML results" (then WebFetch the XML URL)`
|
|
416
627
|
};
|
|
417
628
|
|
|
418
629
|
// src/tools/get-slowest-tests.ts
|
|
419
|
-
import { z as
|
|
630
|
+
import { z as z5 } from "zod";
|
|
420
631
|
var getSlowestTestsInputSchema = {
|
|
421
|
-
projectId:
|
|
422
|
-
days:
|
|
423
|
-
limit:
|
|
424
|
-
framework:
|
|
632
|
+
projectId: z5.string().describe("Project ID to get slowest tests for. Required. Use list_projects to find project IDs."),
|
|
633
|
+
days: z5.number().int().min(1).max(365).optional().describe("Analysis period in days (default: 30)"),
|
|
634
|
+
limit: z5.number().int().min(1).max(100).optional().describe("Maximum number of tests to return (default: 20)"),
|
|
635
|
+
framework: z5.string().optional().describe('Filter by test framework (e.g., "playwright", "vitest", "jest")'),
|
|
636
|
+
branch: z5.string().optional().describe('Filter by git branch name (e.g., "main", "develop")')
|
|
425
637
|
};
|
|
426
638
|
var getSlowestTestsOutputSchema = {
|
|
427
|
-
slowestTests:
|
|
428
|
-
name:
|
|
429
|
-
fullName:
|
|
430
|
-
filePath:
|
|
431
|
-
framework:
|
|
432
|
-
avgDurationMs:
|
|
433
|
-
p95DurationMs:
|
|
434
|
-
runCount:
|
|
639
|
+
slowestTests: z5.array(z5.object({
|
|
640
|
+
name: z5.string(),
|
|
641
|
+
fullName: z5.string(),
|
|
642
|
+
filePath: z5.string().optional(),
|
|
643
|
+
framework: z5.string().optional(),
|
|
644
|
+
avgDurationMs: z5.number(),
|
|
645
|
+
p95DurationMs: z5.number(),
|
|
646
|
+
runCount: z5.number()
|
|
435
647
|
})),
|
|
436
|
-
summary:
|
|
437
|
-
projectId:
|
|
438
|
-
projectName:
|
|
439
|
-
period:
|
|
440
|
-
totalReturned:
|
|
648
|
+
summary: z5.object({
|
|
649
|
+
projectId: z5.string(),
|
|
650
|
+
projectName: z5.string(),
|
|
651
|
+
period: z5.number(),
|
|
652
|
+
totalReturned: z5.number()
|
|
441
653
|
})
|
|
442
654
|
};
|
|
443
655
|
async function executeGetSlowestTests(client, input) {
|
|
@@ -445,7 +657,8 @@ async function executeGetSlowestTests(client, input) {
|
|
|
445
657
|
projectId: input.projectId,
|
|
446
658
|
days: input.days,
|
|
447
659
|
limit: input.limit,
|
|
448
|
-
framework: input.framework
|
|
660
|
+
framework: input.framework,
|
|
661
|
+
branch: input.branch
|
|
449
662
|
});
|
|
450
663
|
return {
|
|
451
664
|
slowestTests: response.slowestTests.map((test) => ({
|
|
@@ -473,6 +686,7 @@ Parameters:
|
|
|
473
686
|
- days (optional): Analysis period in days (default: 30, max: 365)
|
|
474
687
|
- limit (optional): Max tests to return (default: 20, max: 100)
|
|
475
688
|
- framework (optional): Filter by framework (e.g., "playwright", "vitest")
|
|
689
|
+
- branch (optional): Filter by git branch (e.g., "main", "develop")
|
|
476
690
|
|
|
477
691
|
Returns:
|
|
478
692
|
- List of slowest tests with:
|
|
@@ -488,32 +702,33 @@ Returns:
|
|
|
488
702
|
Use cases:
|
|
489
703
|
- "Which tests are slowing down my CI pipeline?"
|
|
490
704
|
- "Find the slowest Playwright tests to optimize"
|
|
491
|
-
- "Show me e2e tests taking over 30 seconds"
|
|
705
|
+
- "Show me e2e tests taking over 30 seconds"
|
|
706
|
+
- "What are the slowest tests on the main branch?"`
|
|
492
707
|
};
|
|
493
708
|
|
|
494
709
|
// src/tools/get-test-history.ts
|
|
495
|
-
import { z as
|
|
710
|
+
import { z as z6 } from "zod";
|
|
496
711
|
var getTestHistoryInputSchema = {
|
|
497
|
-
projectId:
|
|
498
|
-
testName:
|
|
499
|
-
filePath:
|
|
500
|
-
limit:
|
|
712
|
+
projectId: z6.string().optional().describe("Project ID to get test history for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
|
|
713
|
+
testName: z6.string().optional().describe("Exact test name to search for"),
|
|
714
|
+
filePath: z6.string().optional().describe("File path containing the test"),
|
|
715
|
+
limit: z6.number().int().min(1).max(100).optional().describe("Maximum number of results (default: 20)")
|
|
501
716
|
};
|
|
502
717
|
var getTestHistoryOutputSchema = {
|
|
503
|
-
history:
|
|
504
|
-
testRunId:
|
|
505
|
-
createdAt:
|
|
506
|
-
branch:
|
|
507
|
-
commitSha:
|
|
508
|
-
status:
|
|
509
|
-
durationMs:
|
|
510
|
-
message:
|
|
718
|
+
history: z6.array(z6.object({
|
|
719
|
+
testRunId: z6.string(),
|
|
720
|
+
createdAt: z6.string(),
|
|
721
|
+
branch: z6.string().optional(),
|
|
722
|
+
commitSha: z6.string().optional(),
|
|
723
|
+
status: z6.enum(["passed", "failed", "skipped", "pending"]),
|
|
724
|
+
durationMs: z6.number(),
|
|
725
|
+
message: z6.string().optional()
|
|
511
726
|
})),
|
|
512
|
-
summary:
|
|
513
|
-
totalRuns:
|
|
514
|
-
passedRuns:
|
|
515
|
-
failedRuns:
|
|
516
|
-
passRate:
|
|
727
|
+
summary: z6.object({
|
|
728
|
+
totalRuns: z6.number(),
|
|
729
|
+
passedRuns: z6.number(),
|
|
730
|
+
failedRuns: z6.number(),
|
|
731
|
+
passRate: z6.number().nullable()
|
|
517
732
|
})
|
|
518
733
|
};
|
|
519
734
|
async function executeGetTestHistory(client, input) {
|
|
@@ -567,24 +782,108 @@ Returns:
|
|
|
567
782
|
Use this to investigate flaky tests or understand test stability.`
|
|
568
783
|
};
|
|
569
784
|
|
|
785
|
+
// src/tools/get-test-run-details.ts
|
|
786
|
+
import { z as z7 } from "zod";
|
|
787
|
+
var getTestRunDetailsInputSchema = {
|
|
788
|
+
testRunId: z7.string().describe("The test run ID to get details for. Use list_test_runs to find test run IDs."),
|
|
789
|
+
projectId: z7.string().describe("Project ID the test run belongs to. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
|
|
790
|
+
status: z7.enum(["passed", "failed", "skipped"]).optional().describe("Filter tests by status. Returns only tests matching this status."),
|
|
791
|
+
limit: z7.number().int().min(1).max(500).optional().describe("Maximum number of tests to return (default: 100, max: 500)"),
|
|
792
|
+
offset: z7.number().int().min(0).optional().describe("Number of tests to skip for pagination (default: 0)")
|
|
793
|
+
};
|
|
794
|
+
var getTestRunDetailsOutputSchema = {
|
|
795
|
+
testRunId: z7.string(),
|
|
796
|
+
summary: z7.object({
|
|
797
|
+
passed: z7.number(),
|
|
798
|
+
failed: z7.number(),
|
|
799
|
+
skipped: z7.number(),
|
|
800
|
+
total: z7.number()
|
|
801
|
+
}),
|
|
802
|
+
tests: z7.array(z7.object({
|
|
803
|
+
name: z7.string(),
|
|
804
|
+
fullName: z7.string(),
|
|
805
|
+
status: z7.enum(["passed", "failed", "skipped"]),
|
|
806
|
+
durationMs: z7.number().nullable(),
|
|
807
|
+
filePath: z7.string().nullable(),
|
|
808
|
+
error: z7.string().nullable()
|
|
809
|
+
})),
|
|
810
|
+
pagination: z7.object({
|
|
811
|
+
total: z7.number(),
|
|
812
|
+
limit: z7.number(),
|
|
813
|
+
offset: z7.number(),
|
|
814
|
+
hasMore: z7.boolean()
|
|
815
|
+
})
|
|
816
|
+
};
|
|
817
|
+
async function executeGetTestRunDetails(client, input) {
|
|
818
|
+
const response = await client.getTestRunDetails({
|
|
819
|
+
projectId: input.projectId,
|
|
820
|
+
testRunId: input.testRunId,
|
|
821
|
+
status: input.status,
|
|
822
|
+
limit: input.limit,
|
|
823
|
+
offset: input.offset
|
|
824
|
+
});
|
|
825
|
+
return {
|
|
826
|
+
testRunId: response.testRunId,
|
|
827
|
+
summary: response.summary,
|
|
828
|
+
tests: response.tests,
|
|
829
|
+
pagination: response.pagination
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
var getTestRunDetailsMetadata = {
|
|
833
|
+
name: "get_test_run_details",
|
|
834
|
+
title: "Get Test Run Details",
|
|
835
|
+
description: `Get parsed test results for a specific test run.
|
|
836
|
+
|
|
837
|
+
When using a user API Key (gaf_), you must provide a projectId.
|
|
838
|
+
Use list_projects to find available project IDs, and list_test_runs to find test run IDs.
|
|
839
|
+
|
|
840
|
+
Parameters:
|
|
841
|
+
- testRunId (required): The test run ID to get details for
|
|
842
|
+
- projectId (required): Project ID the test run belongs to
|
|
843
|
+
- status (optional): Filter by test status: "passed", "failed", or "skipped"
|
|
844
|
+
- limit (optional): Max tests to return (default: 100, max: 500)
|
|
845
|
+
- offset (optional): Pagination offset (default: 0)
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
- testRunId: The test run ID
|
|
849
|
+
- summary: Overall counts (passed, failed, skipped, total)
|
|
850
|
+
- tests: Array of individual test results with:
|
|
851
|
+
- name: Short test name
|
|
852
|
+
- fullName: Full test name including describe blocks
|
|
853
|
+
- status: Test status (passed, failed, skipped)
|
|
854
|
+
- durationMs: Test duration in milliseconds (null if not recorded)
|
|
855
|
+
- filePath: Test file path (null if not recorded)
|
|
856
|
+
- error: Error message for failed tests (null otherwise)
|
|
857
|
+
- pagination: Pagination info (total, limit, offset, hasMore)
|
|
858
|
+
|
|
859
|
+
Use cases:
|
|
860
|
+
- "Show me all failed tests from this test run"
|
|
861
|
+
- "Get the test results from commit abc123"
|
|
862
|
+
- "List tests that took the longest in this run"
|
|
863
|
+
- "Find tests with errors in the auth module"
|
|
864
|
+
|
|
865
|
+
Note: For aggregate analytics like flaky test detection or duration trends,
|
|
866
|
+
use get_test_history, get_flaky_tests, or get_slowest_tests instead.`
|
|
867
|
+
};
|
|
868
|
+
|
|
570
869
|
// src/tools/list-projects.ts
|
|
571
|
-
import { z as
|
|
870
|
+
import { z as z8 } from "zod";
|
|
572
871
|
var listProjectsInputSchema = {
|
|
573
|
-
organizationId:
|
|
574
|
-
limit:
|
|
872
|
+
organizationId: z8.string().optional().describe("Filter by organization ID (optional)"),
|
|
873
|
+
limit: z8.number().int().min(1).max(100).optional().describe("Maximum number of projects to return (default: 50)")
|
|
575
874
|
};
|
|
576
875
|
var listProjectsOutputSchema = {
|
|
577
|
-
projects:
|
|
578
|
-
id:
|
|
579
|
-
name:
|
|
580
|
-
description:
|
|
581
|
-
organization:
|
|
582
|
-
id:
|
|
583
|
-
name:
|
|
584
|
-
slug:
|
|
876
|
+
projects: z8.array(z8.object({
|
|
877
|
+
id: z8.string(),
|
|
878
|
+
name: z8.string(),
|
|
879
|
+
description: z8.string().nullable().optional(),
|
|
880
|
+
organization: z8.object({
|
|
881
|
+
id: z8.string(),
|
|
882
|
+
name: z8.string(),
|
|
883
|
+
slug: z8.string()
|
|
585
884
|
})
|
|
586
885
|
})),
|
|
587
|
-
total:
|
|
886
|
+
total: z8.number()
|
|
588
887
|
};
|
|
589
888
|
async function executeListProjects(client, input) {
|
|
590
889
|
const response = await client.listProjects({
|
|
@@ -613,28 +912,28 @@ Requires a user API Key (gaf_). Get one from Account Settings in the Gaffer dash
|
|
|
613
912
|
};
|
|
614
913
|
|
|
615
914
|
// src/tools/list-test-runs.ts
|
|
616
|
-
import { z as
|
|
915
|
+
import { z as z9 } from "zod";
|
|
617
916
|
var listTestRunsInputSchema = {
|
|
618
|
-
projectId:
|
|
619
|
-
commitSha:
|
|
620
|
-
branch:
|
|
621
|
-
status:
|
|
622
|
-
limit:
|
|
917
|
+
projectId: z9.string().optional().describe("Project ID to list test runs for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
|
|
918
|
+
commitSha: z9.string().optional().describe("Filter by commit SHA (exact or prefix match)"),
|
|
919
|
+
branch: z9.string().optional().describe("Filter by branch name"),
|
|
920
|
+
status: z9.enum(["passed", "failed"]).optional().describe('Filter by status: "passed" (no failures) or "failed" (has failures)'),
|
|
921
|
+
limit: z9.number().int().min(1).max(100).optional().describe("Maximum number of test runs to return (default: 20)")
|
|
623
922
|
};
|
|
624
923
|
var listTestRunsOutputSchema = {
|
|
625
|
-
testRuns:
|
|
626
|
-
id:
|
|
627
|
-
commitSha:
|
|
628
|
-
branch:
|
|
629
|
-
passedCount:
|
|
630
|
-
failedCount:
|
|
631
|
-
skippedCount:
|
|
632
|
-
totalCount:
|
|
633
|
-
createdAt:
|
|
924
|
+
testRuns: z9.array(z9.object({
|
|
925
|
+
id: z9.string(),
|
|
926
|
+
commitSha: z9.string().optional(),
|
|
927
|
+
branch: z9.string().optional(),
|
|
928
|
+
passedCount: z9.number(),
|
|
929
|
+
failedCount: z9.number(),
|
|
930
|
+
skippedCount: z9.number(),
|
|
931
|
+
totalCount: z9.number(),
|
|
932
|
+
createdAt: z9.string()
|
|
634
933
|
})),
|
|
635
|
-
pagination:
|
|
636
|
-
total:
|
|
637
|
-
hasMore:
|
|
934
|
+
pagination: z9.object({
|
|
935
|
+
total: z9.number(),
|
|
936
|
+
hasMore: z9.boolean()
|
|
638
937
|
})
|
|
639
938
|
};
|
|
640
939
|
async function executeListTestRuns(client, input) {
|
|
@@ -691,6 +990,23 @@ Use cases:
|
|
|
691
990
|
};
|
|
692
991
|
|
|
693
992
|
// src/index.ts
|
|
993
|
+
function logError(toolName, error) {
|
|
994
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
995
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
996
|
+
const stack = error instanceof Error ? error.stack : void 0;
|
|
997
|
+
console.error(`[${timestamp}] [gaffer-mcp] ${toolName} failed: ${message}`);
|
|
998
|
+
if (stack) {
|
|
999
|
+
console.error(stack);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
function handleToolError(toolName, error) {
|
|
1003
|
+
logError(toolName, error);
|
|
1004
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1005
|
+
return {
|
|
1006
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
1007
|
+
isError: true
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
694
1010
|
async function main() {
|
|
695
1011
|
const apiKey = process.env.GAFFER_API_KEY;
|
|
696
1012
|
if (!apiKey) {
|
|
@@ -733,11 +1049,7 @@ async function main() {
|
|
|
733
1049
|
structuredContent: output
|
|
734
1050
|
};
|
|
735
1051
|
} catch (error) {
|
|
736
|
-
|
|
737
|
-
return {
|
|
738
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
739
|
-
isError: true
|
|
740
|
-
};
|
|
1052
|
+
return handleToolError(getProjectHealthMetadata.name, error);
|
|
741
1053
|
}
|
|
742
1054
|
}
|
|
743
1055
|
);
|
|
@@ -757,11 +1069,7 @@ async function main() {
|
|
|
757
1069
|
structuredContent: output
|
|
758
1070
|
};
|
|
759
1071
|
} catch (error) {
|
|
760
|
-
|
|
761
|
-
return {
|
|
762
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
763
|
-
isError: true
|
|
764
|
-
};
|
|
1072
|
+
return handleToolError(getTestHistoryMetadata.name, error);
|
|
765
1073
|
}
|
|
766
1074
|
}
|
|
767
1075
|
);
|
|
@@ -781,11 +1089,7 @@ async function main() {
|
|
|
781
1089
|
structuredContent: output
|
|
782
1090
|
};
|
|
783
1091
|
} catch (error) {
|
|
784
|
-
|
|
785
|
-
return {
|
|
786
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
787
|
-
isError: true
|
|
788
|
-
};
|
|
1092
|
+
return handleToolError(getFlakyTestsMetadata.name, error);
|
|
789
1093
|
}
|
|
790
1094
|
}
|
|
791
1095
|
);
|
|
@@ -805,11 +1109,7 @@ async function main() {
|
|
|
805
1109
|
structuredContent: output
|
|
806
1110
|
};
|
|
807
1111
|
} catch (error) {
|
|
808
|
-
|
|
809
|
-
return {
|
|
810
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
811
|
-
isError: true
|
|
812
|
-
};
|
|
1112
|
+
return handleToolError(listTestRunsMetadata.name, error);
|
|
813
1113
|
}
|
|
814
1114
|
}
|
|
815
1115
|
);
|
|
@@ -829,11 +1129,7 @@ async function main() {
|
|
|
829
1129
|
structuredContent: output
|
|
830
1130
|
};
|
|
831
1131
|
} catch (error) {
|
|
832
|
-
|
|
833
|
-
return {
|
|
834
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
835
|
-
isError: true
|
|
836
|
-
};
|
|
1132
|
+
return handleToolError(listProjectsMetadata.name, error);
|
|
837
1133
|
}
|
|
838
1134
|
}
|
|
839
1135
|
);
|
|
@@ -853,11 +1149,7 @@ async function main() {
|
|
|
853
1149
|
structuredContent: output
|
|
854
1150
|
};
|
|
855
1151
|
} catch (error) {
|
|
856
|
-
|
|
857
|
-
return {
|
|
858
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
859
|
-
isError: true
|
|
860
|
-
};
|
|
1152
|
+
return handleToolError(getReportMetadata.name, error);
|
|
861
1153
|
}
|
|
862
1154
|
}
|
|
863
1155
|
);
|
|
@@ -877,11 +1169,47 @@ async function main() {
|
|
|
877
1169
|
structuredContent: output
|
|
878
1170
|
};
|
|
879
1171
|
} catch (error) {
|
|
880
|
-
|
|
1172
|
+
return handleToolError(getSlowestTestsMetadata.name, error);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
);
|
|
1176
|
+
server.registerTool(
|
|
1177
|
+
getTestRunDetailsMetadata.name,
|
|
1178
|
+
{
|
|
1179
|
+
title: getTestRunDetailsMetadata.title,
|
|
1180
|
+
description: getTestRunDetailsMetadata.description,
|
|
1181
|
+
inputSchema: getTestRunDetailsInputSchema,
|
|
1182
|
+
outputSchema: getTestRunDetailsOutputSchema
|
|
1183
|
+
},
|
|
1184
|
+
async (input) => {
|
|
1185
|
+
try {
|
|
1186
|
+
const output = await executeGetTestRunDetails(client, input);
|
|
881
1187
|
return {
|
|
882
|
-
content: [{ type: "text", text:
|
|
883
|
-
|
|
1188
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
1189
|
+
structuredContent: output
|
|
884
1190
|
};
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
return handleToolError(getTestRunDetailsMetadata.name, error);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
);
|
|
1196
|
+
server.registerTool(
|
|
1197
|
+
compareTestMetricsMetadata.name,
|
|
1198
|
+
{
|
|
1199
|
+
title: compareTestMetricsMetadata.title,
|
|
1200
|
+
description: compareTestMetricsMetadata.description,
|
|
1201
|
+
inputSchema: compareTestMetricsInputSchema,
|
|
1202
|
+
outputSchema: compareTestMetricsOutputSchema
|
|
1203
|
+
},
|
|
1204
|
+
async (input) => {
|
|
1205
|
+
try {
|
|
1206
|
+
const output = await executeCompareTestMetrics(client, input);
|
|
1207
|
+
return {
|
|
1208
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
1209
|
+
structuredContent: output
|
|
1210
|
+
};
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
return handleToolError(compareTestMetricsMetadata.name, error);
|
|
885
1213
|
}
|
|
886
1214
|
}
|
|
887
1215
|
);
|
package/package.json
CHANGED