@carbonvoice/cv-mcp-server 1.1.3 → 2.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 (54) 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 +143 -31
  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/middleware/log-request.middlware.js +27 -11
  20. package/dist/transports/http/middleware/log-request.middlware.js.map +1 -1
  21. package/dist/transports/http/session/index.js +34 -0
  22. package/dist/transports/http/session/index.js.map +1 -0
  23. package/dist/transports/http/session/session-cleanup.js +60 -0
  24. package/dist/transports/http/session/session-cleanup.js.map +1 -0
  25. package/dist/transports/http/session/session-manager.js +52 -0
  26. package/dist/transports/http/session/session-manager.js.map +1 -0
  27. package/dist/transports/http/session/session.config.js +29 -0
  28. package/dist/transports/http/session/session.config.js.map +1 -0
  29. package/dist/transports/http/session/session.logger.js +68 -0
  30. package/dist/transports/http/session/session.logger.js.map +1 -0
  31. package/dist/transports/http/session/session.service.js +196 -0
  32. package/dist/transports/http/session/session.service.js.map +1 -0
  33. package/dist/transports/http/session/session.types.js +31 -0
  34. package/dist/transports/http/session/session.types.js.map +1 -0
  35. package/dist/transports/http/streamable.js +184 -427
  36. package/dist/transports/http/streamable.js.map +1 -1
  37. package/dist/transports/http/utils/request-context.js +78 -0
  38. package/dist/transports/http/utils/request-context.js.map +1 -0
  39. package/dist/utils/axios-instance.js +34 -22
  40. package/dist/utils/axios-instance.js.map +1 -1
  41. package/dist/utils/get-process-uptime.js +13 -0
  42. package/dist/utils/get-process-uptime.js.map +1 -0
  43. package/dist/utils/index.js +2 -2
  44. package/dist/utils/index.js.map +1 -1
  45. package/dist/utils/logger.js +34 -11
  46. package/dist/utils/logger.js.map +1 -1
  47. package/dist/utils/{format-time-to-human.js → time-to-human.js} +17 -7
  48. package/dist/utils/time-to-human.js.map +1 -0
  49. package/package.json +1 -1
  50. package/dist/auth/auth-middleware.js +0 -203
  51. package/dist/auth/auth-middleware.js.map +0 -1
  52. package/dist/utils/format-time-to-human.js.map +0 -1
  53. package/dist/utils/format-uptime.js +0 -13
  54. package/dist/utils/format-uptime.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']);
