@carbonvoice/cv-mcp-server 1.1.2 → 2.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.
Files changed (44) hide show
  1. package/dist/auth/auth.service.js +79 -0
  2. package/dist/auth/auth.service.js.map +1 -0
  3. package/dist/auth/index.js +15 -0
  4. package/dist/auth/index.js.map +1 -1
  5. package/dist/auth/interfaces/auth.types.js +3 -0
  6. package/dist/auth/interfaces/auth.types.js.map +1 -0
  7. package/dist/auth/interfaces/index.js +1 -0
  8. package/dist/auth/interfaces/index.js.map +1 -1
  9. package/dist/cv-api.js +13 -5
  10. package/dist/cv-api.js.map +1 -1
  11. package/dist/server.js +30 -30
  12. package/dist/server.js.map +1 -1
  13. package/dist/transports/http/interfaces/index.js +3 -0
  14. package/dist/transports/http/interfaces/index.js.map +1 -0
  15. package/dist/transports/http/middleware/add-mcp-session-id.middleware.js +20 -13
  16. package/dist/transports/http/middleware/add-mcp-session-id.middleware.js.map +1 -1
  17. package/dist/transports/http/middleware/add-request-id.middlware.js +13 -4
  18. package/dist/transports/http/middleware/add-request-id.middlware.js.map +1 -1
  19. package/dist/transports/http/session/index.js +34 -0
  20. package/dist/transports/http/session/index.js.map +1 -0
  21. package/dist/transports/http/session/session-cleanup.js +60 -0
  22. package/dist/transports/http/session/session-cleanup.js.map +1 -0
  23. package/dist/transports/http/session/session-manager.js +52 -0
  24. package/dist/transports/http/session/session-manager.js.map +1 -0
  25. package/dist/transports/http/session/session.config.js +29 -0
  26. package/dist/transports/http/session/session.config.js.map +1 -0
  27. package/dist/transports/http/session/session.logger.js +77 -0
  28. package/dist/transports/http/session/session.logger.js.map +1 -0
  29. package/dist/transports/http/session/session.service.js +192 -0
  30. package/dist/transports/http/session/session.service.js.map +1 -0
  31. package/dist/transports/http/session/session.types.js +31 -0
  32. package/dist/transports/http/session/session.types.js.map +1 -0
  33. package/dist/transports/http/streamable.js +172 -422
  34. package/dist/transports/http/streamable.js.map +1 -1
  35. package/dist/transports/http/utils/request-context.js +78 -0
  36. package/dist/transports/http/utils/request-context.js.map +1 -0
  37. package/dist/utils/axios-instance.js +7 -10
  38. package/dist/utils/axios-instance.js.map +1 -1
  39. package/dist/utils/logger.js +34 -11
  40. package/dist/utils/logger.js.map +1 -1
  41. package/package.json +1 -1
  42. package/readme.md +253 -11
  43. package/dist/auth/auth-middleware.js +0 -203
  44. package/dist/auth/auth-middleware.js.map +0 -1
@@ -13,46 +13,15 @@ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamable
13
13
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
14
14
  const _1 = require(".");
15
15
  const constants_1 = require("./constants");
16
+ const session_1 = require("./session");
17
+ const session_config_1 = require("./session/session.config");
16
18
  const utils_1 = require("./utils");
17
- const auth_middleware_1 = require("../../auth/auth-middleware");
19
+ const auth_1 = require("../../auth");
18
20
  const config_1 = require("../../config");
19
- const constants_2 = require("../../constants");
20
21
  const cv_api_1 = require("../../cv-api");
21
22
  const server_1 = __importDefault(require("../../server"));
22
23
  const utils_2 = require("../../utils");
23
24
  const app = (0, express_1.default)();
