cucumberstudio-mcp 1.1.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/.env.example +36 -0
- package/.github/workflows/pr-checks.yml +41 -0
- package/.github/workflows/release.yml +194 -0
- package/.prettierignore +26 -0
- package/.prettierrc +14 -0
- package/CLAUDE.md +140 -0
- package/Dockerfile +50 -0
- package/Dockerfile.dev +31 -0
- package/LICENSE +21 -0
- package/README.md +395 -0
- package/build/api/client.d.ts +49 -0
- package/build/api/client.d.ts.map +1 -0
- package/build/api/client.js +204 -0
- package/build/api/client.js.map +1 -0
- package/build/api/types.d.ts +113 -0
- package/build/api/types.d.ts.map +1 -0
- package/build/api/types.js +2 -0
- package/build/api/types.js.map +1 -0
- package/build/config/settings.d.ts +123 -0
- package/build/config/settings.d.ts.map +1 -0
- package/build/config/settings.js +97 -0
- package/build/config/settings.js.map +1 -0
- package/build/constants.d.ts +16 -0
- package/build/constants.d.ts.map +1 -0
- package/build/constants.js +24 -0
- package/build/constants.js.map +1 -0
- package/build/generated/version.d.ts +3 -0
- package/build/generated/version.d.ts.map +1 -0
- package/build/generated/version.js +5 -0
- package/build/generated/version.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +81 -0
- package/build/index.js.map +1 -0
- package/build/mcp-server.d.ts +6 -0
- package/build/mcp-server.d.ts.map +1 -0
- package/build/mcp-server.js +263 -0
- package/build/mcp-server.js.map +1 -0
- package/build/tools/action-words.d.ts +18 -0
- package/build/tools/action-words.d.ts.map +1 -0
- package/build/tools/action-words.js +191 -0
- package/build/tools/action-words.js.map +1 -0
- package/build/tools/projects.d.ts +19 -0
- package/build/tools/projects.d.ts.map +1 -0
- package/build/tools/projects.js +123 -0
- package/build/tools/projects.js.map +1 -0
- package/build/tools/scenarios.d.ts +18 -0
- package/build/tools/scenarios.d.ts.map +1 -0
- package/build/tools/scenarios.js +194 -0
- package/build/tools/scenarios.js.map +1 -0
- package/build/tools/test-runs.d.ts +21 -0
- package/build/tools/test-runs.d.ts.map +1 -0
- package/build/tools/test-runs.js +324 -0
- package/build/tools/test-runs.js.map +1 -0
- package/build/transports/http.d.ts +38 -0
- package/build/transports/http.d.ts.map +1 -0
- package/build/transports/http.js +381 -0
- package/build/transports/http.js.map +1 -0
- package/build/transports/index.d.ts +22 -0
- package/build/transports/index.d.ts.map +1 -0
- package/build/transports/index.js +10 -0
- package/build/transports/index.js.map +1 -0
- package/build/transports/stdio.d.ts +13 -0
- package/build/transports/stdio.d.ts.map +1 -0
- package/build/transports/stdio.js +24 -0
- package/build/transports/stdio.js.map +1 -0
- package/build/utils/errors.d.ts +10 -0
- package/build/utils/errors.d.ts.map +1 -0
- package/build/utils/errors.js +35 -0
- package/build/utils/errors.js.map +1 -0
- package/build/utils/logger-constants.d.ts +15 -0
- package/build/utils/logger-constants.d.ts.map +1 -0
- package/build/utils/logger-constants.js +16 -0
- package/build/utils/logger-constants.js.map +1 -0
- package/build/utils/logger.d.ts +55 -0
- package/build/utils/logger.d.ts.map +1 -0
- package/build/utils/logger.js +113 -0
- package/build/utils/logger.js.map +1 -0
- package/build/utils/validation.d.ts +89 -0
- package/build/utils/validation.d.ts.map +1 -0
- package/build/utils/validation.js +78 -0
- package/build/utils/validation.js.map +1 -0
- package/docker-compose.yml +20 -0
- package/eslint.config.js +97 -0
- package/package.json +92 -0
- package/scripts/generate-version.js +31 -0
- package/src/api/client.ts +286 -0
- package/src/api/types.ts +137 -0
- package/src/config/settings.ts +113 -0
- package/src/constants.ts +29 -0
- package/src/index.ts +99 -0
- package/src/mcp-server.ts +342 -0
- package/src/tools/action-words.ts +240 -0
- package/src/tools/projects.ts +144 -0
- package/src/tools/scenarios.ts +231 -0
- package/src/tools/test-runs.ts +400 -0
- package/src/transports/http.ts +467 -0
- package/src/transports/index.ts +26 -0
- package/src/transports/stdio.ts +28 -0
- package/src/utils/errors.ts +45 -0
- package/src/utils/logger-constants.ts +18 -0
- package/src/utils/logger.ts +150 -0
- package/src/utils/validation.ts +94 -0
- package/test/api/client-with-msw.test.ts +122 -0
- package/test/api/client.test.ts +326 -0
- package/test/api/types.test.ts +88 -0
- package/test/config/settings.test.ts +204 -0
- package/test/mocks/data/action-words.ts +40 -0
- package/test/mocks/data/index.ts +13 -0
- package/test/mocks/data/projects.ts +38 -0
- package/test/mocks/data/scenarios.ts +53 -0
- package/test/mocks/data/test-runs.ts +101 -0
- package/test/mocks/handlers/action-words.ts +52 -0
- package/test/mocks/handlers/index.ts +10 -0
- package/test/mocks/handlers/projects.ts +45 -0
- package/test/mocks/handlers/scenarios.ts +72 -0
- package/test/mocks/handlers/test-runs.ts +106 -0
- package/test/mocks/server.ts +26 -0
- package/test/setup/vitest.setup.ts +18 -0
- package/test/tools/coverage-boost.test.ts +252 -0
- package/test/tools/projects.test.ts +290 -0
- package/test/tools/tools-basic.test.ts +146 -0
- package/test/transports/http-basic.test.ts +87 -0
- package/test/transports/http-simple.test.ts +33 -0
- package/test/transports/stdio.test.ts +73 -0
- package/test/utils/errors.test.ts +117 -0
- package/test/utils/validation.test.ts +261 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +27 -0
- package/vitest.config.ts +43 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import axios, { AxiosInstance, AxiosResponse } from 'axios'
|
|
2
|
+
|
|
3
|
+
import { Config } from '../config/settings.js'
|
|
4
|
+
import { API_VERSION_HEADER, API_TIMEOUT, REDACTED_STRING } from '../constants.js'
|
|
5
|
+
import { Logger } from '../utils/logger.js'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
CucumberStudioResponse,
|
|
9
|
+
CucumberStudioError,
|
|
10
|
+
ListParams,
|
|
11
|
+
Project,
|
|
12
|
+
Scenario,
|
|
13
|
+
ActionWord,
|
|
14
|
+
Folder,
|
|
15
|
+
TestRun,
|
|
16
|
+
TestExecution,
|
|
17
|
+
Build,
|
|
18
|
+
ExecutionEnvironment,
|
|
19
|
+
} from './types.js'
|
|
20
|
+
|
|
21
|
+
export class CucumberStudioApiError extends Error {
|
|
22
|
+
constructor(
|
|
23
|
+
message: string,
|
|
24
|
+
public status?: number,
|
|
25
|
+
public details?: CucumberStudioError,
|
|
26
|
+
) {
|
|
27
|
+
super(message)
|
|
28
|
+
this.name = 'CucumberStudioApiError'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class CucumberStudioApiClient {
|
|
33
|
+
private client: AxiosInstance
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private config: Config,
|
|
37
|
+
private logger: Logger,
|
|
38
|
+
) {
|
|
39
|
+
this.client = axios.create({
|
|
40
|
+
baseURL: config.cucumberStudio.baseUrl,
|
|
41
|
+
headers: {
|
|
42
|
+
Accept: API_VERSION_HEADER,
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
'access-token': config.cucumberStudio.accessToken,
|
|
45
|
+
client: config.cucumberStudio.clientId,
|
|
46
|
+
uid: config.cucumberStudio.uid,
|
|
47
|
+
},
|
|
48
|
+
timeout: API_TIMEOUT,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Add request interceptor for logging
|
|
52
|
+
this.client.interceptors.request.use(
|
|
53
|
+
(config) => {
|
|
54
|
+
this.logger.debug(`🚀 Request: ${config.method?.toUpperCase()} ${config.url}`, {
|
|
55
|
+
headers: this.sanitizeHeaders(config.headers),
|
|
56
|
+
params: config.params,
|
|
57
|
+
bodySize: config.data ? JSON.stringify(config.data).length : 0,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (this.getLoggingConfig().logRequestBodies && config.data) {
|
|
61
|
+
this.logger.trace('📤 Request Body:', config.data)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return config
|
|
65
|
+
},
|
|
66
|
+
(error) => {
|
|
67
|
+
this.logger.error('❌ Request Error:', error)
|
|
68
|
+
return Promise.reject(error)
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
// Add response interceptor for logging and error handling
|
|
73
|
+
this.client.interceptors.response.use(
|
|
74
|
+
(response) => {
|
|
75
|
+
this.logger.debug(
|
|
76
|
+
`✅ Response: ${response.status} ${response.config.method?.toUpperCase()} ${response.config.url}`,
|
|
77
|
+
{
|
|
78
|
+
status: response.status,
|
|
79
|
+
statusText: response.statusText,
|
|
80
|
+
headers: response.headers,
|
|
81
|
+
dataSize: response.data ? JSON.stringify(response.data).length : 0,
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if (this.getLoggingConfig().logApiResponses || this.getLoggingConfig().logResponseBodies) {
|
|
86
|
+
this.logger.debug('📥 Cucumber Studio Response:', {
|
|
87
|
+
status: response.status,
|
|
88
|
+
url: response.config.url,
|
|
89
|
+
data: response.data,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return response
|
|
94
|
+
},
|
|
95
|
+
(error) => {
|
|
96
|
+
if (error.response) {
|
|
97
|
+
const status = error.response.status
|
|
98
|
+
const data = error.response.data as CucumberStudioError
|
|
99
|
+
|
|
100
|
+
this.logger.error(`❌ API Error: ${status} ${error.config?.method?.toUpperCase()} ${error.config?.url}`, {
|
|
101
|
+
status,
|
|
102
|
+
statusText: error.response.statusText,
|
|
103
|
+
data,
|
|
104
|
+
headers: error.response.headers,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
let message = `API request failed with status ${status}`
|
|
108
|
+
if (data?.errors?.length > 0) {
|
|
109
|
+
message = data.errors.map((e) => e.detail).join('; ')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new CucumberStudioApiError(message, status, data)
|
|
113
|
+
} else if (error.request) {
|
|
114
|
+
this.logger.error('🔌 No Response:', {
|
|
115
|
+
url: error.config?.url,
|
|
116
|
+
timeout: error.code === 'ECONNABORTED',
|
|
117
|
+
})
|
|
118
|
+
throw new CucumberStudioApiError('No response received from Cucumber Studio API')
|
|
119
|
+
} else {
|
|
120
|
+
this.logger.error('⚙️ Request Setup Error:', error.message)
|
|
121
|
+
throw new CucumberStudioApiError(`Request setup failed: ${error.message}`)
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get logging configuration with safe defaults
|
|
129
|
+
*/
|
|
130
|
+
private getLoggingConfig() {
|
|
131
|
+
return (
|
|
132
|
+
this.config.logging || {
|
|
133
|
+
level: 'info' as const,
|
|
134
|
+
logApiResponses: false,
|
|
135
|
+
logRequestBodies: false,
|
|
136
|
+
logResponseBodies: false,
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Sanitize headers to avoid logging sensitive information
|
|
143
|
+
*/
|
|
144
|
+
private sanitizeHeaders(headers: Record<string, unknown>): Record<string, unknown> {
|
|
145
|
+
if (!headers) return headers
|
|
146
|
+
|
|
147
|
+
const sanitized = { ...headers }
|
|
148
|
+
const sensitiveKeys = ['access-token', 'authorization', 'cookie', 'x-api-key']
|
|
149
|
+
|
|
150
|
+
for (const key of sensitiveKeys) {
|
|
151
|
+
if (key in sanitized) {
|
|
152
|
+
sanitized[key] = REDACTED_STRING
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return sanitized
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Generic GET request handler
|
|
161
|
+
*/
|
|
162
|
+
private async get<T>(endpoint: string, params?: Record<string, unknown>): Promise<CucumberStudioResponse<T>> {
|
|
163
|
+
const response: AxiosResponse<CucumberStudioResponse<T>> = await this.client.get(endpoint, { params })
|
|
164
|
+
return response.data
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// PROJECT ENDPOINTS
|
|
168
|
+
async getProjects(params?: ListParams): Promise<CucumberStudioResponse<Project[]>> {
|
|
169
|
+
return this.get<Project[]>('/projects', params)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async getProject(projectId: string): Promise<CucumberStudioResponse<Project>> {
|
|
173
|
+
return this.get<Project>(`/projects/${projectId}`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// SCENARIO ENDPOINTS
|
|
177
|
+
async getScenarios(projectId: string, params?: ListParams): Promise<CucumberStudioResponse<Scenario[]>> {
|
|
178
|
+
return this.get<Scenario[]>(`/projects/${projectId}/scenarios`, params)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async getScenario(projectId: string, scenarioId: string): Promise<CucumberStudioResponse<Scenario>> {
|
|
182
|
+
return this.get<Scenario>(`/projects/${projectId}/scenarios/${scenarioId}`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async findScenariosByTag(
|
|
186
|
+
projectId: string,
|
|
187
|
+
tags: string,
|
|
188
|
+
params?: ListParams,
|
|
189
|
+
): Promise<CucumberStudioResponse<Scenario[]>> {
|
|
190
|
+
return this.get<Scenario[]>(`/projects/${projectId}/scenarios/find_by_tags`, {
|
|
191
|
+
...params,
|
|
192
|
+
'filter[tags]': tags,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ACTION WORD ENDPOINTS
|
|
197
|
+
async getActionWords(projectId: string, params?: ListParams): Promise<CucumberStudioResponse<ActionWord[]>> {
|
|
198
|
+
return this.get<ActionWord[]>(`/projects/${projectId}/actionwords`, params)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async getActionWord(projectId: string, actionWordId: string): Promise<CucumberStudioResponse<ActionWord>> {
|
|
202
|
+
return this.get<ActionWord>(`/projects/${projectId}/actionwords/${actionWordId}`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async findActionWordsByTag(
|
|
206
|
+
projectId: string,
|
|
207
|
+
tags: string,
|
|
208
|
+
params?: ListParams,
|
|
209
|
+
): Promise<CucumberStudioResponse<ActionWord[]>> {
|
|
210
|
+
return this.get<ActionWord[]>(`/projects/${projectId}/actionwords/find_by_tags`, {
|
|
211
|
+
...params,
|
|
212
|
+
'filter[tags]': tags,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// FOLDER ENDPOINTS
|
|
217
|
+
async getFolders(projectId: string, params?: ListParams): Promise<CucumberStudioResponse<Folder[]>> {
|
|
218
|
+
return this.get<Folder[]>(`/projects/${projectId}/folders`, params)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async getFolder(projectId: string, folderId: string): Promise<CucumberStudioResponse<Folder>> {
|
|
222
|
+
return this.get<Folder>(`/projects/${projectId}/folders/${folderId}`)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async getFolderChildren(
|
|
226
|
+
projectId: string,
|
|
227
|
+
folderId: string,
|
|
228
|
+
params?: ListParams,
|
|
229
|
+
): Promise<CucumberStudioResponse<Folder[]>> {
|
|
230
|
+
return this.get<Folder[]>(`/projects/${projectId}/folders/${folderId}/children`, params)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async getFolderScenarios(
|
|
234
|
+
projectId: string,
|
|
235
|
+
folderId: string,
|
|
236
|
+
params?: ListParams,
|
|
237
|
+
): Promise<CucumberStudioResponse<Scenario[]>> {
|
|
238
|
+
return this.get<Scenario[]>(`/projects/${projectId}/folders/${folderId}/scenarios`, params)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// TEST RUN ENDPOINTS
|
|
242
|
+
async getTestRuns(projectId: string, params?: ListParams): Promise<CucumberStudioResponse<TestRun[]>> {
|
|
243
|
+
return this.get<TestRun[]>(`/projects/${projectId}/test_runs`, params)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async getTestRun(projectId: string, testRunId: string): Promise<CucumberStudioResponse<TestRun>> {
|
|
247
|
+
return this.get<TestRun>(`/projects/${projectId}/test_runs/${testRunId}`)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async getTestExecutions(
|
|
251
|
+
projectId: string,
|
|
252
|
+
testRunId: string,
|
|
253
|
+
params?: ListParams,
|
|
254
|
+
): Promise<CucumberStudioResponse<TestExecution[]>> {
|
|
255
|
+
return this.get<TestExecution[]>(`/projects/${projectId}/test_runs/${testRunId}/test_executions`, params)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// BUILD ENDPOINTS
|
|
259
|
+
async getBuilds(projectId: string, params?: ListParams): Promise<CucumberStudioResponse<Build[]>> {
|
|
260
|
+
return this.get<Build[]>(`/projects/${projectId}/builds`, params)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async getBuild(projectId: string, buildId: string): Promise<CucumberStudioResponse<Build>> {
|
|
264
|
+
return this.get<Build>(`/projects/${projectId}/builds/${buildId}`)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// EXECUTION ENVIRONMENT ENDPOINTS
|
|
268
|
+
async getExecutionEnvironments(
|
|
269
|
+
projectId: string,
|
|
270
|
+
params?: ListParams,
|
|
271
|
+
): Promise<CucumberStudioResponse<ExecutionEnvironment[]>> {
|
|
272
|
+
return this.get<ExecutionEnvironment[]>(`/projects/${projectId}/execution_environments`, params)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Test the connection to Cucumber Studio API
|
|
277
|
+
*/
|
|
278
|
+
async testConnection(): Promise<boolean> {
|
|
279
|
+
try {
|
|
280
|
+
await this.getProjects({ 'page[size]': 1 })
|
|
281
|
+
return true
|
|
282
|
+
} catch {
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
package/src/api/types.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Common API response structure for Cucumber Studio
|
|
2
|
+
export interface CucumberStudioResponse<T = unknown> {
|
|
3
|
+
data: T
|
|
4
|
+
included?: Record<string, unknown>[]
|
|
5
|
+
meta?: {
|
|
6
|
+
count?: number
|
|
7
|
+
total_count?: number
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Base attributes common to many resources
|
|
12
|
+
export interface BaseAttributes {
|
|
13
|
+
id: string
|
|
14
|
+
type: string
|
|
15
|
+
attributes: Record<string, unknown>
|
|
16
|
+
relationships?: Record<string, unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Project related types
|
|
20
|
+
export interface Project extends BaseAttributes {
|
|
21
|
+
type: 'projects'
|
|
22
|
+
attributes: {
|
|
23
|
+
name: string
|
|
24
|
+
description?: string
|
|
25
|
+
created_at: string
|
|
26
|
+
updated_at: string
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Scenario related types
|
|
31
|
+
export interface Scenario extends BaseAttributes {
|
|
32
|
+
type: 'scenarios'
|
|
33
|
+
attributes: {
|
|
34
|
+
name: string
|
|
35
|
+
description?: string
|
|
36
|
+
definition?: string
|
|
37
|
+
created_at: string
|
|
38
|
+
updated_at: string
|
|
39
|
+
folder_id?: string
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Action Word related types
|
|
44
|
+
export interface ActionWord extends BaseAttributes {
|
|
45
|
+
type: 'actionwords'
|
|
46
|
+
attributes: {
|
|
47
|
+
name: string
|
|
48
|
+
description?: string
|
|
49
|
+
definition?: string
|
|
50
|
+
created_at: string
|
|
51
|
+
updated_at: string
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Folder related types
|
|
56
|
+
export interface Folder extends BaseAttributes {
|
|
57
|
+
type: 'folders'
|
|
58
|
+
attributes: {
|
|
59
|
+
name: string
|
|
60
|
+
description?: string
|
|
61
|
+
created_at: string
|
|
62
|
+
updated_at: string
|
|
63
|
+
parent_id?: string
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Test Run related types
|
|
68
|
+
export interface TestRun extends BaseAttributes {
|
|
69
|
+
type: 'test_runs'
|
|
70
|
+
attributes: {
|
|
71
|
+
name: string
|
|
72
|
+
description?: string
|
|
73
|
+
created_at: string
|
|
74
|
+
updated_at: string
|
|
75
|
+
execution_environment?: string
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Test Execution related types
|
|
80
|
+
export interface TestExecution extends BaseAttributes {
|
|
81
|
+
type: 'test_executions'
|
|
82
|
+
attributes: {
|
|
83
|
+
status: 'passed' | 'failed' | 'wip' | 'retest' | 'blocked' | 'skipped' | 'undefined'
|
|
84
|
+
created_at: string
|
|
85
|
+
updated_at: string
|
|
86
|
+
scenario_id: string
|
|
87
|
+
test_run_id: string
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Build related types
|
|
92
|
+
export interface Build extends BaseAttributes {
|
|
93
|
+
type: 'builds'
|
|
94
|
+
attributes: {
|
|
95
|
+
name: string
|
|
96
|
+
description?: string
|
|
97
|
+
created_at: string
|
|
98
|
+
updated_at: string
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Execution Environment related types
|
|
103
|
+
export interface ExecutionEnvironment extends BaseAttributes {
|
|
104
|
+
type: 'execution_environments'
|
|
105
|
+
attributes: {
|
|
106
|
+
name: string
|
|
107
|
+
description?: string
|
|
108
|
+
created_at: string
|
|
109
|
+
updated_at: string
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Tag related types
|
|
114
|
+
export interface Tag extends BaseAttributes {
|
|
115
|
+
type: 'tags'
|
|
116
|
+
attributes: {
|
|
117
|
+
key: string
|
|
118
|
+
value?: string
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// API Error structure
|
|
123
|
+
export interface CucumberStudioError {
|
|
124
|
+
errors: Array<{
|
|
125
|
+
detail: string
|
|
126
|
+
status: string
|
|
127
|
+
title: string
|
|
128
|
+
}>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Query parameters for list endpoints
|
|
132
|
+
export interface ListParams extends Record<string, unknown> {
|
|
133
|
+
'page[number]'?: number
|
|
134
|
+
'page[size]'?: number
|
|
135
|
+
'filter[name]'?: string
|
|
136
|
+
'filter[tags]'?: string
|
|
137
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { config as loadDotenv } from 'dotenv'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
SERVER_NAME,
|
|
6
|
+
SERVER_VERSION,
|
|
7
|
+
DEFAULT_API_BASE_URL,
|
|
8
|
+
DEFAULT_PORT,
|
|
9
|
+
DEFAULT_HOST,
|
|
10
|
+
DEFAULT_LOG_LEVEL,
|
|
11
|
+
} from '../constants.js'
|
|
12
|
+
|
|
13
|
+
// Configuration schema for type safety and validation
|
|
14
|
+
const ConfigSchema = z.object({
|
|
15
|
+
cucumberStudio: z.object({
|
|
16
|
+
baseUrl: z.string().url().default(DEFAULT_API_BASE_URL),
|
|
17
|
+
accessToken: z.string().min(1),
|
|
18
|
+
clientId: z.string().min(1),
|
|
19
|
+
uid: z.string().min(1),
|
|
20
|
+
}),
|
|
21
|
+
server: z.object({
|
|
22
|
+
name: z.string().default(SERVER_NAME),
|
|
23
|
+
version: z.string().default(SERVER_VERSION),
|
|
24
|
+
transport: z.enum(['stdio', 'http', 'streamable-http']).default('stdio'),
|
|
25
|
+
port: z.number().int().min(1).max(65535).default(DEFAULT_PORT),
|
|
26
|
+
host: z.string().default(DEFAULT_HOST),
|
|
27
|
+
}),
|
|
28
|
+
logging: z.object({
|
|
29
|
+
level: z.enum(['error', 'warn', 'info', 'debug', 'trace']).default(DEFAULT_LOG_LEVEL as 'info'),
|
|
30
|
+
logApiResponses: z.boolean().default(false),
|
|
31
|
+
logRequestBodies: z.boolean().default(false),
|
|
32
|
+
logResponseBodies: z.boolean().default(false),
|
|
33
|
+
transport: z.enum(['console', 'file', 'stderr', 'none']).default('stderr'),
|
|
34
|
+
filePath: z.string().optional(),
|
|
35
|
+
}),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export type Config = z.infer<typeof ConfigSchema>
|
|
39
|
+
|
|
40
|
+
export class ConfigManager {
|
|
41
|
+
private config: Config | null = null
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load configuration from environment variables
|
|
45
|
+
*/
|
|
46
|
+
public loadFromEnvironment(): Config {
|
|
47
|
+
// Auto-load .env file if it exists
|
|
48
|
+
loadDotenv({ path: '.env' })
|
|
49
|
+
const rawConfig = {
|
|
50
|
+
cucumberStudio: {
|
|
51
|
+
baseUrl: process.env.CUCUMBERSTUDIO_BASE_URL,
|
|
52
|
+
accessToken: process.env.CUCUMBERSTUDIO_ACCESS_TOKEN,
|
|
53
|
+
clientId: process.env.CUCUMBERSTUDIO_CLIENT_ID,
|
|
54
|
+
uid: process.env.CUCUMBERSTUDIO_UID,
|
|
55
|
+
},
|
|
56
|
+
server: {
|
|
57
|
+
name: process.env.MCP_SERVER_NAME,
|
|
58
|
+
version: process.env.MCP_SERVER_VERSION,
|
|
59
|
+
transport: process.env.MCP_TRANSPORT,
|
|
60
|
+
port: process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : undefined,
|
|
61
|
+
host: process.env.MCP_HOST,
|
|
62
|
+
},
|
|
63
|
+
logging: {
|
|
64
|
+
level: process.env.LOG_LEVEL,
|
|
65
|
+
logApiResponses: process.env.LOG_API_RESPONSES === 'true',
|
|
66
|
+
logRequestBodies: process.env.LOG_REQUEST_BODIES === 'true',
|
|
67
|
+
logResponseBodies: process.env.LOG_RESPONSE_BODIES === 'true',
|
|
68
|
+
transport: process.env.LOG_TRANSPORT,
|
|
69
|
+
filePath: process.env.LOG_FILE,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
this.config = ConfigSchema.parse(rawConfig)
|
|
75
|
+
return this.config
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof z.ZodError) {
|
|
78
|
+
const missingFields = error.errors.map((e) => e.path.join('.')).join(', ')
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Configuration validation failed. Missing or invalid fields: ${missingFields}. ` +
|
|
81
|
+
'Please ensure all required environment variables are set: ' +
|
|
82
|
+
'CUCUMBERSTUDIO_ACCESS_TOKEN, CUCUMBERSTUDIO_CLIENT_ID, CUCUMBERSTUDIO_UID',
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
throw error
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get current configuration
|
|
91
|
+
*/
|
|
92
|
+
public getConfig(): Config {
|
|
93
|
+
if (!this.config) {
|
|
94
|
+
throw new Error('Configuration not loaded. Call loadFromEnvironment() first.')
|
|
95
|
+
}
|
|
96
|
+
return this.config
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate that all required configuration is present
|
|
101
|
+
*/
|
|
102
|
+
public validate(): boolean {
|
|
103
|
+
try {
|
|
104
|
+
this.getConfig()
|
|
105
|
+
return true
|
|
106
|
+
} catch {
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Singleton instance
|
|
113
|
+
export const configManager = new ConfigManager()
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Import build-time generated version
|
|
2
|
+
import { PACKAGE_VERSION, PACKAGE_NAME } from './generated/version.js'
|
|
3
|
+
|
|
4
|
+
// Server Configuration
|
|
5
|
+
export const SERVER_NAME = PACKAGE_NAME
|
|
6
|
+
export const SERVER_VERSION = PACKAGE_VERSION // Build-time constant from package.json
|
|
7
|
+
export const PROTOCOL_VERSION = '2025-03-26' // MCP protocol version
|
|
8
|
+
|
|
9
|
+
// API Configuration
|
|
10
|
+
export const API_VERSION_HEADER = 'application/vnd.api+json; version=1'
|
|
11
|
+
export const API_TIMEOUT = 30000 // 30 seconds
|
|
12
|
+
export const DEFAULT_API_BASE_URL = 'https://studio.cucumberstudio.com/api'
|
|
13
|
+
|
|
14
|
+
// Network Configuration
|
|
15
|
+
export const DEFAULT_PORT = 3000
|
|
16
|
+
export const DEFAULT_HOST = '0.0.0.0'
|
|
17
|
+
export const JSON_BODY_LIMIT = '10mb'
|
|
18
|
+
export const DEFAULT_CORS_ORIGINS = ['localhost', '127.0.0.1', '0.0.0.0']
|
|
19
|
+
|
|
20
|
+
// Pagination
|
|
21
|
+
export const MAX_PAGE_SIZE = 100
|
|
22
|
+
export const DEFAULT_PAGE_SIZE = 20
|
|
23
|
+
|
|
24
|
+
// Security
|
|
25
|
+
export const REDACTED_STRING = '***REDACTED***'
|
|
26
|
+
|
|
27
|
+
// Logger
|
|
28
|
+
export const LOG_PREFIX = '🥒 API'
|
|
29
|
+
export const DEFAULT_LOG_LEVEL = 'info'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
4
|
+
import { config as loadDotenv } from 'dotenv'
|
|
5
|
+
|
|
6
|
+
import { createCucumberStudioMcpServer } from './mcp-server.js'
|
|
7
|
+
import { StreamableHttpTransport, TransportType } from './transports/index.js'
|
|
8
|
+
import { StderrLogger, getLogLevel } from './utils/logger.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Main entry point for the Cucumber Studio MCP Server
|
|
12
|
+
* Supports both STDIO and HTTP transports based on environment variables
|
|
13
|
+
*/
|
|
14
|
+
async function main(): Promise<void> {
|
|
15
|
+
// Load .env file first to ensure environment variables are available
|
|
16
|
+
loadDotenv({ path: '.env' })
|
|
17
|
+
|
|
18
|
+
// Determine transport based on environment variable
|
|
19
|
+
const transportString = process.env.MCP_TRANSPORT?.toLowerCase() || 'stdio'
|
|
20
|
+
const transport = Object.values(TransportType).includes(transportString as TransportType)
|
|
21
|
+
? (transportString as TransportType)
|
|
22
|
+
: TransportType.STDIO
|
|
23
|
+
|
|
24
|
+
const port = parseInt(process.env.MCP_PORT || '3000', 10)
|
|
25
|
+
const host = process.env.MCP_HOST || '127.0.0.1'
|
|
26
|
+
|
|
27
|
+
console.error(`🎯 Starting Cucumber Studio MCP Server with ${transport} transport...`)
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
switch (transport) {
|
|
31
|
+
case TransportType.HTTP:
|
|
32
|
+
case TransportType.STREAMABLE_HTTP: {
|
|
33
|
+
// HTTP/Streamable HTTP transport
|
|
34
|
+
const httpLogger = new StderrLogger({ level: getLogLevel(), prefix: '🌐 HTTP' })
|
|
35
|
+
const httpTransport = new StreamableHttpTransport(
|
|
36
|
+
createCucumberStudioMcpServer,
|
|
37
|
+
{
|
|
38
|
+
port,
|
|
39
|
+
host,
|
|
40
|
+
cors: {
|
|
41
|
+
origin: process.env.MCP_CORS_ORIGIN === 'false' ? false : true,
|
|
42
|
+
credentials: true,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
httpLogger,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
await httpTransport.start()
|
|
49
|
+
|
|
50
|
+
// Handle graceful shutdown for HTTP transport
|
|
51
|
+
const shutdown = async () => {
|
|
52
|
+
console.error('🛑 Shutting down HTTP transport...')
|
|
53
|
+
await httpTransport.close()
|
|
54
|
+
process.exit(0)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process.on('SIGINT', shutdown)
|
|
58
|
+
process.on('SIGTERM', shutdown)
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case TransportType.STDIO:
|
|
63
|
+
default: {
|
|
64
|
+
// STDIO transport (default)
|
|
65
|
+
const server = createCucumberStudioMcpServer()
|
|
66
|
+
const stdioTransport = new StdioServerTransport()
|
|
67
|
+
|
|
68
|
+
console.error('🚀 CucumberStudio MCP Server running on stdio')
|
|
69
|
+
console.error('📡 Transport: STDIO (standard input/output)')
|
|
70
|
+
console.error('🔄 Protocol: MCP')
|
|
71
|
+
|
|
72
|
+
await server.connect(stdioTransport)
|
|
73
|
+
break
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(`❌ ${transport} transport failed to start:`, error)
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Handle graceful shutdown for STDIO transport
|
|
83
|
+
process.on('SIGINT', () => {
|
|
84
|
+
console.error('🛑 Received SIGINT, shutting down gracefully...')
|
|
85
|
+
process.exit(0)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
process.on('SIGTERM', () => {
|
|
89
|
+
console.error('🛑 Received SIGTERM, shutting down gracefully...')
|
|
90
|
+
process.exit(0)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Run if this file is executed directly
|
|
94
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
95
|
+
main().catch((error) => {
|
|
96
|
+
console.error('❌ Unhandled error:', error)
|
|
97
|
+
process.exit(1)
|
|
98
|
+
})
|
|
99
|
+
}
|