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,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
|