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,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
+ }
@@ -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()
@@ -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
+ }