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.
Files changed (130) hide show
  1. package/.env.example +36 -0
  2. package/.github/workflows/pr-checks.yml +41 -0
  3. package/.github/workflows/release.yml +194 -0
  4. package/.prettierignore +26 -0
  5. package/.prettierrc +14 -0
  6. package/CLAUDE.md +140 -0
  7. package/Dockerfile +50 -0
  8. package/Dockerfile.dev +31 -0
  9. package/LICENSE +21 -0
  10. package/README.md +395 -0
  11. package/build/api/client.d.ts +49 -0
  12. package/build/api/client.d.ts.map +1 -0
  13. package/build/api/client.js +204 -0
  14. package/build/api/client.js.map +1 -0
  15. package/build/api/types.d.ts +113 -0
  16. package/build/api/types.d.ts.map +1 -0
  17. package/build/api/types.js +2 -0
  18. package/build/api/types.js.map +1 -0
  19. package/build/config/settings.d.ts +123 -0
  20. package/build/config/settings.d.ts.map +1 -0
  21. package/build/config/settings.js +97 -0
  22. package/build/config/settings.js.map +1 -0
  23. package/build/constants.d.ts +16 -0
  24. package/build/constants.d.ts.map +1 -0
  25. package/build/constants.js +24 -0
  26. package/build/constants.js.map +1 -0
  27. package/build/generated/version.d.ts +3 -0
  28. package/build/generated/version.d.ts.map +1 -0
  29. package/build/generated/version.js +5 -0
  30. package/build/generated/version.js.map +1 -0
  31. package/build/index.d.ts +3 -0
  32. package/build/index.d.ts.map +1 -0
  33. package/build/index.js +81 -0
  34. package/build/index.js.map +1 -0
  35. package/build/mcp-server.d.ts +6 -0
  36. package/build/mcp-server.d.ts.map +1 -0
  37. package/build/mcp-server.js +263 -0
  38. package/build/mcp-server.js.map +1 -0
  39. package/build/tools/action-words.d.ts +18 -0
  40. package/build/tools/action-words.d.ts.map +1 -0
  41. package/build/tools/action-words.js +191 -0
  42. package/build/tools/action-words.js.map +1 -0
  43. package/build/tools/projects.d.ts +19 -0
  44. package/build/tools/projects.d.ts.map +1 -0
  45. package/build/tools/projects.js +123 -0
  46. package/build/tools/projects.js.map +1 -0
  47. package/build/tools/scenarios.d.ts +18 -0
  48. package/build/tools/scenarios.d.ts.map +1 -0
  49. package/build/tools/scenarios.js +194 -0
  50. package/build/tools/scenarios.js.map +1 -0
  51. package/build/tools/test-runs.d.ts +21 -0
  52. package/build/tools/test-runs.d.ts.map +1 -0
  53. package/build/tools/test-runs.js +324 -0
  54. package/build/tools/test-runs.js.map +1 -0
  55. package/build/transports/http.d.ts +38 -0
  56. package/build/transports/http.d.ts.map +1 -0
  57. package/build/transports/http.js +381 -0
  58. package/build/transports/http.js.map +1 -0
  59. package/build/transports/index.d.ts +22 -0
  60. package/build/transports/index.d.ts.map +1 -0
  61. package/build/transports/index.js +10 -0
  62. package/build/transports/index.js.map +1 -0
  63. package/build/transports/stdio.d.ts +13 -0
  64. package/build/transports/stdio.d.ts.map +1 -0
  65. package/build/transports/stdio.js +24 -0
  66. package/build/transports/stdio.js.map +1 -0
  67. package/build/utils/errors.d.ts +10 -0
  68. package/build/utils/errors.d.ts.map +1 -0
  69. package/build/utils/errors.js +35 -0
  70. package/build/utils/errors.js.map +1 -0
  71. package/build/utils/logger-constants.d.ts +15 -0
  72. package/build/utils/logger-constants.d.ts.map +1 -0
  73. package/build/utils/logger-constants.js +16 -0
  74. package/build/utils/logger-constants.js.map +1 -0
  75. package/build/utils/logger.d.ts +55 -0
  76. package/build/utils/logger.d.ts.map +1 -0
  77. package/build/utils/logger.js +113 -0
  78. package/build/utils/logger.js.map +1 -0
  79. package/build/utils/validation.d.ts +89 -0
  80. package/build/utils/validation.d.ts.map +1 -0
  81. package/build/utils/validation.js +78 -0
  82. package/build/utils/validation.js.map +1 -0
  83. package/docker-compose.yml +20 -0
  84. package/eslint.config.js +97 -0
  85. package/package.json +92 -0
  86. package/scripts/generate-version.js +31 -0
  87. package/src/api/client.ts +286 -0
  88. package/src/api/types.ts +137 -0
  89. package/src/config/settings.ts +113 -0
  90. package/src/constants.ts +29 -0
  91. package/src/index.ts +99 -0
  92. package/src/mcp-server.ts +342 -0
  93. package/src/tools/action-words.ts +240 -0
  94. package/src/tools/projects.ts +144 -0
  95. package/src/tools/scenarios.ts +231 -0
  96. package/src/tools/test-runs.ts +400 -0
  97. package/src/transports/http.ts +467 -0
  98. package/src/transports/index.ts +26 -0
  99. package/src/transports/stdio.ts +28 -0
  100. package/src/utils/errors.ts +45 -0
  101. package/src/utils/logger-constants.ts +18 -0
  102. package/src/utils/logger.ts +150 -0
  103. package/src/utils/validation.ts +94 -0
  104. package/test/api/client-with-msw.test.ts +122 -0
  105. package/test/api/client.test.ts +326 -0
  106. package/test/api/types.test.ts +88 -0
  107. package/test/config/settings.test.ts +204 -0
  108. package/test/mocks/data/action-words.ts +40 -0
  109. package/test/mocks/data/index.ts +13 -0
  110. package/test/mocks/data/projects.ts +38 -0
  111. package/test/mocks/data/scenarios.ts +53 -0
  112. package/test/mocks/data/test-runs.ts +101 -0
  113. package/test/mocks/handlers/action-words.ts +52 -0
  114. package/test/mocks/handlers/index.ts +10 -0
  115. package/test/mocks/handlers/projects.ts +45 -0
  116. package/test/mocks/handlers/scenarios.ts +72 -0
  117. package/test/mocks/handlers/test-runs.ts +106 -0
  118. package/test/mocks/server.ts +26 -0
  119. package/test/setup/vitest.setup.ts +18 -0
  120. package/test/tools/coverage-boost.test.ts +252 -0
  121. package/test/tools/projects.test.ts +290 -0
  122. package/test/tools/tools-basic.test.ts +146 -0
  123. package/test/transports/http-basic.test.ts +87 -0
  124. package/test/transports/http-simple.test.ts +33 -0
  125. package/test/transports/stdio.test.ts +73 -0
  126. package/test/utils/errors.test.ts +117 -0
  127. package/test/utils/validation.test.ts +261 -0
  128. package/tsconfig.build.json +8 -0
  129. package/tsconfig.json +27 -0
  130. package/vitest.config.ts +43 -0