24
- const SESSION_TTL_MS = 1000 * 60 * 60 * 1; // 1 hour
25
- /**
26
- * Sets standard headers for all requests.
27
- * @param _req - The request.
28
- * @param res - The response.
29
- * @param next - The next handler.
30
- */
31
- function standardHeaders(_req, res, next) {
32
- // Disables all caching
33
- res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
34
- res.set('Pragma', 'no-cache');
35
- // if (getConfig().baseUrl.startsWith('https://')) {
36
- // // Only connect to this site and subdomains via HTTPS for the next two years
37
- // // and also include in the preload list
38
- // res.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
39
- // }
40
- // Set Content Security Policy
41
- // As an API server, block everything
42
- // See: https://stackoverflow.com/a/45631261/2051724
43
- res.set('Content-Security-Policy', "default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none';");
44
- // Disable browser features
45
- res.set('Permissions-Policy', 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), interest-cohort=()');
46
- // Never send the Referer header
47
- res.set('Referrer-Policy', 'no-referrer');
48
- // Prevent browsers from incorrectly detecting non-scripts as scripts
49
- res.set('X-Content-Type-Options', 'nosniff');
50
- // Disallow attempts to iframe site
51
- res.set('X-Frame-Options', 'DENY');
52
- // Block pages from loading when they detect reflected XSS attacks
53
- res.set('X-XSS-Protection', '1; mode=block');
54
- next();
55
- }
56
25
  app.set('x-powered-by', false);
57
26
  // Trust proxy for rate limiting - only trust localhost and private networks
58
27
  app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
@@ -74,65 +43,8 @@ app.use(_1.logRequest);
74
43
  let carbonVoiceApiHealth = {
75
44
  isHealthy: false,
76
45
  lastChecked: new Date().toISOString(),
46
+ apiUrl: config_1.env.CARBON_VOICE_BASE_URL,
77
47
  };