@@ -69,75 +38,18 @@ app.use((0, express_rate_limit_1.default)({
69
38
  app.use(express_1.default.json({ limit: '1mb' }));
70
39
  // Add request ID middleware
71
40
  app.use(_1.addRequestIdMiddleware);
72
- // Request logging middleware
41
+ // Request logging middleware (includes session metrics)
73
42
  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',
139
51
  timestamp: new Date().toISOString(),
140
- uptime: (0, utils_2.formatProcessUptime)(),
52
+ uptime: (0, utils_2.getProcessUptime)(),
141
53
  dependencies: {
142
54
  carbonVoiceApi: {
143
55
  status: carbonVoiceApiHealth.isHealthy ? 'healthy' : 'unhealthy',
@@ -177,111 +89,142 @@ 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
- var _a, _b, _c, _d, _e, _f;
151
+ var _a, _b, _c, _d, _e, _f, _g;
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
+ // Don't record interaction for tool calls as they'll be recorded separately
167
+ if (((_a = req.body) === null || _a === void 0 ? void 0 : _a.method) !== 'tools/call') {
168
+ session_1.sessionService.recordInteraction(sessionId);
169
+ }
170
+ await handleSessionRequest(req, res, session);
171
+ return;
172
+ }
235
173
  }
236
174
  // Create New Session
237
175
  if ((0, types_js_1.isInitializeRequest)(req.body)) {
238
176
  const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
239
177
  sessionIdGenerator: () => (0, utils_1.getOrCreateSessionId)(req),
240
- // sessionIdGenerator: undefined,
241
178
  onsessioninitialized: (sessionId) => {
242
- // const user = req.auth?.extra?.user;
243
179
  var _a, _b, _c;
244
- utils_2.logger.info('🆕 New session initialized', {
180
+ utils_2.logger.info('New session initialized', {
245
181
  sessionId,
246
- requestorHeaders: (0, utils_2.obfuscateAuthHeaders)(req.headers),
247
182
  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
183
  });
249
- createSession(transport, req, sessionId);
184
+ try {
185
+ session_1.sessionService.createSession(transport, req, sessionId);
186
+ }
187
+ catch (error) {
188
+ utils_2.logger.error('Failed to create session', {
189
+ sessionId,
190
+ error: error instanceof Error ? error.message : String(error),
191
+ });
192
+ // Close transport if session creation fails
193
+ transport.close();
194
+ }
250
195
  },
251
196
  enableJsonResponse: true,
252
197
  });
253
198
  transport.onclose = () => {
254
- utils_2.logger.info('🔴 Transport onclose triggered', {
199
+ utils_2.logger.debug('Transport closed', {
255
200
  sessionId: transport.sessionId,
256
- hasSessionId: !!transport.sessionId,
257
201
  });
258
- if (transport.sessionId)
259
- destroySession(transport.sessionId);
202
+ if (transport.sessionId) {
203
+ session_1.sessionService.destroySession(transport.sessionId);
204
+ }
260
205
  };
261
206
  // Add error handling for the transport
262
207
  transport.onerror = (error) => {
263
- var _a;
264
- utils_2.logger.error('🚨 Transport error occurred', {
208
+ utils_2.logger.error('Transport error', {
265
209
  sessionId: transport.sessionId,
266
- sessionMetrics: (_a = sessions.get(transport.sessionId)) === null || _a === void 0 ? void 0 : _a.metrics,
267
210
  error: {
268
211
  message: error.message,
269
212
  name: error.name,
270
- stack: error.stack,
271
213
  },
272
214
  });
215
+ // Record error in session metrics
216
+ if (transport.sessionId) {
217
+ session_1.sessionService.recordError(transport.sessionId);
218
+ }
273
219
  };
274
220
  await server_1.default.connect(transport);
275
221
  await transport.handleRequest(req, res, req.body);
276
222
  return;
277
- // const session = sessions.get(transport!.sessionId!);
278
- // await handleSessionRequest(req, res, session!);
279
223
  }
280
224
  // No session ID and no initialize request
281
- // Should never happen since we are gonna always have a session ID
282
225
  utils_2.logger.warn('❌ Session ID not found', {
283
226
  sessionId,
284
- 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,
227
+ userId: (_d = (_c = (_b = req.auth) === null || _b === void 0 ? void 0 : _b.extra) === null || _c === void 0 ? void 0 : _c.user) === null || _d === void 0 ? void 0 : _d.id,
285
228
  headers: req.headers,
286
229
  });
287
230
  res.status(404).json({
@@ -292,9 +235,6 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
292
235
  },
293
236
  id: null,
294
237
  });
295
- return;
296
- // await handleSessionRequest(req, res, sessions.get(sessionId));
297
- // await sharedTransport.handleRequest(req, res, req.body);
298
238
  }
299
239
  catch (error) {
300
240
  utils_2.logger.error('❌ Error in POST handler', {
@@ -306,17 +246,28 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
306
246
  url: req.url,
307
247
  sessionId: (0, utils_1.getOrCreateSessionId)(req),
308
248
  hasInitializeRequest: (0, types_js_1.isInitializeRequest)(req.body),
309
- userId: (_f = (_e = (_d = req.auth) === null || _d === void 0 ? void 0 : _d.extra) === null || _e === void 0 ? void 0 : _e.user) === null || _f === void 0 ? void 0 : _f.id,
249
+ userId: (_g = (_f = (_e = req.auth) === null || _e === void 0 ? void 0 : _e.extra) === null || _f === void 0 ? void 0 : _f.user) === null || _g === void 0 ? void 0 : _g.id,
310
250
  });
311
251
  next(error);
312
252
  }
313
253
  });
314
254
  // GET/DELETE : server-to-client (SSE) and session termination
255
+ app.get('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
256
+ verifier: (0, auth_1.createOAuthTokenVerifier)(),
257
+ requiredScopes: constants_1.REQUIRED_SCOPES,
258
+ resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
259
+ }), _1.addMcpSessionId, handleSessionRequestGetDelete);
260
+ app.delete('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
261
+ verifier: (0, auth_1.createOAuthTokenVerifier)(),
262
+ requiredScopes: constants_1.REQUIRED_SCOPES,
263
+ resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
264
+ }), _1.addMcpSessionId, handleSessionRequestGetDelete);
315
265
  // GET/DELETE : server-to-client (SSE) and session termination