@@ -0,0 +1,400 @@
1
+ import { Tool, CallToolRequest, CallToolResult, TextContent } from '@modelcontextprotocol/sdk/types.js'
2
+
3
+ import { CucumberStudioApiClient } from '../api/client.js'
4
+ import { TestRun, TestExecution, Build, ExecutionEnvironment } from '../api/types.js'
5
+ import { safeExecute } from '../utils/errors.js'
6
+ import {
7
+ validateInput,
8
+ ProjectIdSchema,
9
+ TestRunIdSchema,
10
+ BuildIdSchema,
11
+ ListParamsSchema,
12
+ convertToApiParams,
13
+ } from '../utils/validation.js'
14
+
15
+ export class TestRunTools {
16
+ constructor(private apiClient: CucumberStudioApiClient) {}
17
+
18
+ /**
19
+ * Get all available MCP tools for test runs
20
+ */
21
+ getTools(): Tool[] {
22
+ return [
23
+ {
24
+ name: 'cucumberstudio_list_test_runs',
25
+ description: 'List all test runs in a project',
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ projectId: { type: 'string', description: 'The ID of the project' },
30
+ pagination: {
31
+ type: 'object',
32
+ properties: {
33
+ page: { type: 'number', minimum: 1, description: 'Page number' },
34
+ pageSize: { type: 'number', minimum: 1, maximum: 100, description: 'Number of items per page' },
35
+ },
36
+ additionalProperties: false,
37
+ },
38
+ filter: {
39
+ type: 'object',
40
+ properties: {
41
+ name: { type: 'string', description: 'Filter test runs by name' },
42
+ },
43
+ additionalProperties: false,
44
+ },
45
+ },
46
+ required: ['projectId'],
47
+ additionalProperties: false,
48
+ },
49
+ },
50
+ {
51
+ name: 'cucumberstudio_get_test_run',
52
+ description: 'Get detailed information about a specific test run',
53
+ inputSchema: {
54
+ type: 'object',
55
+ properties: {
56
+ projectId: { type: 'string', description: 'The ID of the project' },
57
+ testRunId: { type: 'string', description: 'The ID of the test run to retrieve' },
58
+ },
59
+ required: ['projectId', 'testRunId'],
60
+ additionalProperties: false,
61
+ },
62
+ },
63
+ {
64
+ name: 'cucumberstudio_get_test_executions',
65
+ description: 'Get test executions (individual test results) for a test run',
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ projectId: { type: 'string', description: 'The ID of the project' },
70
+ testRunId: { type: 'string', description: 'The ID of the test run' },
71
+ pagination: {
72
+ type: 'object',
73
+ properties: {
74
+ page: { type: 'number', minimum: 1, description: 'Page number' },
75
+ pageSize: { type: 'number', minimum: 1, maximum: 100, description: 'Number of items per page' },
76
+ },
77
+ additionalProperties: false,
78
+ },
79
+ },
80
+ required: ['projectId', 'testRunId'],
81
+ additionalProperties: false,
82
+ },
83
+ },
84
+ {
85
+ name: 'cucumberstudio_list_builds',
86
+ description: 'List all builds in a project',
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: {
90
+ projectId: { type: 'string', description: 'The ID of the project' },
91
+ pagination: {
92
+ type: 'object',
93
+ properties: {
94
+ page: { type: 'number', minimum: 1, description: 'Page number' },
95
+ pageSize: { type: 'number', minimum: 1, maximum: 100, description: 'Number of items per page' },
96
+ },
97
+ additionalProperties: false,
98
+ },
99
+ },
100
+ required: ['projectId'],
101
+ additionalProperties: false,
102
+ },
103
+ },
104
+ {
105
+ name: 'cucumberstudio_get_build',
106
+ description: 'Get detailed information about a specific build',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ projectId: { type: 'string', description: 'The ID of the project' },
111
+ buildId: { type: 'string', description: 'The ID of the build to retrieve' },
112
+ },
113
+ required: ['projectId', 'buildId'],
114
+ additionalProperties: false,
115
+ },
116
+ },
117
+ {
118
+ name: 'cucumberstudio_list_execution_environments',
119
+ description: 'List all execution environments in a project',
120
+ inputSchema: {
121
+ type: 'object',
122
+ properties: {
123
+ projectId: { type: 'string', description: 'The ID of the project' },
124
+ pagination: {
125
+ type: 'object',
126
+ properties: {
127
+ page: { type: 'number', minimum: 1, description: 'Page number' },
128
+ pageSize: { type: 'number', minimum: 1, maximum: 100, description: 'Number of items per page' },
129
+ },
130
+ additionalProperties: false,
131
+ },
132
+ },
133
+ required: ['projectId'],
134
+ additionalProperties: false,
135
+ },
136
+ },
137
+ ]
138
+ }
139
+
140
+ /**
141
+ * Handle tool calls for test run-related operations
142
+ */
143
+ async handleToolCall(request: CallToolRequest): Promise<CallToolResult> {
144
+ switch (request.params.name) {
145
+ case 'cucumberstudio_list_test_runs':
146
+ return this.listTestRuns(request.params.arguments)
147
+
148
+ case 'cucumberstudio_get_test_run':
149
+ return this.getTestRun(request.params.arguments)
150
+
151
+ case 'cucumberstudio_get_test_executions':
152
+ return this.getTestExecutions(request.params.arguments)
153
+
154
+ case 'cucumberstudio_list_builds':
155
+ return this.listBuilds(request.params.arguments)
156
+
157
+ case 'cucumberstudio_get_build':
158
+ return this.getBuild(request.params.arguments)
159
+
160
+ case 'cucumberstudio_list_execution_environments':
161
+ return this.listExecutionEnvironments(request.params.arguments)
162
+
163
+ default:
164
+ throw new Error(`Unknown tool: ${request.params.name}`)
165
+ }
166
+ }
167
+
168
+ async listTestRuns(args: unknown): Promise<CallToolResult> {
169
+ return safeExecute(async () => {
170
+ const projectId = validateInput(ProjectIdSchema, (args as Record<string, unknown>)?.projectId, 'list_test_runs')
171
+ const listParams = validateInput(ListParamsSchema, args, 'list_test_runs')
172
+ const apiParams = convertToApiParams(listParams)
173
+
174
+ const response = await this.apiClient.getTestRuns(projectId, apiParams)
175
+
176
+ const testRuns = Array.isArray(response.data) ? response.data : [response.data]
177
+ const testRunList = testRuns.map((testRun: TestRun) => ({
178
+ id: testRun.id,
179
+ name: testRun.attributes.name,
180
+ description: testRun.attributes.description || '',
181
+ execution_environment: testRun.attributes.execution_environment || '',
182
+ created_at: testRun.attributes.created_at,
183
+ updated_at: testRun.attributes.updated_at,
184
+ }))
185
+
186
+ return {
187
+ content: [
188
+ {
189
+ type: 'text',
190
+ text: JSON.stringify(
191
+ {
192
+ test_runs: testRunList,
193
+ meta: response.meta || {},
194
+ total_count: testRunList.length,
195
+ },
196
+ null,
197
+ 2,
198
+ ),
199
+ } as TextContent,
200
+ ],
201
+ }
202
+ }, 'listing test runs')
203
+ }
204
+
205
+ async getTestRun(args: unknown): Promise<CallToolResult> {
206
+ return safeExecute(async () => {
207
+ const projectId = validateInput(ProjectIdSchema, (args as Record<string, unknown>)?.projectId, 'get_test_run')
208
+ const testRunId = validateInput(TestRunIdSchema, (args as Record<string, unknown>)?.testRunId, 'get_test_run')
209
+
210
+ const response = await this.apiClient.getTestRun(projectId, testRunId)
211
+
212
+ const testRun = response.data
213
+ const testRunDetails = {
214
+ id: testRun.id,
215
+ type: testRun.type,
216
+ name: testRun.attributes.name,
217
+ description: testRun.attributes.description || '',
218
+ execution_environment: testRun.attributes.execution_environment || '',
219
+ created_at: testRun.attributes.created_at,
220
+ updated_at: testRun.attributes.updated_at,
221
+ relationships: testRun.relationships || {},
222
+ }
223
+
224
+ return {
225
+ content: [
226
+ {
227
+ type: 'text',
228
+ text: JSON.stringify(
229
+ {
230
+ test_run: testRunDetails,
231
+ included: response.included || [],
232
+ },
233
+ null,
234
+ 2,
235
+ ),
236
+ } as TextContent,
237
+ ],
238
+ }
239
+ }, 'getting test run details')
240
+ }
241
+
242
+ async getTestExecutions(args: unknown): Promise<CallToolResult> {
243
+ return safeExecute(async () => {
244
+ const projectId = validateInput(
245
+ ProjectIdSchema,
246
+ (args as Record<string, unknown>)?.projectId,
247
+ 'get_test_executions',
248
+ )
249
+ const testRunId = validateInput(
250
+ TestRunIdSchema,
251
+ (args as Record<string, unknown>)?.testRunId,
252
+ 'get_test_executions',
253
+ )
254
+ const listParams = validateInput(ListParamsSchema, args, 'get_test_executions')
255
+ const apiParams = convertToApiParams(listParams)
256
+
257
+ const response = await this.apiClient.getTestExecutions(projectId, testRunId, apiParams)
258
+
259
+ const executions = Array.isArray(response.data) ? response.data : [response.data]
260
+ const executionList = executions.map((execution: TestExecution) => ({
261
+ id: execution.id,
262
+ status: execution.attributes.status,
263
+ scenario_id: execution.attributes.scenario_id,
264
+ test_run_id: execution.attributes.test_run_id,
265
+ created_at: execution.attributes.created_at,
266
+ updated_at: execution.attributes.updated_at,
267
+ }))
268
+
269
+ return {
270
+ content: [
271
+ {
272
+ type: 'text',
273
+ text: JSON.stringify(
274
+ {
275
+ test_executions: executionList,
276
+ test_run_id: testRunId,
277
+ meta: response.meta || {},
278
+ total_count: executionList.length,
279
+ },
280
+ null,
281
+ 2,
282
+ ),
283
+ } as TextContent,
284
+ ],
285
+ }
286
+ }, 'getting test executions')
287
+ }
288
+
289
+ async listBuilds(args: unknown): Promise<CallToolResult> {
290
+ return safeExecute(async () => {
291
+ const projectId = validateInput(ProjectIdSchema, (args as Record<string, unknown>)?.projectId, 'list_builds')
292
+ const listParams = validateInput(ListParamsSchema, args, 'list_builds')
293
+ const apiParams = convertToApiParams(listParams)
294
+
295
+ const response = await this.apiClient.getBuilds(projectId, apiParams)
296
+
297
+ const builds = Array.isArray(response.data) ? response.data : [response.data]
298
+ const buildList = builds.map((build: Build) => ({
299
+ id: build.id,
300
+ name: build.attributes.name,
301
+ description: build.attributes.description || '',
302
+ created_at: build.attributes.created_at,
303
+ updated_at: build.attributes.updated_at,
304
+ }))
305
+
306
+ return {
307
+ content: [
308
+ {
309
+ type: 'text',
310
+ text: JSON.stringify(
311
+ {
312
+ builds: buildList,
313
+ meta: response.meta || {},
314
+ total_count: buildList.length,
315
+ },
316
+ null,
317
+ 2,
318
+ ),
319
+ } as TextContent,
320
+ ],
321
+ }
322
+ }, 'listing builds')
323
+ }
324
+
325
+ async getBuild(args: unknown): Promise<CallToolResult> {
326
+ return safeExecute(async () => {
327
+ const projectId = validateInput(ProjectIdSchema, (args as Record<string, unknown>)?.projectId, 'get_build')
328
+ const buildId = validateInput(BuildIdSchema, (args as Record<string, unknown>)?.buildId, 'get_build')
329
+
330
+ const response = await this.apiClient.getBuild(projectId, buildId)
331
+
332
+ const build = response.data
333
+ const buildDetails = {
334
+ id: build.id,
335
+ type: build.type,
336
+ name: build.attributes.name,
337
+ description: build.attributes.description || '',
338
+ created_at: build.attributes.created_at,
339
+ updated_at: build.attributes.updated_at,
340
+ relationships: build.relationships || {},
341
+ }
342
+
343
+ return {
344
+ content: [
345
+ {
346
+ type: 'text',
347
+ text: JSON.stringify(
348
+ {
349
+ build: buildDetails,
350
+ included: response.included || [],
351
+ },
352
+ null,
353
+ 2,
354
+ ),
355
+ } as TextContent,
356
+ ],
357
+ }
358
+ }, 'getting build details')
359
+ }
360
+
361
+ async listExecutionEnvironments(args: unknown): Promise<CallToolResult> {
362
+ return safeExecute(async () => {
363
+ const projectId = validateInput(
364
+ ProjectIdSchema,
365
+ (args as Record<string, unknown>)?.projectId,
366
+ 'list_execution_environments',
367
+ )
368
+ const listParams = validateInput(ListParamsSchema, args, 'list_execution_environments')
369
+ const apiParams = convertToApiParams(listParams)
370
+
371
+ const response = await this.apiClient.getExecutionEnvironments(projectId, apiParams)
372
+
373
+ const environments = Array.isArray(response.data) ? response.data : [response.data]
374
+ const environmentList = environments.map((env: ExecutionEnvironment) => ({
375
+ id: env.id,
376
+ name: env.attributes.name,
377
+ description: env.attributes.description || '',
378
+ created_at: env.attributes.created_at,
379
+ updated_at: env.attributes.updated_at,
380
+ }))
381
+
382
+ return {
383
+ content: [
384
+ {
385
+ type: 'text',
386
+ text: JSON.stringify(
387
+ {
388
+ execution_environments: environmentList,
389
+ meta: response.meta || {},
390
+ total_count: environmentList.length,
391
+ },
392
+ null,
393
+ 2,
394
+ ),
395
+ } as TextContent,
396
+ ],
397
+ }
398
+ }, 'listing execution environments')
399
+ }
400
+ }