@gaffer-sh/mcp 0.1.0 → 0.2.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/README.md +95 -15
- package/dist/index.js +451 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,11 +9,27 @@ This MCP server connects AI coding assistants like Claude Code and Cursor to you
|
|
|
9
9
|
- Check your project's test health (pass rate, flaky tests, trends)
|
|
10
10
|
- Look up the history of specific tests to understand stability
|
|
11
11
|
- Get context about test failures when debugging
|
|
12
|
+
- Browse all your projects (with user API Keys)
|
|
13
|
+
- Access test report files (HTML reports, coverage, etc.)
|
|
12
14
|
|
|
13
15
|
## Prerequisites
|
|
14
16
|
|
|
15
17
|
1. A [Gaffer](https://gaffer.sh) account with test results uploaded
|
|
16
|
-
2. An API
|
|
18
|
+
2. An API Key from Account Settings > API Keys
|
|
19
|
+
|
|
20
|
+
## Authentication
|
|
21
|
+
|
|
22
|
+
The MCP server supports two types of authentication:
|
|
23
|
+
|
|
24
|
+
### User API Keys (Recommended)
|
|
25
|
+
|
|
26
|
+
User API Keys (`gaf_` prefix) provide read-only access to all projects across your organizations. This is the recommended approach as it allows your AI assistant to work across multiple projects.
|
|
27
|
+
|
|
28
|
+
Get your API Key from: **Account Settings > API Keys**
|
|
29
|
+
|
|
30
|
+
### Project Upload Tokens (Legacy)
|
|
31
|
+
|
|
32
|
+
Project Upload Tokens (`gfr_` prefix) are designed for uploading test results and only provide access to a single project. While still supported for backward compatibility, user API Keys are preferred for the MCP server.
|
|
17
33
|
|
|
18
34
|
## Setup
|
|
19
35
|
|
|
@@ -28,7 +44,7 @@ Add to your Claude Code settings (`~/.claude.json` or project `.claude/settings.
|
|
|
28
44
|
"command": "npx",
|
|
29
45
|
"args": ["-y", "@gaffer-sh/mcp"],
|
|
30
46
|
"env": {
|
|
31
|
-
"GAFFER_API_KEY": "
|
|
47
|
+
"GAFFER_API_KEY": "gaf_your_api_key_here"
|
|
32
48
|
}
|
|
33
49
|
}
|
|
34
50
|
}
|
|
@@ -46,7 +62,7 @@ Add to `.cursor/mcp.json` in your project:
|
|
|
46
62
|
"command": "npx",
|
|
47
63
|
"args": ["-y", "@gaffer-sh/mcp"],
|
|
48
64
|
"env": {
|
|
49
|
-
"GAFFER_API_KEY": "
|
|
65
|
+
"GAFFER_API_KEY": "gaf_your_api_key_here"
|
|
50
66
|
}
|
|
51
67
|
}
|
|
52
68
|
}
|
|
@@ -55,11 +71,25 @@ Add to `.cursor/mcp.json` in your project:
|
|
|
55
71
|
|
|
56
72
|
## Available Tools
|
|
57
73
|
|
|
74
|
+
### `list_projects`
|
|
75
|
+
|
|
76
|
+
List all projects you have access to. **Requires a user API Key (`gaf_`).**
|
|
77
|
+
|
|
78
|
+
**Input:**
|
|
79
|
+
- `organizationId` (optional): Filter by organization
|
|
80
|
+
- `limit` (optional): Max results (default: 50)
|
|
81
|
+
|
|
82
|
+
**Returns:**
|
|
83
|
+
- List of projects with IDs, names, and organization info
|
|
84
|
+
|
|
85
|
+
**Example prompt:** "What projects do I have in Gaffer?"
|
|
86
|
+
|
|
58
87
|
### `get_project_health`
|
|
59
88
|
|
|
60
|
-
Get the health metrics for
|
|
89
|
+
Get the health metrics for a project.
|
|
61
90
|
|
|
62
91
|
**Input:**
|
|
92
|
+
- `projectId` (required with user API Keys): Project ID from `list_projects`
|
|
63
93
|
- `days` (optional): Number of days to analyze (default: 30)
|
|
64
94
|
|
|
65
95
|
**Returns:**
|
|
@@ -75,9 +105,10 @@ Get the health metrics for your project.
|
|
|
75
105
|
|
|
76
106
|
Get the pass/fail history for a specific test.
|
|
77
107
|
|
|
78
|
-
**Input
|
|
79
|
-
- `
|
|
80
|
-
- `
|
|
108
|
+
**Input:**
|
|
109
|
+
- `projectId` (required with user API Keys): Project ID from `list_projects`
|
|
110
|
+
- `testName`: Exact test name to search for (one of testName or filePath required)
|
|
111
|
+
- `filePath`: File path containing the test (one of testName or filePath required)
|
|
81
112
|
- `limit` (optional): Max results (default: 20)
|
|
82
113
|
|
|
83
114
|
**Returns:**
|
|
@@ -93,9 +124,10 @@ Get the pass/fail history for a specific test.
|
|
|
93
124
|
|
|
94
125
|
### `get_flaky_tests`
|
|
95
126
|
|
|
96
|
-
Get the list of flaky tests in
|
|
127
|
+
Get the list of flaky tests in a project.
|
|
97
128
|
|
|
98
129
|
**Input:**
|
|
130
|
+
- `projectId` (required with user API Keys): Project ID from `list_projects`
|
|
99
131
|
- `threshold` (optional): Minimum flip rate to be considered flaky (0-1, default: 0.1)
|
|
100
132
|
- `limit` (optional): Max results (default: 50)
|
|
101
133
|
- `days` (optional): Analysis period in days (default: 30)
|
|
@@ -114,11 +146,12 @@ Get the list of flaky tests in your project.
|
|
|
114
146
|
|
|
115
147
|
List recent test runs with optional filtering.
|
|
116
148
|
|
|
117
|
-
**Input
|
|
118
|
-
- `
|
|
119
|
-
- `
|
|
120
|
-
- `
|
|
121
|
-
- `
|
|
149
|
+
**Input:**
|
|
150
|
+
- `projectId` (required with user API Keys): Project ID from `list_projects`
|
|
151
|
+
- `commitSha` (optional): Filter by commit SHA (supports prefix matching)
|
|
152
|
+
- `branch` (optional): Filter by branch name
|
|
153
|
+
- `status` (optional): Filter by "passed" (no failures) or "failed" (has failures)
|
|
154
|
+
- `limit` (optional): Max results (default: 20)
|
|
122
155
|
|
|
123
156
|
**Returns:**
|
|
124
157
|
- List of test runs with pass/fail/skip counts
|
|
@@ -130,11 +163,58 @@ List recent test runs with optional filtering.
|
|
|
130
163
|
- "Show me test runs on the main branch"
|
|
131
164
|
- "Did any tests fail on my feature branch?"
|
|
132
165
|
|
|
166
|
+
### `get_report`
|
|
167
|
+
|
|
168
|
+
Get the report files for a specific test run. **Requires a user API Key (`gaf_`).**
|
|
169
|
+
|
|
170
|
+
**Input:**
|
|
171
|
+
- `testRunId` (required): Test run ID from `list_test_runs`
|
|
172
|
+
|
|
173
|
+
**Returns:**
|
|
174
|
+
- Test run ID and project info
|
|
175
|
+
- Framework used (e.g., playwright, vitest)
|
|
176
|
+
- List of files with:
|
|
177
|
+
- Filename (e.g., "report.html", "coverage/index.html")
|
|
178
|
+
- File size in bytes
|
|
179
|
+
- Content type (e.g., "text/html")
|
|
180
|
+
- Download URL
|
|
181
|
+
|
|
182
|
+
**Example prompts:**
|
|
183
|
+
- "Get the Playwright report for the latest test run"
|
|
184
|
+
- "What files were uploaded with this test run?"
|
|
185
|
+
- "Show me the coverage report"
|
|
186
|
+
|
|
187
|
+
### `get_slowest_tests`
|
|
188
|
+
|
|
189
|
+
Get the slowest tests in a project, sorted by P95 duration. **Requires a user API Key (`gaf_`).**
|
|
190
|
+
|
|
191
|
+
**Input:**
|
|
192
|
+
- `projectId` (required): Project ID from `list_projects`
|
|
193
|
+
- `days` (optional): Analysis period in days (default: 30, max: 365)
|
|
194
|
+
- `limit` (optional): Max tests to return (default: 20, max: 100)
|
|
195
|
+
- `framework` (optional): Filter by framework (e.g., "playwright", "vitest")
|
|
196
|
+
|
|
197
|
+
**Returns:**
|
|
198
|
+
- List of slowest tests with:
|
|
199
|
+
- name: Short test name
|
|
200
|
+
- fullName: Full test name including describe blocks
|
|
201
|
+
- filePath: Test file path (if available)
|
|
202
|
+
- framework: Test framework used
|
|
203
|
+
- avgDurationMs: Average duration in milliseconds
|
|
204
|
+
- p95DurationMs: 95th percentile duration
|
|
205
|
+
- runCount: Number of runs in the period
|
|
206
|
+
- Summary with project info
|
|
207
|
+
|
|
208
|
+
**Example prompts:**
|
|
209
|
+
- "Which tests are slowing down my CI pipeline?"
|
|
210
|
+
- "Find the slowest Playwright tests"
|
|
211
|
+
- "Show me e2e tests that need optimization"
|
|
212
|
+
|
|
133
213
|
## Environment Variables
|
|
134
214
|
|
|
135
215
|
| Variable | Required | Description |
|
|
136
216
|
|----------|----------|-------------|
|
|
137
|
-
| `GAFFER_API_KEY` | Yes | Your Gaffer API
|
|
217
|
+
| `GAFFER_API_KEY` | Yes | Your Gaffer API Key (starts with `gaf_`) |
|
|
138
218
|
| `GAFFER_API_URL` | No | API base URL (default: `https://app.gaffer.sh`) |
|
|
139
219
|
|
|
140
220
|
## Local Development
|
|
@@ -151,7 +231,7 @@ pnpm --filter @gaffer-sh/mcp build
|
|
|
151
231
|
"command": "node",
|
|
152
232
|
"args": ["/path/to/gaffer-v2/packages/mcp-server/dist/index.js"],
|
|
153
233
|
"env": {
|
|
154
|
-
"GAFFER_API_KEY": "
|
|
234
|
+
"GAFFER_API_KEY": "gaf_..."
|
|
155
235
|
}
|
|
156
236
|
}
|
|
157
237
|
}
|
package/dist/index.js
CHANGED
|
@@ -6,15 +6,26 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6
6
|
|
|
7
7
|
// src/api-client.ts
|
|
8
8
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
9
|
+
function detectTokenType(token) {
|
|
10
|
+
if (token.startsWith("gaf_")) {
|
|
11
|
+
return "user";
|
|
12
|
+
}
|
|
13
|
+
return "project";
|
|
14
|
+
}
|
|
9
15
|
var GafferApiClient = class _GafferApiClient {
|
|
10
16
|
apiKey;
|
|
11
17
|
baseUrl;
|
|
18
|
+
tokenType;
|
|
12
19
|
constructor(config) {
|
|
13
20
|
this.apiKey = config.apiKey;
|
|
14
21
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
22
|
+
this.tokenType = detectTokenType(config.apiKey);
|
|
15
23
|
}
|
|
16
24
|
/**
|
|
17
25
|
* Create client from environment variables
|
|
26
|
+
*
|
|
27
|
+
* Supports:
|
|
28
|
+
* - GAFFER_API_KEY (for user API Keys gaf_)
|
|
18
29
|
*/
|
|
19
30
|
static fromEnv() {
|
|
20
31
|
const apiKey = process.env.GAFFER_API_KEY;
|
|
@@ -24,6 +35,12 @@ var GafferApiClient = class _GafferApiClient {
|
|
|
24
35
|
const baseUrl = process.env.GAFFER_API_URL || "https://app.gaffer.sh";
|
|
25
36
|
return new _GafferApiClient({ apiKey, baseUrl });
|
|
26
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if using a user API Key (enables cross-project features)
|
|
40
|
+
*/
|
|
41
|
+
isUserToken() {
|
|
42
|
+
return this.tokenType === "user";
|
|
43
|
+
}
|
|
27
44
|
/**
|
|
28
45
|
* Make authenticated request to Gaffer API
|
|
29
46
|
*/
|
|
@@ -44,7 +61,7 @@ var GafferApiClient = class _GafferApiClient {
|
|
|
44
61
|
headers: {
|
|
45
62
|
"X-API-Key": this.apiKey,
|
|
46
63
|
"Accept": "application/json",
|
|
47
|
-
"User-Agent": "gaffer-mcp/0.
|
|
64
|
+
"User-Agent": "gaffer-mcp/0.2.0"
|
|
48
65
|
},
|
|
49
66
|
signal: controller.signal
|
|
50
67
|
});
|
|
@@ -63,16 +80,53 @@ var GafferApiClient = class _GafferApiClient {
|
|
|
63
80
|
clearTimeout(timeoutId);
|
|
64
81
|
}
|
|
65
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* List all projects the user has access to
|
|
85
|
+
* Requires user API Key (gaf_)
|
|
86
|
+
*
|
|
87
|
+
* @param options - Query options
|
|
88
|
+
* @param options.organizationId - Filter by organization ID
|
|
89
|
+
* @param options.limit - Maximum number of results
|
|
90
|
+
* @param options.offset - Offset for pagination
|
|
91
|
+
*/
|
|
92
|
+
async listProjects(options = {}) {
|
|
93
|
+
if (!this.isUserToken()) {
|
|
94
|
+
throw new Error("listProjects requires a user API Key (gaf_). Upload Tokens (gfr_) can only access a single project.");
|
|
95
|
+
}
|
|
96
|
+
return this.request("/user/projects", {
|
|
97
|
+
...options.organizationId && { organizationId: options.organizationId },
|
|
98
|
+
...options.limit && { limit: options.limit },
|
|
99
|
+
...options.offset && { offset: options.offset }
|
|
100
|
+
});
|
|
101
|
+
}
|
|
66
102
|
/**
|
|
67
103
|
* Get project health analytics
|
|
104
|
+
*
|
|
105
|
+
* @param options - Query options
|
|
106
|
+
* @param options.projectId - Required for user tokens, ignored for project tokens
|
|
107
|
+
* @param options.days - Analysis period in days (default: 30)
|
|
68
108
|
*/
|
|
69
109
|
async getProjectHealth(options = {}) {
|
|
110
|
+
if (this.isUserToken()) {
|
|
111
|
+
if (!options.projectId) {
|
|
112
|
+
throw new Error("projectId is required when using a user API Key");
|
|
113
|
+
}
|
|
114
|
+
return this.request(`/user/projects/${options.projectId}/health`, {
|
|
115
|
+
days: options.days || 30
|
|
116
|
+
});
|
|
117
|
+
}
|
|
70
118
|
return this.request("/project/analytics", {
|
|
71
119
|
days: options.days || 30
|
|
72
120
|
});
|
|
73
121
|
}
|
|
74
122
|
/**
|
|
75
123
|
* Get test history for a specific test
|
|
124
|
+
*
|
|
125
|
+
* @param options - Query options
|
|
126
|
+
* @param options.projectId - Required for user tokens, ignored for project tokens
|
|
127
|
+
* @param options.testName - Test name to search for
|
|
128
|
+
* @param options.filePath - File path to search for
|
|
129
|
+
* @param options.limit - Maximum number of results
|
|
76
130
|
*/
|
|
77
131
|
async getTestHistory(options) {
|
|
78
132
|
const testName = options.testName?.trim();
|
|
@@ -80,6 +134,16 @@ var GafferApiClient = class _GafferApiClient {
|
|
|
80
134
|
if (!testName && !filePath) {
|
|
81
135
|
throw new Error("Either testName or filePath is required (and must not be empty)");
|
|
82
136
|
}
|
|
137
|
+
if (this.isUserToken()) {
|
|
138
|
+
if (!options.projectId) {
|
|
139
|
+
throw new Error("projectId is required when using a user API Key");
|
|
140
|
+
}
|
|
141
|
+
return this.request(`/user/projects/${options.projectId}/test-history`, {
|
|
142
|
+
...testName && { testName },
|
|
143
|
+
...filePath && { filePath },
|
|
144
|
+
...options.limit && { limit: options.limit }
|
|
145
|
+
});
|
|
146
|
+
}
|
|
83
147
|
return this.request("/project/test-history", {
|
|
84
148
|
...testName && { testName },
|
|
85
149
|
...filePath && { filePath },
|
|
@@ -88,8 +152,24 @@ var GafferApiClient = class _GafferApiClient {
|
|
|
88
152
|
}
|
|
89
153
|
/**
|
|
90
154
|
* Get flaky tests for the project
|
|
155
|
+
*
|
|
156
|
+
* @param options - Query options
|
|
157
|
+
* @param options.projectId - Required for user tokens, ignored for project tokens
|
|
158
|
+
* @param options.threshold - Minimum flip rate to be considered flaky (0-1)
|
|
159
|
+
* @param options.limit - Maximum number of results
|
|
160
|
+
* @param options.days - Analysis period in days
|
|
91
161
|
*/
|
|
92
162
|
async getFlakyTests(options = {}) {
|
|
163
|
+
if (this.isUserToken()) {
|
|
164
|
+
if (!options.projectId) {
|
|
165
|
+
throw new Error("projectId is required when using a user API Key");
|
|
166
|
+
}
|
|
167
|
+
return this.request(`/user/projects/${options.projectId}/flaky-tests`, {
|
|
168
|
+
...options.threshold && { threshold: options.threshold },
|
|
169
|
+
...options.limit && { limit: options.limit },
|
|
170
|
+
...options.days && { days: options.days }
|
|
171
|
+
});
|
|
172
|
+
}
|
|
93
173
|
return this.request("/project/flaky-tests", {
|
|
94
174
|
...options.threshold && { threshold: options.threshold },
|
|
95
175
|
...options.limit && { limit: options.limit },
|
|
@@ -98,8 +178,26 @@ var GafferApiClient = class _GafferApiClient {
|
|
|
98
178
|
}
|
|
99
179
|
/**
|
|
100
180
|
* List test runs for the project
|
|
181
|
+
*
|
|
182
|
+
* @param options - Query options
|
|
183
|
+
* @param options.projectId - Required for user tokens, ignored for project tokens
|
|
184
|
+
* @param options.commitSha - Filter by commit SHA
|
|
185
|
+
* @param options.branch - Filter by branch name
|
|
186
|
+
* @param options.status - Filter by status ('passed' or 'failed')
|
|
187
|
+
* @param options.limit - Maximum number of results
|
|
101
188
|
*/
|
|
102
189
|
async getTestRuns(options = {}) {
|
|
190
|
+
if (this.isUserToken()) {
|
|
191
|
+
if (!options.projectId) {
|
|
192
|
+
throw new Error("projectId is required when using a user API Key");
|
|
193
|
+
}
|
|
194
|
+
return this.request(`/user/projects/${options.projectId}/test-runs`, {
|
|
195
|
+
...options.commitSha && { commitSha: options.commitSha },
|
|
196
|
+
...options.branch && { branch: options.branch },
|
|
197
|
+
...options.status && { status: options.status },
|
|
198
|
+
...options.limit && { limit: options.limit }
|
|
199
|
+
});
|
|
200
|
+
}
|
|
103
201
|
return this.request("/project/test-runs", {
|
|
104
202
|
...options.commitSha && { commitSha: options.commitSha },
|
|
105
203
|
...options.branch && { branch: options.branch },
|
|
@@ -107,11 +205,50 @@ var GafferApiClient = class _GafferApiClient {
|
|
|
107
205
|
...options.limit && { limit: options.limit }
|
|
108
206
|
});
|
|
109
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Get report files for a test run
|
|
210
|
+
*
|
|
211
|
+
* @param testRunId - The test run ID
|
|
212
|
+
* @returns Report metadata with download URLs for each file
|
|
213
|
+
*/
|
|
214
|
+
async getReport(testRunId) {
|
|
215
|
+
if (!this.isUserToken()) {
|
|
216
|
+
throw new Error("getReport requires a user API Key (gaf_). Upload Tokens (gfr_) cannot access reports via API.");
|
|
217
|
+
}
|
|
218
|
+
if (!testRunId) {
|
|
219
|
+
throw new Error("testRunId is required");
|
|
220
|
+
}
|
|
221
|
+
return this.request(`/user/test-runs/${testRunId}/report`);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Get slowest tests for a project
|
|
225
|
+
*
|
|
226
|
+
* @param options - Query options
|
|
227
|
+
* @param options.projectId - The project ID (required)
|
|
228
|
+
* @param options.days - Analysis period in days (default: 30)
|
|
229
|
+
* @param options.limit - Maximum number of results (default: 20)
|
|
230
|
+
* @param options.framework - Filter by test framework
|
|
231
|
+
* @returns Slowest tests sorted by P95 duration
|
|
232
|
+
*/
|
|
233
|
+
async getSlowestTests(options) {
|
|
234
|
+
if (!this.isUserToken()) {
|
|
235
|
+
throw new Error("getSlowestTests requires a user API Key (gaf_).");
|
|
236
|
+
}
|
|
237
|
+
if (!options.projectId) {
|
|
238
|
+
throw new Error("projectId is required");
|
|
239
|
+
}
|
|
240
|
+
return this.request(`/user/projects/${options.projectId}/slowest-tests`, {
|
|
241
|
+
...options.days && { days: options.days },
|
|
242
|
+
...options.limit && { limit: options.limit },
|
|
243
|
+
...options.framework && { framework: options.framework }
|
|
244
|
+
});
|
|
245
|
+
}
|
|
110
246
|
};
|
|
111
247
|
|
|
112
248
|
// src/tools/get-flaky-tests.ts
|
|
113
249
|
import { z } from "zod";
|
|
114
250
|
var getFlakyTestsInputSchema = {
|
|
251
|
+
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."),
|
|
115
252
|
threshold: z.number().min(0).max(1).optional().describe("Minimum flip rate to be considered flaky (0-1, default: 0.1 = 10%)"),
|
|
116
253
|
limit: z.number().int().min(1).max(100).optional().describe("Maximum number of flaky tests to return (default: 50)"),
|
|
117
254
|
days: z.number().int().min(1).max(365).optional().describe("Analysis period in days (default: 30)")
|
|
@@ -132,6 +269,7 @@ var getFlakyTestsOutputSchema = {
|
|
|
132
269
|
};
|
|
133
270
|
async function executeGetFlakyTests(client, input) {
|
|
134
271
|
const response = await client.getFlakyTests({
|
|
272
|
+
projectId: input.projectId,
|
|
135
273
|
threshold: input.threshold,
|
|
136
274
|
limit: input.limit,
|
|
137
275
|
days: input.days
|
|
@@ -144,7 +282,10 @@ async function executeGetFlakyTests(client, input) {
|
|
|
144
282
|
var getFlakyTestsMetadata = {
|
|
145
283
|
name: "get_flaky_tests",
|
|
146
284
|
title: "Get Flaky Tests",
|
|
147
|
-
description: `Get the list of flaky tests in
|
|
285
|
+
description: `Get the list of flaky tests in a project.
|
|
286
|
+
|
|
287
|
+
When using a user API Key (gaf_), you must provide a projectId.
|
|
288
|
+
Use list_projects first to find available project IDs.
|
|
148
289
|
|
|
149
290
|
A test is considered flaky if it frequently switches between pass and fail states
|
|
150
291
|
(high "flip rate"). This helps identify unreliable tests that need attention.
|
|
@@ -165,6 +306,7 @@ specific tests are flaky and need investigation.`
|
|
|
165
306
|
// src/tools/get-project-health.ts
|
|
166
307
|
import { z as z2 } from "zod";
|
|
167
308
|
var getProjectHealthInputSchema = {
|
|
309
|
+
projectId: z2.string().optional().describe("Project ID to get health for. Required when using a user API Key (gaf_). Use list_projects to find project IDs."),
|
|
168
310
|
days: z2.number().int().min(1).max(365).optional().describe("Number of days to analyze (default: 30)")
|
|
169
311
|
};
|
|
170
312
|
var getProjectHealthOutputSchema = {
|
|
@@ -181,7 +323,10 @@ var getProjectHealthOutputSchema = {
|
|
|
181
323
|
})
|
|
182
324
|
};
|
|
183
325
|
async function executeGetProjectHealth(client, input) {
|
|
184
|
-
const response = await client.getProjectHealth({
|
|
326
|
+
const response = await client.getProjectHealth({
|
|
327
|
+
projectId: input.projectId,
|
|
328
|
+
days: input.days
|
|
329
|
+
});
|
|
185
330
|
return {
|
|
186
331
|
projectName: response.analytics.projectName,
|
|
187
332
|
healthScore: response.analytics.healthScore,
|
|
@@ -195,7 +340,10 @@ async function executeGetProjectHealth(client, input) {
|
|
|
195
340
|
var getProjectHealthMetadata = {
|
|
196
341
|
name: "get_project_health",
|
|
197
342
|
title: "Get Project Health",
|
|
198
|
-
description: `Get the health metrics for
|
|
343
|
+
description: `Get the health metrics for a project.
|
|
344
|
+
|
|
345
|
+
When using a user API Key (gaf_), you must provide a projectId.
|
|
346
|
+
Use list_projects first to find available project IDs.
|
|
199
347
|
|
|
200
348
|
Returns:
|
|
201
349
|
- Health score (0-100): Overall project health based on pass rate and trend
|
|
@@ -207,28 +355,162 @@ Returns:
|
|
|
207
355
|
Use this to understand the current state of your test suite.`
|
|
208
356
|
};
|
|
209
357
|
|
|
210
|
-
// src/tools/get-
|
|
358
|
+
// src/tools/get-report.ts
|
|
211
359
|
import { z as z3 } from "zod";
|
|
360
|
+
var getReportInputSchema = {
|
|
361
|
+
testRunId: z3.string().describe("The test run ID to get report files for. Use list_test_runs to find test run IDs.")
|
|
362
|
+
};
|
|
363
|
+
var getReportOutputSchema = {
|
|
364
|
+
testRunId: z3.string(),
|
|
365
|
+
projectId: z3.string(),
|
|
366
|
+
projectName: z3.string(),
|
|
367
|
+
resultSchema: z3.string().optional(),
|
|
368
|
+
files: z3.array(z3.object({
|
|
369
|
+
filename: z3.string(),
|
|
370
|
+
size: z3.number(),
|
|
371
|
+
contentType: z3.string(),
|
|
372
|
+
downloadUrl: z3.string()
|
|
373
|
+
}))
|
|
374
|
+
};
|
|
375
|
+
async function executeGetReport(client, input) {
|
|
376
|
+
const response = await client.getReport(input.testRunId);
|
|
377
|
+
return {
|
|
378
|
+
testRunId: response.testRunId,
|
|
379
|
+
projectId: response.projectId,
|
|
380
|
+
projectName: response.projectName,
|
|
381
|
+
resultSchema: response.resultSchema,
|
|
382
|
+
files: response.files.map((file) => ({
|
|
383
|
+
filename: file.filename,
|
|
384
|
+
size: file.size,
|
|
385
|
+
contentType: file.contentType,
|
|
386
|
+
downloadUrl: file.downloadUrl
|
|
387
|
+
}))
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
var getReportMetadata = {
|
|
391
|
+
name: "get_report",
|
|
392
|
+
title: "Get Report Files",
|
|
393
|
+
description: `Get the report files for a specific test run.
|
|
394
|
+
|
|
395
|
+
Returns a list of files uploaded with the test run, including:
|
|
396
|
+
- filename: The file name (e.g., "report.html", "coverage/index.html")
|
|
397
|
+
- size: File size in bytes
|
|
398
|
+
- contentType: MIME type (e.g., "text/html", "application/json")
|
|
399
|
+
- downloadUrl: URL to download the file (requires authentication)
|
|
400
|
+
|
|
401
|
+
Common report types:
|
|
402
|
+
- HTML reports (Playwright, pytest-html, Vitest UI)
|
|
403
|
+
- JSON results (Jest, Vitest)
|
|
404
|
+
- JUnit XML files
|
|
405
|
+
- Coverage reports
|
|
406
|
+
|
|
407
|
+
Use cases:
|
|
408
|
+
- "Get the Playwright report for this test run"
|
|
409
|
+
- "Download the coverage report"
|
|
410
|
+
- "What files were uploaded with this test run?"
|
|
411
|
+
|
|
412
|
+
Note: Download URLs require the same API key authentication used for this request.`
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// src/tools/get-slowest-tests.ts
|
|
416
|
+
import { z as z4 } from "zod";
|
|
417
|
+
var getSlowestTestsInputSchema = {
|
|
418
|
+
projectId: z4.string().describe("Project ID to get slowest tests for. Required. Use list_projects to find project IDs."),
|
|
419
|
+
days: z4.number().int().min(1).max(365).optional().describe("Analysis period in days (default: 30)"),
|
|
420
|
+
limit: z4.number().int().min(1).max(100).optional().describe("Maximum number of tests to return (default: 20)"),
|
|
421
|
+
framework: z4.string().optional().describe('Filter by test framework (e.g., "playwright", "vitest", "jest")')
|
|
422
|
+
};
|
|
423
|
+
var getSlowestTestsOutputSchema = {
|
|
424
|
+
slowestTests: z4.array(z4.object({
|
|
425
|
+
name: z4.string(),
|
|
426
|
+
fullName: z4.string(),
|
|
427
|
+
filePath: z4.string().optional(),
|
|
428
|
+
framework: z4.string().optional(),
|
|
429
|
+
avgDurationMs: z4.number(),
|
|
430
|
+
p95DurationMs: z4.number(),
|
|
431
|
+
runCount: z4.number()
|
|
432
|
+
})),
|
|
433
|
+
summary: z4.object({
|
|
434
|
+
projectId: z4.string(),
|
|
435
|
+
projectName: z4.string(),
|
|
436
|
+
period: z4.number(),
|
|
437
|
+
totalReturned: z4.number()
|
|
438
|
+
})
|
|
439
|
+
};
|
|
440
|
+
async function executeGetSlowestTests(client, input) {
|
|
441
|
+
const response = await client.getSlowestTests({
|
|
442
|
+
projectId: input.projectId,
|
|
443
|
+
days: input.days,
|
|
444
|
+
limit: input.limit,
|
|
445
|
+
framework: input.framework
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
slowestTests: response.slowestTests.map((test) => ({
|
|
449
|
+
name: test.name,
|
|
450
|
+
fullName: test.fullName,
|
|
451
|
+
filePath: test.filePath,
|
|
452
|
+
framework: test.framework,
|
|
453
|
+
avgDurationMs: test.avgDurationMs,
|
|
454
|
+
p95DurationMs: test.p95DurationMs,
|
|
455
|
+
runCount: test.runCount
|
|
456
|
+
})),
|
|
457
|
+
summary: response.summary
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
var getSlowestTestsMetadata = {
|
|
461
|
+
name: "get_slowest_tests",
|
|
462
|
+
title: "Get Slowest Tests",
|
|
463
|
+
description: `Get the slowest tests in a project, sorted by P95 duration.
|
|
464
|
+
|
|
465
|
+
When using a user API Key (gaf_), you must provide a projectId.
|
|
466
|
+
Use list_projects first to find available project IDs.
|
|
467
|
+
|
|
468
|
+
Parameters:
|
|
469
|
+
- projectId (required): Project ID to analyze
|
|
470
|
+
- days (optional): Analysis period in days (default: 30, max: 365)
|
|
471
|
+
- limit (optional): Max tests to return (default: 20, max: 100)
|
|
472
|
+
- framework (optional): Filter by framework (e.g., "playwright", "vitest")
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
- List of slowest tests with:
|
|
476
|
+
- name: Short test name
|
|
477
|
+
- fullName: Full test name including describe blocks
|
|
478
|
+
- filePath: Test file path (if available)
|
|
479
|
+
- framework: Test framework used
|
|
480
|
+
- avgDurationMs: Average test duration in milliseconds
|
|
481
|
+
- p95DurationMs: 95th percentile duration (used for sorting)
|
|
482
|
+
- runCount: Number of times the test ran in the period
|
|
483
|
+
- Summary with project info and period
|
|
484
|
+
|
|
485
|
+
Use cases:
|
|
486
|
+
- "Which tests are slowing down my CI pipeline?"
|
|
487
|
+
- "Find the slowest Playwright tests to optimize"
|
|
488
|
+
- "Show me e2e tests taking over 30 seconds"`
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// src/tools/get-test-history.ts
|
|
492
|
+
import { z as z5 } from "zod";
|
|
212
493
|
var getTestHistoryInputSchema = {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
494
|
+
projectId: z5.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."),
|
|
495
|
+
testName: z5.string().optional().describe("Exact test name to search for"),
|
|
496
|
+
filePath: z5.string().optional().describe("File path containing the test"),
|
|
497
|
+
limit: z5.number().int().min(1).max(100).optional().describe("Maximum number of results (default: 20)")
|
|
216
498
|
};
|
|
217
499
|
var getTestHistoryOutputSchema = {
|
|
218
|
-
history:
|
|
219
|
-
testRunId:
|
|
220
|
-
createdAt:
|
|
221
|
-
branch:
|
|
222
|
-
commitSha:
|
|
223
|
-
status:
|
|
224
|
-
durationMs:
|
|
225
|
-
message:
|
|
500
|
+
history: z5.array(z5.object({
|
|
501
|
+
testRunId: z5.string(),
|
|
502
|
+
createdAt: z5.string(),
|
|
503
|
+
branch: z5.string().optional(),
|
|
504
|
+
commitSha: z5.string().optional(),
|
|
505
|
+
status: z5.enum(["passed", "failed", "skipped", "pending"]),
|
|
506
|
+
durationMs: z5.number(),
|
|
507
|
+
message: z5.string().optional()
|
|
226
508
|
})),
|
|
227
|
-
summary:
|
|
228
|
-
totalRuns:
|
|
229
|
-
passedRuns:
|
|
230
|
-
failedRuns:
|
|
231
|
-
passRate:
|
|
509
|
+
summary: z5.object({
|
|
510
|
+
totalRuns: z5.number(),
|
|
511
|
+
passedRuns: z5.number(),
|
|
512
|
+
failedRuns: z5.number(),
|
|
513
|
+
passRate: z5.number().nullable()
|
|
232
514
|
})
|
|
233
515
|
};
|
|
234
516
|
async function executeGetTestHistory(client, input) {
|
|
@@ -236,6 +518,7 @@ async function executeGetTestHistory(client, input) {
|
|
|
236
518
|
throw new Error("Either testName or filePath is required");
|
|
237
519
|
}
|
|
238
520
|
const response = await client.getTestHistory({
|
|
521
|
+
projectId: input.projectId,
|
|
239
522
|
testName: input.testName,
|
|
240
523
|
filePath: input.filePath,
|
|
241
524
|
limit: input.limit || 20
|
|
@@ -264,6 +547,9 @@ var getTestHistoryMetadata = {
|
|
|
264
547
|
title: "Get Test History",
|
|
265
548
|
description: `Get the pass/fail history for a specific test.
|
|
266
549
|
|
|
550
|
+
When using a user API Key (gaf_), you must provide a projectId.
|
|
551
|
+
Use list_projects first to find available project IDs.
|
|
552
|
+
|
|
267
553
|
Search by either:
|
|
268
554
|
- testName: The exact name of the test (e.g., "should handle user login")
|
|
269
555
|
- filePath: The file path containing the test (e.g., "tests/auth.test.ts")
|
|
@@ -278,32 +564,79 @@ Returns:
|
|
|
278
564
|
Use this to investigate flaky tests or understand test stability.`
|
|
279
565
|
};
|
|
280
566
|
|
|
567
|
+
// src/tools/list-projects.ts
|
|
568
|
+
import { z as z6 } from "zod";
|
|
569
|
+
var listProjectsInputSchema = {
|
|
570
|
+
organizationId: z6.string().optional().describe("Filter by organization ID (optional)"),
|
|
571
|
+
limit: z6.number().int().min(1).max(100).optional().describe("Maximum number of projects to return (default: 50)")
|
|
572
|
+
};
|
|
573
|
+
var listProjectsOutputSchema = {
|
|
574
|
+
projects: z6.array(z6.object({
|
|
575
|
+
id: z6.string(),
|
|
576
|
+
name: z6.string(),
|
|
577
|
+
description: z6.string().nullable().optional(),
|
|
578
|
+
organization: z6.object({
|
|
579
|
+
id: z6.string(),
|
|
580
|
+
name: z6.string(),
|
|
581
|
+
slug: z6.string()
|
|
582
|
+
})
|
|
583
|
+
})),
|
|
584
|
+
total: z6.number()
|
|
585
|
+
};
|
|
586
|
+
async function executeListProjects(client, input) {
|
|
587
|
+
const response = await client.listProjects({
|
|
588
|
+
organizationId: input.organizationId,
|
|
589
|
+
limit: input.limit
|
|
590
|
+
});
|
|
591
|
+
return {
|
|
592
|
+
projects: response.projects.map((p) => ({
|
|
593
|
+
id: p.id,
|
|
594
|
+
name: p.name,
|
|
595
|
+
description: p.description,
|
|
596
|
+
organization: p.organization
|
|
597
|
+
})),
|
|
598
|
+
total: response.pagination.total
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
var listProjectsMetadata = {
|
|
602
|
+
name: "list_projects",
|
|
603
|
+
title: "List Projects",
|
|
604
|
+
description: `List all projects you have access to.
|
|
605
|
+
|
|
606
|
+
Returns a list of projects with their IDs, names, and organization info.
|
|
607
|
+
Use this to find project IDs for other tools like get_project_health.
|
|
608
|
+
|
|
609
|
+
Requires a user API Key (gaf_). Get one from Account Settings in the Gaffer dashboard.`
|
|
610
|
+
};
|
|
611
|
+
|
|
281
612
|
// src/tools/list-test-runs.ts
|
|
282
|
-
import { z as
|
|
613
|
+
import { z as z7 } from "zod";
|
|
283
614
|
var listTestRunsInputSchema = {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
615
|
+
projectId: z7.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."),
|
|
616
|
+
commitSha: z7.string().optional().describe("Filter by commit SHA (exact or prefix match)"),
|
|
617
|
+
branch: z7.string().optional().describe("Filter by branch name"),
|
|
618
|
+
status: z7.enum(["passed", "failed"]).optional().describe('Filter by status: "passed" (no failures) or "failed" (has failures)'),
|
|
619
|
+
limit: z7.number().int().min(1).max(100).optional().describe("Maximum number of test runs to return (default: 20)")
|
|
288
620
|
};
|
|
289
621
|
var listTestRunsOutputSchema = {
|
|
290
|
-
testRuns:
|
|
291
|
-
id:
|
|
292
|
-
commitSha:
|
|
293
|
-
branch:
|
|
294
|
-
passedCount:
|
|
295
|
-
failedCount:
|
|
296
|
-
skippedCount:
|
|
297
|
-
totalCount:
|
|
298
|
-
createdAt:
|
|
622
|
+
testRuns: z7.array(z7.object({
|
|
623
|
+
id: z7.string(),
|
|
624
|
+
commitSha: z7.string().optional(),
|
|
625
|
+
branch: z7.string().optional(),
|
|
626
|
+
passedCount: z7.number(),
|
|
627
|
+
failedCount: z7.number(),
|
|
628
|
+
skippedCount: z7.number(),
|
|
629
|
+
totalCount: z7.number(),
|
|
630
|
+
createdAt: z7.string()
|
|
299
631
|
})),
|
|
300
|
-
pagination:
|
|
301
|
-
total:
|
|
302
|
-
hasMore:
|
|
632
|
+
pagination: z7.object({
|
|
633
|
+
total: z7.number(),
|
|
634
|
+
hasMore: z7.boolean()
|
|
303
635
|
})
|
|
304
636
|
};
|
|
305
637
|
async function executeListTestRuns(client, input) {
|
|
306
638
|
const response = await client.getTestRuns({
|
|
639
|
+
projectId: input.projectId,
|
|
307
640
|
commitSha: input.commitSha,
|
|
308
641
|
branch: input.branch,
|
|
309
642
|
status: input.status,
|
|
@@ -329,7 +662,10 @@ async function executeListTestRuns(client, input) {
|
|
|
329
662
|
var listTestRunsMetadata = {
|
|
330
663
|
name: "list_test_runs",
|
|
331
664
|
title: "List Test Runs",
|
|
332
|
-
description: `List recent test runs for
|
|
665
|
+
description: `List recent test runs for a project with optional filtering.
|
|
666
|
+
|
|
667
|
+
When using a user API Key (gaf_), you must provide a projectId.
|
|
668
|
+
Use list_projects first to find available project IDs.
|
|
333
669
|
|
|
334
670
|
Filter by:
|
|
335
671
|
- commitSha: Filter by commit SHA (supports prefix matching)
|
|
@@ -353,10 +689,11 @@ Use cases:
|
|
|
353
689
|
|
|
354
690
|
// src/index.ts
|
|
355
691
|
async function main() {
|
|
356
|
-
|
|
692
|
+
const apiKey = process.env.GAFFER_API_KEY;
|
|
693
|
+
if (!apiKey) {
|
|
357
694
|
console.error("Error: GAFFER_API_KEY environment variable is required");
|
|
358
695
|
console.error("");
|
|
359
|
-
console.error("Get your API
|
|
696
|
+
console.error("Get your API Key from: https://app.gaffer.sh/account/api-keys");
|
|
360
697
|
console.error("");
|
|
361
698
|
console.error("Then configure Claude Code or Cursor with:");
|
|
362
699
|
console.error(JSON.stringify({
|
|
@@ -365,7 +702,7 @@ async function main() {
|
|
|
365
702
|
command: "npx",
|
|
366
703
|
args: ["-y", "@gaffer-sh/mcp"],
|
|
367
704
|
env: {
|
|
368
|
-
GAFFER_API_KEY: "
|
|
705
|
+
GAFFER_API_KEY: "gaf_your-api-key-here"
|
|
369
706
|
}
|
|
370
707
|
}
|
|
371
708
|
}
|
|
@@ -473,6 +810,78 @@ async function main() {
|
|
|
473
810
|
}
|
|
474
811
|
}
|
|
475
812
|
);
|
|
813
|
+
server.registerTool(
|
|
814
|
+
listProjectsMetadata.name,
|
|
815
|
+
{
|
|
816
|
+
title: listProjectsMetadata.title,
|
|
817
|
+
description: listProjectsMetadata.description,
|
|
818
|
+
inputSchema: listProjectsInputSchema,
|
|
819
|
+
outputSchema: listProjectsOutputSchema
|
|
820
|
+
},
|
|
821
|
+
async (input) => {
|
|
822
|
+
try {
|
|
823
|
+
const output = await executeListProjects(client, input);
|
|
824
|
+
return {
|
|
825
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
826
|
+
structuredContent: output
|
|
827
|
+
};
|
|
828
|
+
} catch (error) {
|
|
829
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
830
|
+
return {
|
|
831
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
832
|
+
isError: true
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
);
|
|
837
|
+
server.registerTool(
|
|
838
|
+
getReportMetadata.name,
|
|
839
|
+
{
|
|
840
|
+
title: getReportMetadata.title,
|
|
841
|
+
description: getReportMetadata.description,
|
|
842
|
+
inputSchema: getReportInputSchema,
|
|
843
|
+
outputSchema: getReportOutputSchema
|
|
844
|
+
},
|
|
845
|
+
async (input) => {
|
|
846
|
+
try {
|
|
847
|
+
const output = await executeGetReport(client, input);
|
|
848
|
+
return {
|
|
849
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
850
|
+
structuredContent: output
|
|
851
|
+
};
|
|
852
|
+
} catch (error) {
|
|
853
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
854
|
+
return {
|
|
855
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
856
|
+
isError: true
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
);
|
|
861
|
+
server.registerTool(
|
|
862
|
+
getSlowestTestsMetadata.name,
|
|
863
|
+
{
|
|
864
|
+
title: getSlowestTestsMetadata.title,
|
|
865
|
+
description: getSlowestTestsMetadata.description,
|
|
866
|
+
inputSchema: getSlowestTestsInputSchema,
|
|
867
|
+
outputSchema: getSlowestTestsOutputSchema
|
|
868
|
+
},
|
|
869
|
+
async (input) => {
|
|
870
|
+
try {
|
|
871
|
+
const output = await executeGetSlowestTests(client, input);
|
|
872
|
+
return {
|
|
873
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
874
|
+
structuredContent: output
|
|
875
|
+
};
|
|
876
|
+
} catch (error) {
|
|
877
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
878
|
+
return {
|
|
879
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
880
|
+
isError: true
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
);
|
|
476
885
|
const transport = new StdioServerTransport();
|
|
477
886
|
await server.connect(transport);
|
|
478
887
|
}
|
package/package.json
CHANGED