converse-mcp-server 1.0.1
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 +177 -0
- package/README.md +425 -0
- package/bin/converse.js +45 -0
- package/docs/API.md +897 -0
- package/docs/ARCHITECTURE.md +552 -0
- package/docs/EXAMPLES.md +736 -0
- package/package.json +101 -0
- package/src/config.js +521 -0
- package/src/continuationStore.js +340 -0
- package/src/index.js +216 -0
- package/src/providers/google.js +441 -0
- package/src/providers/index.js +87 -0
- package/src/providers/openai.js +348 -0
- package/src/providers/xai.js +305 -0
- package/src/router.js +497 -0
- package/src/systemPrompts.js +90 -0
- package/src/tools/chat.js +336 -0
- package/src/tools/consensus.js +478 -0
- package/src/tools/index.js +156 -0
- package/src/transport/httpTransport.js +548 -0
- package/src/utils/console.js +64 -0
- package/src/utils/contextProcessor.js +475 -0
- package/src/utils/errorHandler.js +555 -0
- package/src/utils/logger.js +450 -0
- package/src/utils/tokenLimiter.js +217 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Streaming Transport for MCP Server
|
|
3
|
+
*
|
|
4
|
+
* Implements StreamableHTTPServerTransport to replace stdio transport,
|
|
5
|
+
* eliminating console output interference and providing better local development experience.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import express from 'express';
|
|
9
|
+
import cors from 'cors';
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
12
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
+
import { createLogger } from '../utils/logger.js';
|
|
14
|
+
|
|
15
|
+
const logger = createLogger('http-transport');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* HTTP Transport Server for MCP
|
|
19
|
+
* Manages Express server with MCP endpoints and session management
|
|
20
|
+
*/
|
|
21
|
+
export class HTTPTransportServer {
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
this.config = {
|
|
24
|
+
// Server settings
|
|
25
|
+
port: config.port || 3000,
|
|
26
|
+
host: config.host || 'localhost',
|
|
27
|
+
requestTimeout: config.requestTimeout || 300000,
|
|
28
|
+
maxRequestSize: config.maxRequestSize || '10mb',
|
|
29
|
+
|
|
30
|
+
// Session management
|
|
31
|
+
sessionTimeout: config.sessionTimeout || 1800000,
|
|
32
|
+
sessionCleanupInterval: config.sessionCleanupInterval || 300000,
|
|
33
|
+
maxConcurrentSessions: config.maxConcurrentSessions || 100,
|
|
34
|
+
|
|
35
|
+
// CORS configuration
|
|
36
|
+
enableCors: config.enableCors !== false,
|
|
37
|
+
corsOptions: config.corsOptions || {
|
|
38
|
+
origin: '*',
|
|
39
|
+
methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
|
40
|
+
allowedHeaders: ['Content-Type', 'mcp-session-id', 'Authorization'],
|
|
41
|
+
credentials: false,
|
|
42
|
+
exposedHeaders: ['Mcp-Session-Id'],
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Security settings
|
|
46
|
+
enableDnsRebindingProtection: config.enableDnsRebindingProtection || false,
|
|
47
|
+
allowedHosts: config.allowedHosts || ['127.0.0.1', 'localhost'],
|
|
48
|
+
rateLimitEnabled: config.rateLimitEnabled || false,
|
|
49
|
+
rateLimitWindow: config.rateLimitWindow || 900000,
|
|
50
|
+
rateLimitMaxRequests: config.rateLimitMaxRequests || 1000,
|
|
51
|
+
|
|
52
|
+
...config
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
this.app = express();
|
|
56
|
+
this.server = null;
|
|
57
|
+
this.transports = new Map(); // sessionId -> transport
|
|
58
|
+
this.sessionTimers = new Map(); // sessionId -> timeout timer
|
|
59
|
+
this.mcpServer = null;
|
|
60
|
+
this.isStarted = false;
|
|
61
|
+
this.cleanupInterval = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Initialize the HTTP transport server
|
|
66
|
+
* @param {object} mcpServer - MCP Server instance
|
|
67
|
+
*/
|
|
68
|
+
async initialize(mcpServer) {
|
|
69
|
+
this.mcpServer = mcpServer;
|
|
70
|
+
this.setupMiddleware();
|
|
71
|
+
this.setupRoutes();
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Setup Express middleware
|
|
77
|
+
*/
|
|
78
|
+
setupMiddleware() {
|
|
79
|
+
// JSON parsing with size limit
|
|
80
|
+
this.app.use(express.json({
|
|
81
|
+
limit: this.config.maxRequestSize,
|
|
82
|
+
strict: true
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
// Request timeout middleware
|
|
86
|
+
this.app.use((req, res, next) => {
|
|
87
|
+
req.setTimeout(this.config.requestTimeout, () => {
|
|
88
|
+
logger.warn('Request timeout', {
|
|
89
|
+
data: {
|
|
90
|
+
method: req.method,
|
|
91
|
+
path: req.path,
|
|
92
|
+
timeout: this.config.requestTimeout
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
if (!res.headersSent) {
|
|
96
|
+
res.status(408).json({
|
|
97
|
+
jsonrpc: '2.0',
|
|
98
|
+
error: {
|
|
99
|
+
code: -32000,
|
|
100
|
+
message: 'Request timeout',
|
|
101
|
+
},
|
|
102
|
+
id: null,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
next();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Rate limiting middleware (if enabled)
|
|
110
|
+
if (this.config.rateLimitEnabled) {
|
|
111
|
+
const rateLimitMap = new Map();
|
|
112
|
+
|
|
113
|
+
this.app.use((req, res, next) => {
|
|
114
|
+
const clientId = req.ip || req.connection.remoteAddress;
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
const windowStart = now - this.config.rateLimitWindow;
|
|
117
|
+
|
|
118
|
+
// Clean old entries
|
|
119
|
+
const clientRequests = rateLimitMap.get(clientId) || [];
|
|
120
|
+
const validRequests = clientRequests.filter(time => time > windowStart);
|
|
121
|
+
|
|
122
|
+
if (validRequests.length >= this.config.rateLimitMaxRequests) {
|
|
123
|
+
logger.warn('Rate limit exceeded', {
|
|
124
|
+
data: {
|
|
125
|
+
clientId,
|
|
126
|
+
requests: validRequests.length,
|
|
127
|
+
limit: this.config.rateLimitMaxRequests
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
res.status(429).json({
|
|
131
|
+
jsonrpc: '2.0',
|
|
132
|
+
error: {
|
|
133
|
+
code: -32000,
|
|
134
|
+
message: 'Rate limit exceeded',
|
|
135
|
+
},
|
|
136
|
+
id: null,
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
validRequests.push(now);
|
|
142
|
+
rateLimitMap.set(clientId, validRequests);
|
|
143
|
+
next();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
logger.debug('Rate limiting enabled', {
|
|
147
|
+
data: {
|
|
148
|
+
window: this.config.rateLimitWindow,
|
|
149
|
+
maxRequests: this.config.rateLimitMaxRequests
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// CORS configuration for browser clients
|
|
155
|
+
if (this.config.enableCors) {
|
|
156
|
+
this.app.use(cors(this.config.corsOptions));
|
|
157
|
+
logger.debug('CORS enabled for HTTP transport', {
|
|
158
|
+
data: { corsOptions: this.config.corsOptions }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Request logging
|
|
163
|
+
this.app.use((req, res, next) => {
|
|
164
|
+
logger.debug('HTTP request received', {
|
|
165
|
+
data: {
|
|
166
|
+
method: req.method,
|
|
167
|
+
path: req.path,
|
|
168
|
+
sessionId: req.headers['mcp-session-id']
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
next();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Setup MCP HTTP routes
|
|
177
|
+
*/
|
|
178
|
+
setupRoutes() {
|
|
179
|
+
// Main MCP endpoint - handles POST requests for client-to-server communication
|
|
180
|
+
this.app.post('/mcp', async (req, res) => {
|
|
181
|
+
await this.handleMcpRequest(req, res);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// SSE endpoint - handles GET requests for server-to-client notifications
|
|
185
|
+
this.app.get('/mcp', async (req, res) => {
|
|
186
|
+
await this.handleSseRequest(req, res);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Session termination - handles DELETE requests
|
|
190
|
+
this.app.delete('/mcp', async (req, res) => {
|
|
191
|
+
await this.handleSessionTermination(req, res);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Health check endpoint
|
|
195
|
+
this.app.get('/health', (req, res) => {
|
|
196
|
+
res.json({
|
|
197
|
+
status: 'healthy',
|
|
198
|
+
transport: 'http',
|
|
199
|
+
server: this.mcpServer ? 'connected' : 'disconnected',
|
|
200
|
+
sessions: this.transports.size,
|
|
201
|
+
timestamp: new Date().toISOString()
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Server info endpoint
|
|
206
|
+
this.app.get('/info', (req, res) => {
|
|
207
|
+
res.json({
|
|
208
|
+
name: this.mcpServer?.serverCapabilities?.name || 'unknown',
|
|
209
|
+
version: this.mcpServer?.serverCapabilities?.version || 'unknown',
|
|
210
|
+
transport: 'http-streaming',
|
|
211
|
+
endpoints: {
|
|
212
|
+
mcp: '/mcp',
|
|
213
|
+
health: '/health',
|
|
214
|
+
info: '/info'
|
|
215
|
+
},
|
|
216
|
+
sessions: this.transports.size
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Handle MCP POST requests (client-to-server communication)
|
|
223
|
+
*/
|
|
224
|
+
async handleMcpRequest(req, res) {
|
|
225
|
+
try {
|
|
226
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
227
|
+
let transport;
|
|
228
|
+
|
|
229
|
+
if (sessionId && this.transports.has(sessionId)) {
|
|
230
|
+
// Reuse existing transport and reset session timeout
|
|
231
|
+
transport = this.transports.get(sessionId);
|
|
232
|
+
this.resetSessionTimeout(sessionId);
|
|
233
|
+
logger.debug('Reusing existing transport', { data: { sessionId } });
|
|
234
|
+
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
235
|
+
// Check session limit before creating new transport
|
|
236
|
+
if (this.transports.size >= this.config.maxConcurrentSessions) {
|
|
237
|
+
logger.warn('Maximum concurrent sessions reached', {
|
|
238
|
+
data: {
|
|
239
|
+
currentSessions: this.transports.size,
|
|
240
|
+
maxSessions: this.config.maxConcurrentSessions
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
res.status(503).json({
|
|
244
|
+
jsonrpc: '2.0',
|
|
245
|
+
error: {
|
|
246
|
+
code: -32000,
|
|
247
|
+
message: 'Maximum concurrent sessions reached. Please try again later.',
|
|
248
|
+
},
|
|
249
|
+
id: null,
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// New initialization request
|
|
255
|
+
transport = await this.createNewTransport();
|
|
256
|
+
logger.info('Created new MCP transport', {});
|
|
257
|
+
} else {
|
|
258
|
+
// Invalid request
|
|
259
|
+
logger.warn('Invalid MCP request - no session ID or not initialize request', {
|
|
260
|
+
data: { sessionId, hasInitialize: isInitializeRequest(req.body) }
|
|
261
|
+
});
|
|
262
|
+
res.status(400).json({
|
|
263
|
+
jsonrpc: '2.0',
|
|
264
|
+
error: {
|
|
265
|
+
code: -32000,
|
|
266
|
+
message: 'Bad Request: No valid session ID provided',
|
|
267
|
+
},
|
|
268
|
+
id: null,
|
|
269
|
+
});
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Handle the request through the transport
|
|
274
|
+
await transport.handleRequest(req, res, req.body);
|
|
275
|
+
|
|
276
|
+
} catch (error) {
|
|
277
|
+
logger.error('Error handling MCP request', { error });
|
|
278
|
+
if (!res.headersSent) {
|
|
279
|
+
res.status(500).json({
|
|
280
|
+
jsonrpc: '2.0',
|
|
281
|
+
error: {
|
|
282
|
+
code: -32603,
|
|
283
|
+
message: 'Internal server error',
|
|
284
|
+
},
|
|
285
|
+
id: null,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Handle SSE GET requests (server-to-client notifications)
|
|
293
|
+
*/
|
|
294
|
+
async handleSseRequest(req, res) {
|
|
295
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
296
|
+
|
|
297
|
+
if (!sessionId || !this.transports.has(sessionId)) {
|
|
298
|
+
logger.warn('SSE request with invalid session ID', { data: { sessionId } });
|
|
299
|
+
res.status(400).send('Invalid or missing session ID');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const transport = this.transports.get(sessionId);
|
|
305
|
+
this.resetSessionTimeout(sessionId);
|
|
306
|
+
await transport.handleRequest(req, res);
|
|
307
|
+
logger.debug('SSE connection established', { data: { sessionId } });
|
|
308
|
+
} catch (error) {
|
|
309
|
+
logger.error('Error handling SSE request', { error, data: { sessionId } });
|
|
310
|
+
if (!res.headersSent) {
|
|
311
|
+
res.status(500).send('Internal server error');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Handle session termination DELETE requests
|
|
318
|
+
*/
|
|
319
|
+
async handleSessionTermination(req, res) {
|
|
320
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
321
|
+
|
|
322
|
+
if (!sessionId || !this.transports.has(sessionId)) {
|
|
323
|
+
logger.warn('Session termination with invalid session ID', { data: { sessionId } });
|
|
324
|
+
res.status(400).send('Invalid or missing session ID');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const transport = this.transports.get(sessionId);
|
|
330
|
+
await transport.handleRequest(req, res);
|
|
331
|
+
|
|
332
|
+
// Clean up the transport
|
|
333
|
+
this.transports.delete(sessionId);
|
|
334
|
+
logger.info('Session terminated', { data: { sessionId } });
|
|
335
|
+
} catch (error) {
|
|
336
|
+
logger.error('Error terminating session', { error, data: { sessionId } });
|
|
337
|
+
if (!res.headersSent) {
|
|
338
|
+
res.status(500).send('Internal server error');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Create a new MCP transport with session management
|
|
345
|
+
*/
|
|
346
|
+
async createNewTransport() {
|
|
347
|
+
const transport = new StreamableHTTPServerTransport({
|
|
348
|
+
sessionIdGenerator: () => randomUUID(),
|
|
349
|
+
onsessioninitialized: (sessionId) => {
|
|
350
|
+
this.transports.set(sessionId, transport);
|
|
351
|
+
this.setupSessionTimeout(sessionId);
|
|
352
|
+
logger.debug('Transport session initialized', { data: { sessionId } });
|
|
353
|
+
},
|
|
354
|
+
enableDnsRebindingProtection: this.config.enableDnsRebindingProtection,
|
|
355
|
+
allowedHosts: this.config.allowedHosts,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Clean up transport when closed
|
|
359
|
+
transport.onclose = () => {
|
|
360
|
+
if (transport.sessionId) {
|
|
361
|
+
this.cleanupSession(transport.sessionId);
|
|
362
|
+
logger.debug('Transport session closed', {
|
|
363
|
+
data: { sessionId: transport.sessionId }
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Connect to the MCP server
|
|
369
|
+
await this.mcpServer.connect(transport);
|
|
370
|
+
|
|
371
|
+
return transport;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Setup session timeout for a given session
|
|
376
|
+
*/
|
|
377
|
+
setupSessionTimeout(sessionId) {
|
|
378
|
+
// Clear existing timeout if any
|
|
379
|
+
if (this.sessionTimers.has(sessionId)) {
|
|
380
|
+
clearTimeout(this.sessionTimers.get(sessionId));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Set new timeout
|
|
384
|
+
const timeoutId = setTimeout(() => {
|
|
385
|
+
logger.info('Session timeout expired', { data: { sessionId } });
|
|
386
|
+
this.cleanupSession(sessionId);
|
|
387
|
+
}, this.config.sessionTimeout);
|
|
388
|
+
|
|
389
|
+
this.sessionTimers.set(sessionId, timeoutId);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Reset session timeout for active session
|
|
394
|
+
*/
|
|
395
|
+
resetSessionTimeout(sessionId) {
|
|
396
|
+
if (this.transports.has(sessionId)) {
|
|
397
|
+
this.setupSessionTimeout(sessionId);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Clean up session resources
|
|
403
|
+
* Note: Following MCP SDK pattern - only clean up our references,
|
|
404
|
+
* let transport.onclose handle its own cleanup
|
|
405
|
+
*/
|
|
406
|
+
cleanupSession(sessionId) {
|
|
407
|
+
// Clear timeout
|
|
408
|
+
if (this.sessionTimers.has(sessionId)) {
|
|
409
|
+
clearTimeout(this.sessionTimers.get(sessionId));
|
|
410
|
+
this.sessionTimers.delete(sessionId);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Remove transport from our map (don't call transport.close() here)
|
|
414
|
+
if (this.transports.has(sessionId)) {
|
|
415
|
+
this.transports.delete(sessionId);
|
|
416
|
+
logger.debug('Transport session cleaned up', { data: { sessionId } });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Periodic cleanup of expired sessions
|
|
422
|
+
*/
|
|
423
|
+
startSessionCleanup() {
|
|
424
|
+
if (this.cleanupInterval) {
|
|
425
|
+
clearInterval(this.cleanupInterval);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this.cleanupInterval = setInterval(() => {
|
|
429
|
+
logger.debug('Running session cleanup', {
|
|
430
|
+
data: { activeSessions: this.transports.size }
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// The timeout mechanism handles cleanup automatically,
|
|
434
|
+
// but we can add additional checks here if needed
|
|
435
|
+
}, this.config.sessionCleanupInterval);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Start the HTTP server
|
|
440
|
+
*/
|
|
441
|
+
async start() {
|
|
442
|
+
if (this.isStarted) {
|
|
443
|
+
throw new Error('HTTP transport server is already started');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return new Promise((resolve, reject) => {
|
|
447
|
+
this.server = this.app.listen(this.config.port, this.config.host, (err) => {
|
|
448
|
+
if (err) {
|
|
449
|
+
logger.error('Failed to start HTTP transport server', { error: err });
|
|
450
|
+
reject(err);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
this.isStarted = true;
|
|
455
|
+
this.startSessionCleanup();
|
|
456
|
+
|
|
457
|
+
const address = this.server.address();
|
|
458
|
+
logger.info('HTTP transport server started', {
|
|
459
|
+
data: {
|
|
460
|
+
host: address.address,
|
|
461
|
+
port: address.port,
|
|
462
|
+
endpoint: `http://${this.config.host}:${address.port}/mcp`,
|
|
463
|
+
sessionTimeout: this.config.sessionTimeout,
|
|
464
|
+
maxSessions: this.config.maxConcurrentSessions
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
resolve(address);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Stop the HTTP server
|
|
474
|
+
*/
|
|
475
|
+
async stop() {
|
|
476
|
+
if (!this.isStarted || !this.server) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return new Promise((resolve) => {
|
|
481
|
+
// Stop session cleanup interval
|
|
482
|
+
if (this.cleanupInterval) {
|
|
483
|
+
clearInterval(this.cleanupInterval);
|
|
484
|
+
this.cleanupInterval = null;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Clear all session timers
|
|
488
|
+
for (const [sessionId, timeoutId] of this.sessionTimers) {
|
|
489
|
+
clearTimeout(timeoutId);
|
|
490
|
+
}
|
|
491
|
+
this.sessionTimers.clear();
|
|
492
|
+
|
|
493
|
+
// Clear transport references (let the server close handle actual transport cleanup)
|
|
494
|
+
this.transports.clear();
|
|
495
|
+
|
|
496
|
+
// Close all active connections first to prevent hanging handles
|
|
497
|
+
if (this.server.listening) {
|
|
498
|
+
this.server.closeAllConnections?.(); // Available in Node 18.02+
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
this.server.close((err) => {
|
|
502
|
+
this.isStarted = false;
|
|
503
|
+
if (err) {
|
|
504
|
+
logger.warn('Error closing HTTP server', { error: err });
|
|
505
|
+
} else {
|
|
506
|
+
logger.info('HTTP transport server stopped');
|
|
507
|
+
}
|
|
508
|
+
resolve();
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Get server status information
|
|
515
|
+
*/
|
|
516
|
+
getStatus() {
|
|
517
|
+
return {
|
|
518
|
+
isStarted: this.isStarted,
|
|
519
|
+
port: this.config.port,
|
|
520
|
+
host: this.config.host,
|
|
521
|
+
activeSessions: this.transports.size,
|
|
522
|
+
maxSessions: this.config.maxConcurrentSessions,
|
|
523
|
+
sessionIds: Array.from(this.transports.keys()),
|
|
524
|
+
address: this.server?.address() || null,
|
|
525
|
+
configuration: {
|
|
526
|
+
sessionTimeout: this.config.sessionTimeout,
|
|
527
|
+
sessionCleanupInterval: this.config.sessionCleanupInterval,
|
|
528
|
+
requestTimeout: this.config.requestTimeout,
|
|
529
|
+
maxRequestSize: this.config.maxRequestSize,
|
|
530
|
+
corsEnabled: this.config.enableCors,
|
|
531
|
+
rateLimitEnabled: this.config.rateLimitEnabled,
|
|
532
|
+
dnsRebindingProtection: this.config.enableDnsRebindingProtection
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Factory function to create and configure HTTP transport server
|
|
540
|
+
* @param {object} mcpServer - MCP Server instance
|
|
541
|
+
* @param {object} config - HTTP transport configuration
|
|
542
|
+
* @returns {Promise<HTTPTransportServer>}
|
|
543
|
+
*/
|
|
544
|
+
export async function createHTTPTransport(mcpServer, config = {}) {
|
|
545
|
+
const httpTransport = new HTTPTransportServer(config);
|
|
546
|
+
await httpTransport.initialize(mcpServer);
|
|
547
|
+
return httpTransport;
|
|
548
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe Console Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides console functions that respect MCP transport mode and logging settings.
|
|
5
|
+
* Prevents console output from corrupting stdio transport JSON-RPC streams.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if console output should be suppressed
|
|
10
|
+
* @returns {boolean} True if console output should be suppressed
|
|
11
|
+
*/
|
|
12
|
+
function shouldSuppressConsole() {
|
|
13
|
+
return (
|
|
14
|
+
process.env.LOG_LEVEL === 'silent' ||
|
|
15
|
+
process.env.NODE_ENV === 'test' ||
|
|
16
|
+
process.env.MCP_TRANSPORT === 'stdio'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Safe console.log that respects transport mode
|
|
22
|
+
* @param {...any} args - Arguments to log
|
|
23
|
+
*/
|
|
24
|
+
export function debugLog(...args) {
|
|
25
|
+
if (!shouldSuppressConsole()) {
|
|
26
|
+
console.log(...args);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Safe console.error that respects transport mode
|
|
32
|
+
* @param {...any} args - Arguments to log
|
|
33
|
+
*/
|
|
34
|
+
export function debugError(...args) {
|
|
35
|
+
if (!shouldSuppressConsole()) {
|
|
36
|
+
console.error(...args);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Safe console.warn that respects transport mode
|
|
42
|
+
* @param {...any} args - Arguments to log
|
|
43
|
+
*/
|
|
44
|
+
export function debugWarn(...args) {
|
|
45
|
+
if (!shouldSuppressConsole()) {
|
|
46
|
+
console.warn(...args);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Force console output (for critical errors only)
|
|
52
|
+
* @param {...any} args - Arguments to log
|
|
53
|
+
*/
|
|
54
|
+
export function forceLog(...args) {
|
|
55
|
+
console.log(...args);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Force console error output (for critical errors only)
|
|
60
|
+
* @param {...any} args - Arguments to log
|
|
61
|
+
*/
|
|
62
|
+
export function forceError(...args) {
|
|
63
|
+
console.error(...args);
|
|
64
|
+
}
|