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,467 @@
1
+ import { randomUUID } from 'crypto'
2
+ import { Server as HttpServer } from 'http'
3
+
4
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
5
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
6
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
7
+ import cors from 'cors'
8
+ import express, { Express } from 'express'
9
+
10
+ import { SERVER_NAME, SERVER_VERSION, PROTOCOL_VERSION, JSON_BODY_LIMIT, DEFAULT_CORS_ORIGINS } from '../constants.js'
11
+ import { Logger } from '../utils/logger.js'
12
+
13
+ // Extend Express Request interface to include requestId
14
+ declare global {
15
+ namespace Express {
16
+ interface Request {
17
+ requestId?: string
18
+ }
19
+ }
20
+ }
21
+
22
+ export interface HttpTransportOptions {
23
+ port: number
24
+ host?: string
25
+ cors?: {
26
+ origin?: string | string[] | boolean
27
+ credentials?: boolean
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Creates an HTTP server using MCP's official Streamable HTTP transport
33
+ */
34
+ export class StreamableHttpTransport {
35
+ private app: Express
36
+ private httpServer: HttpServer | null = null
37
+ private transports = new Map<string, StreamableHTTPServerTransport>()
38
+ private createMcpServer: () => McpServer
39
+
40
+ constructor(
41
+ createMcpServer: () => McpServer,
42
+ private options: HttpTransportOptions,
43
+ private logger: Logger,
44
+ ) {
45
+ this.createMcpServer = createMcpServer
46
+ this.app = express()
47
+ this.setupMiddleware()
48
+ this.setupRoutes()
49
+ }
50
+
51
+ private setupMiddleware(): void {
52
+ // Request logging middleware
53
+ this.app.use((req, res, next) => {
54
+ const requestId = randomUUID().substring(0, 8)
55
+ req.requestId = requestId
56
+
57
+ const logger = this.logger // Capture logger reference for closure
58
+ logger.debug(`[${requestId}] ${req.method} ${req.path}`, {
59
+ headers: req.headers,
60
+ query: req.query,
61
+ ip: req.ip,
62
+ })
63
+
64
+ // Log response details
65
+ const originalSend = res.send
66
+ res.send = function (body) {
67
+ logger.debug(`[${requestId}] Response ${res.statusCode}`, {
68
+ statusCode: res.statusCode,
69
+ headers: res.getHeaders(),
70
+ bodySize: typeof body === 'string' ? body.length : JSON.stringify(body).length,
71
+ })
72
+
73
+ // Log response body for errors or when explicitly requested
74
+ if (res.statusCode >= 400) {
75
+ logger.error(`[${requestId}] Error response body`, {
76
+ body: typeof body === 'string' ? body : JSON.stringify(body, null, 2),
77
+ })
78
+ }
79
+
80
+ return originalSend.call(this, body)
81
+ }
82
+
83
+ next()
84
+ })
85
+
86
+ // Security: Validate Origin header to prevent DNS rebinding attacks
87
+ this.app.use((req, res, next) => {
88
+ const origin = req.get('Origin')
89
+ if (origin && !this.isValidOrigin(origin)) {
90
+ const requestId = req.requestId || 'unknown'
91
+ this.logger.warn(`[${requestId}] Invalid origin rejected`, { origin })
92
+ return res.status(403).json({
93
+ error: 'Invalid origin',
94
+ requestId,
95
+ timestamp: new Date().toISOString(),
96
+ })
97
+ }
98
+ next()
99
+ })
100
+
101
+ // Enable CORS with specific security considerations
102
+ this.app.use(
103
+ cors({
104
+ origin: this.options.cors?.origin ?? true,
105
+ credentials: this.options.cors?.credentials ?? true,
106
+ methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
107
+ allowedHeaders: ['Content-Type', 'Authorization', 'Cache-Control', 'Accept', 'Mcp-Session-Id', 'Last-Event-ID'],
108
+ }),
109
+ )
110
+
111
+ // Parse JSON bodies with size limit
112
+ this.app.use(express.json({ limit: JSON_BODY_LIMIT }))
113
+
114
+ // Health check endpoint
115
+ this.app.get('/health', (req, res) => {
116
+ res.json({
117
+ status: 'healthy',
118
+ timestamp: new Date().toISOString(),
119
+ transport: 'streamable-http',
120
+ protocol: '2025-03-26',
121
+ activeSessions: this.transports.size,
122
+ })
123
+ })
124
+ }
125
+
126
+ private setupRoutes(): void {
127
+ // Main MCP Streamable HTTP endpoint
128
+ this.app
129
+ .route('/mcp')
130
+ .post(this.handlePost.bind(this))
131
+ .get(this.handleGet.bind(this))
132
+ .delete(this.handleDelete.bind(this))
133
+
134
+ // Root endpoint for compatibility
135
+ this.app
136
+ .route('/')
137
+ .post(this.handlePost.bind(this))
138
+ .get(this.handleGet.bind(this))
139
+ .delete(this.handleDelete.bind(this))
140
+
141
+ // MCP server info endpoint
142
+ this.app.get('/mcp/info', (req, res) => {
143
+ res.json({
144
+ name: SERVER_NAME,
145
+ version: SERVER_VERSION,
146
+ transport: 'streamable-http',
147
+ protocol: PROTOCOL_VERSION,
148
+ capabilities: {
149
+ tools: true,
150
+ resources: false,
151
+ prompts: false,
152
+ sessionManagement: true,
153
+ streaming: true,
154
+ },
155
+ activeSessions: this.transports.size,
156
+ })
157
+ })
158
+ }
159
+
160
+ private async handlePost(req: express.Request, res: express.Response): Promise<void> {
161
+ const requestId = req.requestId || randomUUID().substring(0, 8)
162
+ const sessionId = req.get('Mcp-Session-Id')
163
+
164
+ // Log incoming request details
165
+ this.logger.debug(`[${requestId}] POST request`, {
166
+ sessionId,
167
+ contentType: req.get('Content-Type'),
168
+ bodySize: JSON.stringify(req.body).length,
169
+ userAgent: req.get('User-Agent'),
170
+ })
171
+
172
+ try {
173
+ let transport = sessionId ? this.transports.get(sessionId) : undefined
174
+
175
+ // If this is an initialize request and we don't have a transport, create one
176
+ if (!transport && Array.isArray(req.body) ? isInitializeRequest(req.body[0]) : isInitializeRequest(req.body)) {
177
+ const newSessionId = randomUUID()
178
+
179
+ this.logger.info(`[${requestId}] Creating new session: ${newSessionId}`)
180
+
181
+ // Create new MCP server instance for this session
182
+ const mcpServer = this.createMcpServer()
183
+
184
+ // Create streamable HTTP transport
185
+ transport = new StreamableHTTPServerTransport({
186
+ sessionIdGenerator: () => newSessionId,
187
+ })
188
+ this.transports.set(newSessionId, transport)
189
+
190
+ // Set session ID in response header
191
+ res.setHeader('Mcp-Session-Id', newSessionId)
192
+
193
+ // Connect transport to server
194
+ await mcpServer.connect(transport)
195
+
196
+ this.logger.info(`[${requestId}] New MCP session created: ${newSessionId}`)
197
+
198
+ // Handle the initial request
199
+ await transport.handleRequest(req, res, req.body)
200
+ return
201
+ }
202
+
203
+ // Use existing transport
204
+ if (!transport) {
205
+ const errorResponse = {
206
+ error: 'Session not found. Please initialize first.',
207
+ code: 'SESSION_NOT_FOUND',
208
+ requestId,
209
+ sessionId,
210
+ }
211
+ this.logger.warn(`[${requestId}] Session not found`, errorResponse)
212
+ res.status(400).json(errorResponse)
213
+ return
214
+ }
215
+
216
+ this.logger.debug(`[${requestId}] Using existing session: ${sessionId}`)
217
+
218
+ // Handle the request through the existing transport
219
+ await transport.handleRequest(req, res, req.body)
220
+
221
+ this.logger.debug(`[${requestId}] Request handled successfully`)
222
+ } catch (error) {
223
+ const errorDetails = {
224
+ requestId,
225
+ sessionId,
226
+ error:
227
+ error instanceof Error
228
+ ? {
229
+ name: error.name,
230
+ message: error.message,
231
+ stack: error.stack,
232
+ }
233
+ : String(error),
234
+ requestBody: req.body,
235
+ headers: req.headers,
236
+ timestamp: new Date().toISOString(),
237
+ }
238
+
239
+ this.logger.error(`[${requestId}] Error in POST handler`, errorDetails)
240
+
241
+ const errorResponse = {
242
+ error: 'Internal server error',
243
+ message: error instanceof Error ? error.message : String(error),
244
+ requestId,
245
+ timestamp: new Date().toISOString(),
246
+ }
247
+
248
+ this.logger.error(`[${requestId}] POST error response`, errorResponse)
249
+
250
+ res.status(500).json(errorResponse)
251
+ }
252
+ }
253
+
254
+ private async handleGet(req: express.Request, res: express.Response): Promise<void> {
255
+ const requestId = req.requestId || randomUUID().substring(0, 8)
256
+ const sessionId = req.get('Mcp-Session-Id')
257
+
258
+ this.logger.debug(`[${requestId}] GET request`, {
259
+ sessionId,
260
+ lastEventId: req.get('Last-Event-ID'),
261
+ userAgent: req.get('User-Agent'),
262
+ })
263
+
264
+ try {
265
+ if (sessionId) {
266
+ const transport = this.transports.get(sessionId)
267
+ if (transport) {
268
+ this.logger.debug(`[${requestId}] Using existing session for GET: ${sessionId}`)
269
+ // Handle GET request for existing session (e.g., resumable connections)
270
+ await transport.handleRequest(req, res)
271
+ this.logger.debug(`[${requestId}] GET request handled successfully`)
272
+ return
273
+ } else {
274
+ this.logger.warn(`[${requestId}] Session not found for GET: ${sessionId}`)
275
+ }
276
+ }
277
+
278
+ // Return server information for GET requests without session
279
+ const infoResponse = {
280
+ name: SERVER_NAME,
281
+ version: SERVER_VERSION,
282
+ transport: 'streamable-http',
283
+ protocol: PROTOCOL_VERSION,
284
+ endpoint: '/mcp',
285
+ methods: ['POST', 'GET', 'DELETE'],
286
+ capabilities: {
287
+ tools: true,
288
+ resources: false,
289
+ prompts: false,
290
+ sessionManagement: true,
291
+ streaming: true,
292
+ },
293
+ activeSessions: this.transports.size,
294
+ usage: {
295
+ initialize: 'POST /mcp with initialize request',
296
+ communicate: 'POST /mcp with Mcp-Session-Id header',
297
+ cleanup: 'DELETE /mcp with Mcp-Session-Id header',
298
+ },
299
+ requestId,
300
+ timestamp: new Date().toISOString(),
301
+ }
302
+
303
+ this.logger.debug(`[${requestId}] Server info response`, infoResponse)
304
+ res.json(infoResponse)
305
+ } catch (error) {
306
+ const errorDetails = {
307
+ requestId,
308
+ sessionId,
309
+ error:
310
+ error instanceof Error
311
+ ? {
312
+ name: error.name,
313
+ message: error.message,
314
+ stack: error.stack,
315
+ }
316
+ : String(error),
317
+ headers: req.headers,
318
+ timestamp: new Date().toISOString(),
319
+ }
320
+
321
+ this.logger.error(`[${requestId}] Error in GET handler`, errorDetails)
322
+
323
+ const errorResponse = {
324
+ error: 'Internal server error',
325
+ message: error instanceof Error ? error.message : String(error),
326
+ requestId,
327
+ timestamp: new Date().toISOString(),
328
+ }
329
+
330
+ this.logger.error(`[${requestId}] GET error response`, errorResponse)
331
+ res.status(500).json(errorResponse)
332
+ }
333
+ }
334
+
335
+ private async handleDelete(req: express.Request, res: express.Response): Promise<void> {
336
+ const requestId = req.requestId || randomUUID().substring(0, 8)
337
+ const sessionId = req.get('Mcp-Session-Id')
338
+
339
+ this.logger.debug(`[${requestId}] DELETE request`, {
340
+ sessionId,
341
+ userAgent: req.get('User-Agent'),
342
+ })
343
+
344
+ if (!sessionId) {
345
+ const errorResponse = {
346
+ error: 'Session ID required for DELETE requests',
347
+ requestId,
348
+ timestamp: new Date().toISOString(),
349
+ }
350
+ this.logger.warn(`[${requestId}] Missing session ID`, errorResponse)
351
+ res.status(400).json(errorResponse)
352
+ return
353
+ }
354
+
355
+ const transport = this.transports.get(sessionId)
356
+ if (transport) {
357
+ try {
358
+ this.logger.info(`[${requestId}] Closing session: ${sessionId}`)
359
+ await transport.close()
360
+ this.transports.delete(sessionId)
361
+
362
+ const successResponse = {
363
+ message: 'Session closed successfully',
364
+ sessionId,
365
+ requestId,
366
+ timestamp: new Date().toISOString(),
367
+ }
368
+
369
+ this.logger.info(`[${requestId}] MCP session closed: ${sessionId}`)
370
+ this.logger.debug(`[${requestId}] Success response`, successResponse)
371
+ res.json(successResponse)
372
+ } catch (error) {
373
+ const errorDetails = {
374
+ requestId,
375
+ sessionId,
376
+ error:
377
+ error instanceof Error
378
+ ? {
379
+ name: error.name,
380
+ message: error.message,
381
+ stack: error.stack,
382
+ }
383
+ : String(error),
384
+ timestamp: new Date().toISOString(),
385
+ }
386
+
387
+ this.logger.error(`[${requestId}] Error closing session`, errorDetails)
388
+
389
+ const errorResponse = {
390
+ error: 'Error closing session',
391
+ message: error instanceof Error ? error.message : String(error),
392
+ requestId,
393
+ sessionId,
394
+ timestamp: new Date().toISOString(),
395
+ }
396
+
397
+ this.logger.error(`[${requestId}] DELETE error response`, errorResponse)
398
+ res.status(500).json(errorResponse)
399
+ }
400
+ } else {
401
+ const errorResponse = {
402
+ error: 'Session not found',
403
+ sessionId,
404
+ requestId,
405
+ timestamp: new Date().toISOString(),
406
+ }
407
+
408
+ this.logger.warn(`[${requestId}] Session not found`, errorResponse)
409
+ this.logger.debug(`[${requestId}] Not found response`, errorResponse)
410
+ res.status(404).json(errorResponse)
411
+ }
412
+ }
413
+
414
+ private isValidOrigin(origin: string): boolean {
415
+ // Implement your origin validation logic here
416
+ // For development, allow localhost and 127.0.0.1
417
+ const allowedOrigins = DEFAULT_CORS_ORIGINS
418
+
419
+ return allowedOrigins.some((allowed) => origin.includes(allowed))
420
+ }
421
+
422
+ async start(): Promise<void> {
423
+ return new Promise((resolve, reject) => {
424
+ try {
425
+ this.httpServer = this.app.listen(
426
+ this.options.port,
427
+ this.options.host || DEFAULT_CORS_ORIGINS[1], // Bind to localhost for security
428
+ () => {
429
+ this.logger.info(
430
+ `Streamable HTTP transport listening on ${this.options.host || '127.0.0.1'}:${this.options.port}`,
431
+ )
432
+ this.logger.info(`MCP endpoint: http://${this.options.host || 'localhost'}:${this.options.port}/mcp`)
433
+ this.logger.info(`Protocol: MCP 2025-03-26 with Streamable HTTP`)
434
+ resolve()
435
+ },
436
+ )
437
+
438
+ this.httpServer?.on('error', (error: Error) => {
439
+ reject(error)
440
+ })
441
+ } catch (error) {
442
+ reject(error)
443
+ }
444
+ })
445
+ }
446
+
447
+ async close(): Promise<void> {
448
+ // Close all active transports
449
+ const closePromises = Array.from(this.transports.values()).map((transport) =>
450
+ transport.close().catch((error) => this.logger.error('Error closing transport', error)),
451
+ )
452
+
453
+ await Promise.all(closePromises)
454
+ this.transports.clear()
455
+
456
+ return new Promise((resolve) => {
457
+ if (this.httpServer) {
458
+ this.httpServer?.close(() => {
459
+ this.logger.info('Streamable HTTP transport closed')
460
+ resolve()
461
+ })
462
+ } else {
463
+ resolve()
464
+ }
465
+ })
466
+ }
467
+ }
@@ -0,0 +1,26 @@
1
+ export { StdioTransport } from './stdio.js'
2
+ export { StreamableHttpTransport } from './http.js'
3
+ export type { HttpTransportOptions } from './http.js'
4
+
5
+ // Transport type enum
6
+ export enum TransportType {
7
+ STDIO = 'stdio',
8
+ HTTP = 'http',
9
+ STREAMABLE_HTTP = 'streamable-http',
10
+ }
11
+
12
+ // Legacy type alias for backwards compatibility
13
+ export type TransportTypeString = 'stdio' | 'http' | 'streamable-http'
14
+
15
+ /**
16
+ * Transport configuration interface
17
+ */
18
+ export interface TransportConfig {
19
+ type: TransportType
20
+ port?: number
21
+ host?: string
22
+ cors?: {
23
+ origin?: string | string[] | boolean
24
+ credentials?: boolean
25
+ }
26
+ }
@@ -0,0 +1,28 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
+
4
+ /**
5
+ * Creates a STDIO transport for MCP server
6
+ * This is the default transport for local MCP servers
7
+ */
8
+ export class StdioTransport {
9
+ private transport: StdioServerTransport
10
+ private mcpServer: Server
11
+
12
+ constructor(mcpServer: Server) {
13
+ this.mcpServer = mcpServer
14
+ this.transport = new StdioServerTransport()
15
+ }
16
+
17
+ async start(): Promise<void> {
18
+ await this.mcpServer.connect(this.transport)
19
+ console.error('🚀 Cucumber Studio MCP Server running on stdio')
20
+ console.error('📡 Transport: STDIO (standard input/output)')
21
+ console.error('🔄 Protocol: MCP 2025-03-26')
22
+ }
23
+
24
+ async close(): Promise<void> {
25
+ await this.transport.close()
26
+ console.error('🛑 STDIO transport closed')
27
+ }
28
+ }
@@ -0,0 +1,45 @@
1
+ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'
2
+
3
+ /**
4
+ * Convert various error types into MCP-compatible errors
5
+ */
6
+ export function createMcpError(error: unknown, context?: string): McpError {
7
+ if (error instanceof McpError) {
8
+ return error
9
+ }
10
+
11
+ if (error instanceof Error) {
12
+ // Handle API-specific errors
13
+ if (error.name === 'CucumberStudioApiError') {
14
+ return new McpError(
15
+ ErrorCode.InternalError,
16
+ `Cucumber Studio API error${context ? ` (${context})` : ''}: ${error.message}`,
17
+ )
18
+ }
19
+
20
+ // Handle validation errors
21
+ if (error.name === 'ZodError') {
22
+ return new McpError(
23
+ ErrorCode.InvalidParams,
24
+ `Validation error${context ? ` (${context})` : ''}: ${error.message}`,
25
+ )
26
+ }
27
+
28
+ // Handle general errors
29
+ return new McpError(ErrorCode.InternalError, `${context ? `${context}: ` : ''}${error.message}`)
30
+ }
31
+
32
+ // Handle unknown errors
33
+ return new McpError(ErrorCode.InternalError, `Unknown error${context ? ` (${context})` : ''}: ${String(error)}`)
34
+ }
35
+
36
+ /**
37
+ * Safely execute async operations with error handling
38
+ */
39
+ export async function safeExecute<T>(operation: () => Promise<T>, context?: string): Promise<T> {
40
+ try {
41
+ return await operation()
42
+ } catch (error) {
43
+ throw createMcpError(error, context)
44
+ }
45
+ }
@@ -0,0 +1,18 @@
1
+ // ANSI color codes for console output
2
+ export const LOG_COLORS = {
3
+ DEBUG: '\x1b[36m', // Cyan
4
+ INFO: '\x1b[32m', // Green
5
+ WARN: '\x1b[33m', // Yellow
6
+ ERROR: '\x1b[31m', // Red
7
+ RESET: '\x1b[0m', // Reset
8
+ } as const
9
+
10
+ // Log level configuration
11
+ export const LOG_LEVELS = {
12
+ DEBUG: 0,
13
+ INFO: 1,
14
+ WARN: 2,
15
+ ERROR: 3,
16
+ } as const
17
+
18
+ export type LogLevel = keyof typeof LOG_LEVELS