316
266
  async function handleSessionRequestGetDelete(req, res) {
267
+ var _a;
317
268
  try {
318
269
  const sessionId = (0, utils_1.getOrCreateSessionId)(req);
319
- const session = sessions.get(sessionId);
270
+ const session = session_1.sessionService.getSession(sessionId);
320
271
  if (!session) {
321
272
  utils_2.logger.warn('Invalid or missing session ID', { sessionId });
322
273
  res.status(404).json({
@@ -329,276 +280,97 @@ async function handleSessionRequestGetDelete(req, res) {
329
280
  });
330
281
  return;
331
282
  }
283
+ // Check if session is expired
284
+ if (session_1.sessionService.isSessionExpired(sessionId)) {
285
+ utils_2.logger.warn('Session expired, destroying', { sessionId });
286
+ session_1.sessionService.destroySession(sessionId);
287
+ res.status(404).json({
288
+ jsonrpc: '2.0',
289
+ error: {
290
+ code: 404,
291
+ message: 'Session expired. Please reinitialize.',
292
+ },
293
+ id: null,
294
+ });
295
+ return;
296
+ }
297
+ // Record interaction for metrics
298
+ // Don't record interaction for tool calls as they'll be recorded separately
299
+ if (((_a = req.body) === null || _a === void 0 ? void 0 : _a.method) !== 'tools/call') {
300
+ session_1.sessionService.recordInteraction(sessionId);
301
+ }
332
302
  await session.transport.handleRequest(req, res, req.body);
333
303
  if (req.method === 'DELETE') {
334
- destroySession(sessionId);
304
+ session_1.sessionService.destroySession(sessionId);
335
305
  }
336
306
  }
337
307
  catch (err) {
308
+ const sessionId = (0, utils_1.getOrCreateSessionId)(req);
309
+ // Record error in session metrics
310
+ if (sessionId) {
311
+ session_1.sessionService.recordError(sessionId);
312
+ }
338
313
  utils_2.logger.error('❌ Error in handleSessionRequestGetDelete', {
339
314
  error: {
340
315
  message: err instanceof Error ? err.message : String(err),
341
316
  stack: err instanceof Error ? err.stack : undefined,
342
317
  },
318
+ method: req.method,
319
+ url: req.url,
320
+ sessionId,
343
321
  });
322
+ if (!res.headersSent) {
323
+ res.status(500).json({
324
+ jsonrpc: '2.0',
325
+ error: {
326
+ code: -32603,
327
+ message: 'Internal server error',
328
+ },
329
+ id: null,
330
+ });
331
+ }
344
332
  }
345
333
  }
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
334
  // Graceful shutdown
564
335
  function shutdown() {
565
336
  utils_2.logger.info('Shutting down server...');
566
337
  // Clear the heartbeat interval
567
338
  clearInterval(heartbeatInterval);
568
- // Clean up all sessions
569
- for (const sessionId of sessions.keys()) {
570
- destroySession(sessionId);
571
- }
339
+ // Clean up all sessions using the enhanced service
340
+ const totalSessions = session_1.sessionService.getSessionCount();
341
+ utils_2.logger.info('Cleaning up sessions before shutdown', { totalSessions });
342
+ session_1.sessionService.clearAllSessions();
572
343
  process.exit(0);
573
344
  }
574
345
  process.on('SIGINT', shutdown);
575
346
  process.on('SIGTERM', shutdown);
576
347
  process.once('SIGUSR2', () => {
577
348
  utils_2.logger.info('SIGUSR2 received, shutting down...');
578
- // shutdown();
579
- // After cleanup, re-emit the signal to let nodemon restart the process
349
+ shutdown();
580
350
  process.kill(process.pid, 'SIGUSR2');
581
351
  });
582
352
  // Start server
583
353
  const PORT = config_1.env.PORT || 3005;
584
354
  const serverInstance = app.listen(PORT, async () => {
585
- // Connect server once at startup
586
- // await server.connect(sharedTransport);
587
355
  // Initialize Carbon Voice API health status
588
- const isApiWorking = await (0, cv_api_1.isCarbonVoiceApiWorking)();
356
+ const status = await (0, cv_api_1.getCarbonVoiceApiStatus)();
589
357
  carbonVoiceApiHealth = {
590
- isHealthy: isApiWorking,
358
+ ...status,
591
359
  lastChecked: new Date().toISOString(),
592
360
  };
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,
361
+ // Initialize and start session cleanup service
362
+ const sessionConfig = session_config_1.SessionConfig.fromEnv();
363
+ const sessionLogger = new session_1.SessionLogger();
364
+ const sessionCleanupService = new session_1.SessionCleanupService(session_1.sessionService, sessionConfig, sessionLogger);
365
+ sessionCleanupService.start();
366
+ utils_2.logger.info('🚀 HTTP MCP Server started', {
367
+ port: PORT,
601
368
  carbonVoiceApiHealth,
369
+ sessionConfig: {
370
+ ttlMs: sessionConfig.ttlMs,
371
+ maxSessions: sessionConfig.maxSessions,
372
+ cleanupIntervalMs: sessionConfig.cleanupIntervalMs,
373
+ },
602
374
  });
603
375
  });
604
376
  serverInstance.on('error', (error) => {
@@ -611,7 +383,7 @@ process.on('uncaughtException', (error) => {
611
383
  stack: error.stack,
612
384
  name: error.name,
613
385
  processId: process.pid,
614
- totalSessions: sessions.size,
386
+ totalSessions: session_1.sessionService.getAllSessionIds().length,
615
387
  });
616
388
  });
617
389
  process.on('unhandledRejection', (reason, promise) => {
@@ -620,42 +392,27 @@ process.on('unhandledRejection', (reason, promise) => {
620
392
  stack: reason instanceof Error ? reason.stack : undefined,
621
393
  promise: promise.toString(),
622
394
  processId: process.pid,
623
- totalSessions: sessions.size,
395
+ totalSessions: session_1.sessionService.getAllSessionIds().length,
624
396
  });
625
397
  });
626
398
  // Log every 30 seconds to show the server is alive
627
399
  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
- }
400
+ const status = await (0, cv_api_1.getCarbonVoiceApiStatus)();
401
+ // Update Carbon Voice API health cache
402
+ carbonVoiceApiHealth = {
403
+ isHealthy: status.isHealthy,
404
+ apiUrl: status.apiUrl,
405
+ lastChecked: new Date().toISOString(),
406
+ ...(status.error && { error: status.error }),
407
+ };
408
+ const icon = carbonVoiceApiHealth.isHealthy ? '🟢' : '🔴';
409
+ const logArgs = {
410
+ totalSessions: session_1.sessionService.getAllSessionIds().length,
411
+ isCarbonVoiceApiWorking: carbonVoiceApiHealth.isHealthy,
412
+ uptime: (0, utils_2.getProcessUptime)(),
413
+ memoryUsage: process.memoryUsage(),
414
+ carbonVoiceApiHealth,
415
+ };
416
+ utils_2.logger.info(`💓 Server heartbeat ${icon}`, logArgs);
660
417
  }, 30000);
661
418
  //# sourceMappingURL=streamable.js.map