78
- const sessions = new Map();
79
- function createSession(transport, req, sessionId) {
80
- var _a, _b, _c, _d, _e, _f;
81
- // Should never happen
82
- if (!((_b = (_a = req.auth) === null || _a === void 0 ? void 0 : _a.extra) === null || _b === void 0 ? void 0 : _b.user)) {
83
- throw new Error('User not found in session creation');
84
- }
85
- // const sessionId = getOrCreateSessionId(req);
86
- // Clean up after TTL
87
- const timeout = setTimeout(() => {
88
- utils_2.logger.info('⏰ Session timeout triggered', { sessionId });
89
- destroySession(sessionId);
90
- }, SESSION_TTL_MS);
91
- const userId = (_d = (_c = req.auth) === null || _c === void 0 ? void 0 : _c.extra) === null || _d === void 0 ? void 0 : _d.user.id;
92
- sessions.set(sessionId, {
93
- transport,
94
- timeout,
95
- userId,
96
- metrics: {
97
- sessionId,
98
- userId,
99
- createdAt: new Date(),
100
- expiresAt: new Date(Date.now() + SESSION_TTL_MS),
101
- totalInteractions: 0,
102
- totalToolCalls: 0,
103
- },
104
- });
105
- utils_2.logger.info('🆕 Session created', {
106
- sessionId,
107
- userId: (_f = (_e = req.auth) === null || _e === void 0 ? void 0 : _e.extra) === null || _f === void 0 ? void 0 : _f.user.id,
108
- });
109
- return sessionId;
110
- }
111
- function destroySession(sessionId) {
112
- utils_2.logger.info('🔚 Destroying session', { sessionId });
113
- const session = sessions.get(sessionId);
114
- if (session && !session.destroying) {
115
- // Mark session as being destroyed to prevent recursive calls
116
- session.destroying = true;
117
- const {} = session.metrics;
118
- clearTimeout(session.timeout);
119
- session.transport.close();
120
- sessions.delete(sessionId);
121
- const durationInSeconds = (new Date().getTime() - session.metrics.createdAt.getTime()) / 1000;
122
- utils_2.logger.info('❌ Session destroyed', {
123
- sessionId,
124
- duration: (0, utils_2.formatTimeToHuman)(durationInSeconds),
125
- createdAt: session.metrics.createdAt.toISOString(),
126
- expiresAt: session.metrics.expiresAt.toISOString(),
127
- totalInteractions: session.metrics.totalInteractions,
128
- totalToolCalls: session.metrics.totalToolCalls,
129
- userId: session.userId,
130
- });
131
- }
132
- else if (session === null || session === void 0 ? void 0 : session.destroying) {
133
- utils_2.logger.debug('Session already being destroyed, skipping', { sessionId });
134
- }
135
- }
136
48
  app.get('/health', (req, res) => {
137
49
  const response = {
138
50
  status: carbonVoiceApiHealth.isHealthy ? 'healthy' : 'degraded',
@@ -177,108 +89,136 @@ app.head('/', _1.logRequest, (req, res) => {
177
89
  .set('MCP-Protocol-Version', mcpProtocolVersion)
178
90
  .end();
179
91
  });
180
- // Single shared transport for all requests
181
- // const sharedTransport = new StreamableHTTPServerTransport({
182
- // sessionIdGenerator: undefined, // Stateless
183
- // enableJsonResponse: true,
184
- // });
185
- // Add error handling
186
- // sharedTransport.onerror = (error) => {
187
- // logger.error('🚨 Transport error', { error: error.message });
188
- // };
189
- // sharedTransport.onclose = () => {
190
- // logger.info('🔴 Transport closed for all requests');
191
- // };
192
92
  async function handleSessionRequest(req, res, session) {
193
- var _a, _b, _c, _d;
194
- if (!session) {
195
- utils_2.logger.warn('❌ Session ID not found', {
196
- sessionId: (0, utils_1.getOrCreateSessionId)(req),
197
- userId: (_c = (_b = (_a = req.auth) === null || _a === void 0 ? void 0 : _a.extra) === null || _b === void 0 ? void 0 : _b.user) === null || _c === void 0 ? void 0 : _c.id,
198
- headers: req.headers,
199
- });
200
- res.status(404).json({
201
- jsonrpc: '2.0',
93
+ var _a;
94
+ try {
95
+ const sessionId = (0, utils_1.getOrCreateSessionId)(req);
96
+ if (!session) {
97
+ utils_2.logger.warn('Session not found for request', {
98
+ sessionId,
99
+ });
100
+ res.status(404).json({
101
+ jsonrpc: '2.0',
102
+ error: {
103
+ code: 404,
104
+ message: 'Session not found. Please reinitialize.',
105
+ },
106
+ id: null,
107
+ });
108
+ return;
109
+ }
110
+ // Record tool call for metrics only when actually executing tools
111
+ if (((_a = req.body) === null || _a === void 0 ? void 0 : _a.method) === 'tools/call') {
112
+ session_1.sessionService.recordToolCall(sessionId);
113
+ }
114
+ await session.transport.handleRequest(req, res, req.body);
115
+ if (req.method === 'DELETE') {
116
+ session_1.sessionService.destroySession(sessionId);
117
+ }
118
+ }
119
+ catch (err) {
120
+ const sessionId = (0, utils_1.getOrCreateSessionId)(req);
121
+ // Record error in session metrics
122
+ if (sessionId) {
123
+ session_1.sessionService.recordError(sessionId);
124
+ }
125
+ utils_2.logger.error('❌ Error in handleSessionRequest', {
202
126
  error: {
203
- code: 404,
204
- message: 'Session ID not found. Please reinitialize.',
127
+ message: err instanceof Error ? err.message : String(err),
128
+ stack: err instanceof Error ? err.stack : undefined,
205
129
  },
206
- id: null,
130
+ method: req.method,
131
+ url: req.url,
132
+ sessionId,
207
133
  });
208
- return;
209
- }
210
- session.metrics.totalInteractions++;
211
- if (((_d = req.body) === null || _d === void 0 ? void 0 : _d.method) === 'tools/call') {
212
- session.metrics.totalToolCalls++;
134
+ if (!res.headersSent) {
135
+ res.status(500).json({
136
+ jsonrpc: '2.0',
137
+ error: {
138
+ code: -32603,
139
+ message: 'Internal server error',
140
+ },
141
+ id: null,
142
+ });
143
+ }
213
144
  }
214
- utils_2.logger.info('🔁 Reusing session', session.metrics);
215
- return session.transport.handleRequest(req, res, req.body);
216
145
  }
217
146
  app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
218
- verifier: (0, auth_middleware_1.createOAuthTokenVerifier)(),
147
+ verifier: (0, auth_1.createOAuthTokenVerifier)(),
219
148
  requiredScopes: constants_1.REQUIRED_SCOPES,
220
149
  resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
221
150
  }), _1.addMcpSessionId, async (req, res, next) => {
222
151
  var _a, _b, _c, _d, _e, _f;
223
152
  try {
224
153
  const sessionId = (0, utils_1.getOrCreateSessionId)(req);
225
- const session = sessions.get(sessionId);
154
+ const session = session_1.sessionService.getSession(sessionId);
226
155
  // Reuse existing session
227
156
  if (session) {
228
- // logger.info('🔁 Reusing session', { sessionId });
229
- // await sessions
230
- // .get(sessionId)!
231
- // .transport!.handleRequest(req, res, req.body);
232
- // return;
233
- await handleSessionRequest(req, res, session);
234
- return;
157
+ // Check if session is expired
158
+ if (session_1.sessionService.isSessionExpired(sessionId)) {
159
+ utils_2.logger.warn('Session expired, destroying and creating new one', {
160
+ sessionId,
161
+ });
162
+ session_1.sessionService.destroySession(sessionId);
163
+ }
164
+ else {
165
+ // Record interaction and reuse session
166
+ session_1.sessionService.recordInteraction(sessionId);
167
+ await handleSessionRequest(req, res, session);
168
+ return;
169
+ }
235
170
  }
236
171
  // Create New Session
237
172
  if ((0, types_js_1.isInitializeRequest)(req.body)) {
238
173
  const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
239
174
  sessionIdGenerator: () => (0, utils_1.getOrCreateSessionId)(req),
240
- // sessionIdGenerator: undefined,
241
175
  onsessioninitialized: (sessionId) => {
242
- // const user = req.auth?.extra?.user;
243
176
  var _a, _b, _c;
244
- utils_2.logger.info('🆕 New session initialized', {
177
+ utils_2.logger.info('New session initialized', {
245
178
  sessionId,
246
- requestorHeaders: (0, utils_2.obfuscateAuthHeaders)(req.headers),
247
179
  userId: (_c = (_b = (_a = req.auth) === null || _a === void 0 ? void 0 : _a.extra) === null || _b === void 0 ? void 0 : _b.user) === null || _c === void 0 ? void 0 : _c.id,
248
180
  });
249
- createSession(transport, req, sessionId);
181
+ try {
182
+ session_1.sessionService.createSession(transport, req, sessionId);
183
+ }
184
+ catch (error) {
185
+ utils_2.logger.error('Failed to create session', {
186
+ sessionId,
187
+ error: error instanceof Error ? error.message : String(error),
188
+ });
189
+ // Close transport if session creation fails
190
+ transport.close();
191
+ }
250
192
  },
251
193
  enableJsonResponse: true,
252
194
  });
253
195
  transport.onclose = () => {
254
- utils_2.logger.info('🔴 Transport onclose triggered', {
196
+ utils_2.logger.debug('Transport closed', {
255
197
  sessionId: transport.sessionId,
256
- hasSessionId: !!transport.sessionId,
257
198
  });
258
- if (transport.sessionId)
259
- destroySession(transport.sessionId);
199
+ if (transport.sessionId) {
200
+ session_1.sessionService.destroySession(transport.sessionId);
201
+ }
260
202
  };
261
203
  // Add error handling for the transport
262
204
  transport.onerror = (error) => {
263
- var _a;
264
- utils_2.logger.error('🚨 Transport error occurred', {
205
+ utils_2.logger.error('Transport error', {
265
206
  sessionId: transport.sessionId,
266
- sessionMetrics: (_a = sessions.get(transport.sessionId)) === null || _a === void 0 ? void 0 : _a.metrics,
267
207
  error: {
268
208
  message: error.message,
269
209
  name: error.name,
270
- stack: error.stack,
271
210
  },
272
211
  });
212
+ // Record error in session metrics
213
+ if (transport.sessionId) {
214
+ session_1.sessionService.recordError(transport.sessionId);
215
+ }
273
216
  };
274
217
  await server_1.default.connect(transport);
275
218
  await transport.handleRequest(req, res, req.body);
276
219
  return;
277
- // const session = sessions.get(transport!.sessionId!);
278
- // await handleSessionRequest(req, res, session!);
279
220
  }
280
221
  // No session ID and no initialize request
281
- // Should never happen since we are gonna always have a session ID
282
222
  utils_2.logger.warn('❌ Session ID not found', {
283
223
  sessionId,
284
224
  userId: (_c = (_b = (_a = req.auth) === null || _a === void 0 ? void 0 : _a.extra) === null || _b === void 0 ? void 0 : _b.user) === null || _c === void 0 ? void 0 : _c.id,
@@ -292,9 +232,6 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
292
232
  },
293
233
  id: null,
294
234
  });
295
- return;
296
- // await handleSessionRequest(req, res, sessions.get(sessionId));
297
- // await sharedTransport.handleRequest(req, res, req.body);
298
235
  }
299
236
  catch (error) {
300
237
  utils_2.logger.error('❌ Error in POST handler', {
@@ -312,11 +249,21 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
312
249
  }
313
250
  });
314
251
  // GET/DELETE : server-to-client (SSE) and session termination
252
+ app.get('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
253
+ verifier: (0, auth_1.createOAuthTokenVerifier)(),
254
+ requiredScopes: constants_1.REQUIRED_SCOPES,
255
+ resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
256
+ }), _1.addMcpSessionId, handleSessionRequestGetDelete);
257
+ app.delete('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
258
+ verifier: (0, auth_1.createOAuthTokenVerifier)(),
259
+ requiredScopes: constants_1.REQUIRED_SCOPES,
260
+ resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
261
+ }), _1.addMcpSessionId, handleSessionRequestGetDelete);
315
262
  // GET/DELETE : server-to-client (SSE) and session termination
316
263
  async function handleSessionRequestGetDelete(req, res) {
317
264
  try {
318
265
  const sessionId = (0, utils_1.getOrCreateSessionId)(req);
319
- const session = sessions.get(sessionId);
266
+ const session = session_1.sessionService.getSession(sessionId);
320
267
  if (!session) {
321
268
  utils_2.logger.warn('Invalid or missing session ID', { sessionId });
322
269
  res.status(404).json({
@@ -329,276 +276,94 @@ async function handleSessionRequestGetDelete(req, res) {
329
276
  });
330
277
  return;
331
278
  }
279
+ // Check if session is expired
280
+ if (session_1.sessionService.isSessionExpired(sessionId)) {
281
+ utils_2.logger.warn('Session expired, destroying', { sessionId });
282
+ session_1.sessionService.destroySession(sessionId);
283
+ res.status(404).json({
284
+ jsonrpc: '2.0',
285
+ error: {
286
+ code: 404,
287
+ message: 'Session expired. Please reinitialize.',
288
+ },
289
+ id: null,
290
+ });
291
+ return;
292
+ }
293
+ // Record interaction for metrics
294
+ session_1.sessionService.recordInteraction(sessionId);
332
295
  await session.transport.handleRequest(req, res, req.body);
333
296
  if (req.method === 'DELETE') {
334
- destroySession(sessionId);
297
+ session_1.sessionService.destroySession(sessionId);
335
298
  }
336
299
  }
337
300
  catch (err) {
301
+ const sessionId = (0, utils_1.getOrCreateSessionId)(req);
302
+ // Record error in session metrics
303
+ if (sessionId) {
304
+ session_1.sessionService.recordError(sessionId);
305
+ }
338
306
  utils_2.logger.error('❌ Error in handleSessionRequestGetDelete', {
339
307
  error: {
340
308
  message: err instanceof Error ? err.message : String(err),
341
309
  stack: err instanceof Error ? err.stack : undefined,
342
310
  },
311
+ method: req.method,
312
+ url: req.url,
313
+ sessionId,
343
314
  });
315
+ if (!res.headersSent) {
316
+ res.status(500).json({
317
+ jsonrpc: '2.0',
318
+ error: {
319
+ code: -32603,
320
+ message: 'Internal server error',
321
+ },
322
+ id: null,
323
+ });
324
+ }
344
325
  }
345
326
  }
346
- // GET/DELETE : server-to-client (SSE) and session termination
347
- app.get('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
348
- verifier: (0, auth_middleware_1.createOAuthTokenVerifier)(),
349
- requiredScopes: constants_1.REQUIRED_SCOPES,
350
- resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
351
- }), _1.addMcpSessionId, handleSessionRequestGetDelete);
352
- app.delete('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
353
- verifier: (0, auth_middleware_1.createOAuthTokenVerifier)(),
354
- requiredScopes: constants_1.REQUIRED_SCOPES,
355
- resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
356
- }), _1.addMcpSessionId, handleSessionRequestGetDelete);
357
- // app.get(
358
- // '/',
359
- // authErrorLogger,
360
- // requireBearerAuth({
361
- // verifier: createOAuthTokenVerifier(),
362
- // requiredScopes: REQUIRED_SCOPES,
363
- // resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
364
- // }),
365
- // addMcpSessionId,
366
- // (req: AuthenticatedRequest, res: Response) => {
367
- // return handleSessionRequest(
368
- // req,
369
- // res,
370
- // sessions.get(getOrCreateSessionId(req)),
371
- // );
372
- // },
373
- // );
374
- // app.delete(
375
- // '/',
376
- // authErrorLogger,
377
- // requireBearerAuth({
378
- // verifier: createOAuthTokenVerifier(),
379
- // requiredScopes: REQUIRED_SCOPES,
380
- // resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
381
- // }),
382
- // addMcpSessionId,
383
- // (req: AuthenticatedRequest, res: Response) => {
384
- // destroySession(getOrCreateSessionId(req));
385
- // },
386
- // );
387
- // For stateless servers, GET should return 405 Method Not Allowed
388
- // app.get('/', (req, res) => {
389
- // logger.warn('GET request to (not supported in stateless mode)');
390
- // res.writeHead(405, {
391
- // 'Content-Type': 'application/json',
392
- // Allow: 'POST',
393
- // });
394
- // res.end(
395
- // JSON.stringify({
396
- // jsonrpc: '2.0',
397
- // error: {
398
- // code: -32601, // "Method not found"
399
- // message: 'GET not supported in stateless mode; use POST at ',
400
- // },
401
- // id: null,
402
- // }),
403
- // );
404
- // return;
405
- // });
406
- // For stateless servers, DELETE should return 405 Method Not Allowed
407
- // app.delete('/', (req, res) => {
408
- // logger.warn('DELETE request to (not supported in stateless mode)');
409
- // res.status(405).json({
410
- // jsonrpc: '2.0',
411
- // error: {
412
- // code: -32000,
413
- // message: "Method not allowed. Stateless server doesn't support sessions.",
414
- // },
415
- // id: null,
416
- // });
417
- // });
418
- // POST : client-to-server (with authentication)
419
- // app.post(
420
- // 'x',
421
- // async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
422
- // logger.warn('Verifying access token');
423
- // verifyAccessToken(req, res, next);
424
- // logger.warn('Access token verified');
425
- // try {
426
- // // if (req.headers) {
427
- // // throw new UnauthorizedException();
428
- // // }
429
- // const sessionId = getOrCreateSessionId(req);
430
- // // Reuse existing session
431
- // if (sessionId && sessions.has(sessionId)) {
432
- // logger.info('Reusing session', { sessionId });
433
- // await sessions
434
- // .get(sessionId)!
435
- // .transport!.handleRequest(req, res, req.body);
436
- // return;
437
- // }
438
- // // Create New Session
439
- // if (!sessionId && isInitializeRequest(req.body)) {
440
- // const requestorHeaders = req.headers;
441
- // // TODO: The place I can see info about client is: user-agent
442
- // const transport = new StreamableHTTPServerTransport({
443
- // sessionIdGenerator: () => randomUUID(),
444
- // onsessioninitialized: (transportSessionId: string) => {
445
- // logger.info('New session initialized', {
446
- // sessionId: transportSessionId,
447
- // requestorHeaders,
448
- // userId: req.auth?.extra?.user?.id,
449
- // });
450
- // const sessionId = createSession(transport!, req.auth?.extra?.user);
451
- // if (req.auth?.extra?.user) {
452
- // setUserContext(sessionId, req.auth?.extra?.user);
453
- // }
454
- // },
455
- // enableJsonResponse: true,
456
- // });
457
- // transport.onclose = () => {
458
- // if (transport!.sessionId) destroySession(transport!.sessionId);
459
- // };
460
- // await server.connect(transport);
461
- // await transport.handleRequest(req, res, req.body);
462
- // return;
463
- // }
464
- // // No session ID and no initialize request
465
- // logger.warn('Bad request: No valid session ID provided', {
466
- // sessionId,
467
- // headers: req.headers,
468
- // });
469
- // res.status(404).json({
470
- // jsonrpc: '2.0',
471
- // error: {
472
- // code: 404,
473
- // message:
474
- // 'Session Not Found or Expired. Please reinitialize with a new session ID.',
475
- // },
476
- // id: null,
477
- // });
478
- // } catch (err) {
479
- // logger.error('Error handling request', { error: err });
480
- // return next(err);
481
- // }
482
- // },
483
- // );
484
- // GET/DELETE : server-to-client (SSE) and session termination
485
- // async function handleSessionRequestWasWorking(
486
- // req: AuthenticatedRequest,
487
- // res: Response,
488
- // next: NextFunction,
489
- // ) {
490
- // try {
491
- // // const sessionId = getOrCreateSessionId(req);
492
- // // if (!sessionId || !sessions.has(sessionId)) {
493
- // // logger.warn('Invalid or missing session ID', { sessionId });
494
- // // res.status(404).json({
495
- // // jsonrpc: '2.0',
496
- // // error: {
497
- // // code: 404,
498
- // // message: 'Session not found. Please reinitialize.',
499
- // // },
500
- // // id: null,
501
- // // });
502
- // // return;
503
- // // }
504
- // // Add SSE-specific headers for GET requests (which are used for SSE)
505
- // if (req.method === 'GET') {
506
- // res.setHeader('Content-Type', 'text/event-stream');
507
- // res.setHeader('Cache-Control', 'no-cache');
508
- // res.setHeader('Connection', 'keep-alive');
509
- // res.setHeader('Access-Control-Allow-Origin', '*');
510
- // res.setHeader('Access-Control-Allow-Headers', 'Cache-Control');
511
- // res.setHeader('Access-Control-Allow-Credentials', 'true');
512
- // }
513
- // // const transport = sessions.get(sessionId)!.transport;
514
- // await sharedTransport.handleRequest(req, res, req.body);
515
- // // if (req.method === 'DELETE') {
516
- // // // destroySession(sessionId);
517
- // // }
518
- // } catch (err) {
519
- // logger.error('❌ Error in handleSessionRequest', {
520
- // error: {
521
- // message: err instanceof Error ? err.message : String(err),
522
- // stack: err instanceof Error ? err.stack : undefined,
523
- // },
524
- // method: req.method,
525
- // url: req.url,
526
- // sessionId: getOrCreateSessionId(req),
527
- // });
528
- // next(err);
529
- // }
530
- // }
531
- // app.get('/oauth/authorize', (req, res) => {
532
- // logger.info('OAuth authorize!!!', {
533
- // url: req.url,
534
- // method: req.method,
535
- // headers: req.headers,
536
- // });
537
- // throw new Error('Not implemented');
538
- // });
539
- // Error handler middleware
540
- // app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
541
- // if (err instanceof HttpError) {
542
- // res.status(err.status).json({
543
- // jsonrpc: '2.0',
544
- // error: { code: err.status, message: err.message },
545
- // id: null,
546
- // });
547
- // return;
548
- // }
549
- // logger.error('Unhandled error', {
550
- // error: { ...err, stack: err.stack },
551
- // errorMessage: err.message,
552
- // url: req.url,
553
- // method: req.method,
554
- // });
555
- // if (!res.headersSent) {
556
- // res.status(500).json({
557
- // jsonrpc: '2.0',
558
- // error: { code: -32603, message: 'Internal server error' },
559
- // id: null,
560
- // });
561
- // }
562
- // });
563
327
  // Graceful shutdown
564
328
  function shutdown() {
565
329
  utils_2.logger.info('Shutting down server...');
566
330
  // Clear the heartbeat interval
567
331
  clearInterval(heartbeatInterval);
568
- // Clean up all sessions
569
- for (const sessionId of sessions.keys()) {
570
- destroySession(sessionId);
571
- }
332
+ // Clean up all sessions using the enhanced service
333
+ const totalSessions = session_1.sessionService.getSessionCount();
334
+ utils_2.logger.info('Cleaning up sessions before shutdown', { totalSessions });
335
+ session_1.sessionService.clearAllSessions();
572
336
  process.exit(0);
573
337
  }
574
338
  process.on('SIGINT', shutdown);
575
339
  process.on('SIGTERM', shutdown);
576
340
  process.once('SIGUSR2', () => {
577
341
  utils_2.logger.info('SIGUSR2 received, shutting down...');
578
- // shutdown();
579
- // After cleanup, re-emit the signal to let nodemon restart the process
342
+ shutdown();
580
343
  process.kill(process.pid, 'SIGUSR2');
581
344
  });
582
345
  // Start server
583
346
  const PORT = config_1.env.PORT || 3005;
584
347
  const serverInstance = app.listen(PORT, async () => {
585
- // Connect server once at startup
586
- // await server.connect(sharedTransport);
587
348
  // Initialize Carbon Voice API health status
588
- const isApiWorking = await (0, cv_api_1.isCarbonVoiceApiWorking)();
349
+ const status = await (0, cv_api_1.getCarbonVoiceApiStatus)();
589
350
  carbonVoiceApiHealth = {
590
- isHealthy: isApiWorking,
351
+ ...status,
591
352
  lastChecked: new Date().toISOString(),
592
353
  };
593
- const logLevel = isApiWorking ? 'info' : 'warn';
594
- const icon = isApiWorking ? '✅ ' : '⚠️ ';
595
- utils_2.logger[logLevel](`${icon} MCP HTTP Server started on port ${PORT}`, {
596
- name: constants_2.SERVICE_NAME,
597
- version: constants_2.SERVICE_VERSION,
598
- processId: process.pid,
599
- nodeVersion: process.version,
600
- totalSessions: sessions.size,
354
+ // Initialize and start session cleanup service
355
+ const sessionConfig = session_config_1.SessionConfig.fromEnv();
356
+ const sessionLogger = new session_1.SessionLogger();
357
+ const sessionCleanupService = new session_1.SessionCleanupService(session_1.sessionService, sessionConfig, sessionLogger);
358
+ sessionCleanupService.start();
359
+ utils_2.logger.info('🚀 HTTP MCP Server started', {
360
+ port: PORT,
601
361
  carbonVoiceApiHealth,
362
+ sessionConfig: {
363
+ ttlMs: sessionConfig.ttlMs,
364
+ maxSessions: sessionConfig.maxSessions,
365
+ cleanupIntervalMs: sessionConfig.cleanupIntervalMs,
366
+ },
602
367
  });
603
368
  });
604
369
  serverInstance.on('error', (error) => {
@@ -611,7 +376,7 @@ process.on('uncaughtException', (error) => {
611
376
  stack: error.stack,
612
377
  name: error.name,
613
378
  processId: process.pid,
614
- totalSessions: sessions.size,
379
+ totalSessions: session_1.sessionService.getAllSessionIds().length,
615
380
  });
616
381
  });
617
382
  process.on('unhandledRejection', (reason, promise) => {
@@ -620,42 +385,27 @@ process.on('unhandledRejection', (reason, promise) => {
620
385
  stack: reason instanceof Error ? reason.stack : undefined,
621
386
  promise: promise.toString(),
622
387
  processId: process.pid,
623
- totalSessions: sessions.size,
388
+ totalSessions: session_1.sessionService.getAllSessionIds().length,
624
389
  });
625
390
  });
626
391
  // Log every 30 seconds to show the server is alive
627
392
  const heartbeatInterval = setInterval(async () => {
628
- try {
629
- const isApiWorking = await (0, cv_api_1.isCarbonVoiceApiWorking)();
630
- // Update Carbon Voice API health cache
631
- carbonVoiceApiHealth = {
632
- isHealthy: isApiWorking,
633
- lastChecked: new Date().toISOString(),
634
- ...(carbonVoiceApiHealth.error && isApiWorking && { error: undefined }), // Clear error if now healthy
635
- };
636
- utils_2.logger.info('💓 Server heartbeat', {
637
- totalSessions: sessions.size,
638
- isCarbonVoiceApiWorking: isApiWorking,
639
- uptime: (0, utils_2.formatProcessUptime)(),
640
- memoryUsage: process.memoryUsage(),
641
- });
642
- }
643
- catch (error) {
644
- // Update cache with error information
645
- carbonVoiceApiHealth = {
646
- isHealthy: false,
647
- lastChecked: new Date().toISOString(),
648
- error: error instanceof Error
649
- ? error.message
650
- : 'Unknown error checking API health',
651
- };
652
- utils_2.logger.warn('💓 Server heartbeat - Carbon Voice API check failed', {
653
- totalSessions: sessions.size,
654
- isCarbonVoiceApiWorking: false,
655
- uptime: (0, utils_2.formatProcessUptime)(),
656
- memoryUsage: process.memoryUsage(),
657
- error: carbonVoiceApiHealth.error,
658
- });
659
- }
393
+ const status = await (0, cv_api_1.getCarbonVoiceApiStatus)();
394
+ // Update Carbon Voice API health cache
395
+ carbonVoiceApiHealth = {
396
+ isHealthy: status.isHealthy,
397
+ apiUrl: status.apiUrl,
398
+ lastChecked: new Date().toISOString(),
399
+ ...(status.error && { error: status.error }),
400
+ };
401
+ const icon = carbonVoiceApiHealth.isHealthy ? '🟢' : '🔴';
402
+ const logArgs = {
403
+ totalSessions: session_1.sessionService.getAllSessionIds().length,
404
+ isCarbonVoiceApiWorking: carbonVoiceApiHealth.isHealthy,
405
+ uptime: (0, utils_2.formatProcessUptime)(),
406
+ memoryUsage: process.memoryUsage(),
407
+ carbonVoiceApiHealth,
408
+ };
409
+ utils_2.logger.info(`💓 Server heartbeat ${icon}`, logArgs);
660
410
  }, 30000);
661
411
  //# sourceMappingURL=streamable.js.map