@gaffer-sh/mcp 0.4.2 → 0.6.0

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