@carbonvoice/cv-mcp-server 2.8.0 → 2.8.14

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 (48) hide show
  1. package/dist/config/env.js +152 -2
  2. package/dist/config/env.js.map +1 -1
  3. package/dist/cv-api.js +15 -0
  4. package/dist/cv-api.js.map +1 -1
  5. package/dist/interfaces/index.js +1 -0
  6. package/dist/interfaces/index.js.map +1 -1
  7. package/dist/interfaces/user-info.interface.js +3 -0
  8. package/dist/interfaces/user-info.interface.js.map +1 -0
  9. package/dist/server.js +582 -554
  10. package/dist/server.js.map +1 -1
  11. package/dist/transports/http/diagnostics/transport-send-diagnostics.js +115 -0
  12. package/dist/transports/http/diagnostics/transport-send-diagnostics.js.map +1 -0
  13. package/dist/transports/http/middleware/add-request-id.middlware.js +2 -2
  14. package/dist/transports/http/middleware/add-request-id.middlware.js.map +1 -1
  15. package/dist/transports/http/middleware/index.js +1 -0
  16. package/dist/transports/http/middleware/index.js.map +1 -1
  17. package/dist/transports/http/middleware/log-request.middlware.js +62 -11
  18. package/dist/transports/http/middleware/log-request.middlware.js.map +1 -1
  19. package/dist/transports/http/middleware/require-bearer-auth-with-absolute-metadata.middleware.js +72 -0
  20. package/dist/transports/http/middleware/require-bearer-auth-with-absolute-metadata.middleware.js.map +1 -0
  21. package/dist/transports/http/session/index.js +4 -1
  22. package/dist/transports/http/session/index.js.map +1 -1
  23. package/dist/transports/http/session/session.config.js +10 -5
  24. package/dist/transports/http/session/session.config.js.map +1 -1
  25. package/dist/transports/http/session/session.logger.js +22 -0
  26. package/dist/transports/http/session/session.logger.js.map +1 -1
  27. package/dist/transports/http/session/session.service.js +55 -22
  28. package/dist/transports/http/session/session.service.js.map +1 -1
  29. package/dist/transports/http/session/session.types.js.map +1 -1
  30. package/dist/transports/http/session/tool-call-queue.service.js +73 -0
  31. package/dist/transports/http/session/tool-call-queue.service.js.map +1 -0
  32. package/dist/transports/http/streamable-stateless.js +288 -0
  33. package/dist/transports/http/streamable-stateless.js.map +1 -0
  34. package/dist/transports/http/streamable.js +379 -37
  35. package/dist/transports/http/streamable.js.map +1 -1
  36. package/dist/transports/http/utils/request-context.js +3 -2
  37. package/dist/transports/http/utils/request-context.js.map +1 -1
  38. package/dist/utils/axios-instance.js +12 -0
  39. package/dist/utils/axios-instance.js.map +1 -1
  40. package/dist/utils/format-bytes-human.js +18 -0
  41. package/dist/utils/format-bytes-human.js.map +1 -0
  42. package/dist/utils/format-to-mcp-tool-response.js +46 -10
  43. package/dist/utils/format-to-mcp-tool-response.js.map +1 -1
  44. package/dist/utils/index.js +1 -0
  45. package/dist/utils/index.js.map +1 -1
  46. package/dist/utils/logger.js +6 -19
  47. package/dist/utils/logger.js.map +1 -1
  48. package/package.json +11 -5
@@ -10,20 +10,25 @@ const cors_1 = __importDefault(require("cors"));
10
10
  const express_1 = __importDefault(require("express"));
11
11
  const helmet_1 = __importDefault(require("helmet"));
12
12
  const serve_favicon_1 = __importDefault(require("serve-favicon"));
13
- const bearerAuth_js_1 = require("@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js");
14
13
  const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
15
14
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
16
- const _1 = require(".");
17
15
  const constants_1 = require("./constants");
16
+ const transport_send_diagnostics_1 = require("./diagnostics/transport-send-diagnostics");
17
+ const middleware_1 = require("./middleware");
18
18
  const session_1 = require("./session");
19
19
  const session_config_1 = require("./session/session.config");
20
20
  const utils_1 = require("./utils");
21
+ const request_context_1 = require("./utils/request-context");
21
22
  const auth_1 = require("../../auth");
22
23
  const config_1 = require("../../config");
23
24
  const cv_api_1 = require("../../cv-api");
24
25
  const server_1 = __importDefault(require("../../server"));
25
26
  const utils_2 = require("../../utils");
26
27
  const app = (0, express_1.default)();
28
+ // TODO: REMOVE THIS AFTER DEBUGGING
29
+ const transportDiagnostics = (0, transport_send_diagnostics_1.createTransportSendDiagnostics)({
30
+ enabled: config_1.env.MCP_TRANSPORT_DIAGNOSTICS_ENABLED,
31
+ });
27
32
  app.set('x-powered-by', false);
28
33
  // Trust proxy for rate limiting - only trust localhost and private networks
29
34
  app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
@@ -36,17 +41,24 @@ if (fs_1.default.existsSync(faviconPath)) {
36
41
  app.use((0, cors_1.default)());
37
42
  // Security middlewares
38
43
  app.use((0, helmet_1.default)());
39
- app.use(_1.rateLimitMiddleware);
44
+ app.use(middleware_1.rateLimitMiddleware);
40
45
  app.use(express_1.default.json({ limit: '1mb' }));
41
46
  // Add request ID middleware
42
- app.use(_1.addRequestIdMiddleware);
47
+ app.use(middleware_1.addRequestIdMiddleware);
43
48
  // Request logging middleware (includes session metrics)
44
- app.use(_1.logRequest);
49
+ app.use(middleware_1.logRequest);
45
50
  let carbonVoiceApiHealth = {
46
51
  isHealthy: (0, config_1.isTestEnvironment)() ? true : false, // Assume healthy in test mode
47
52
  lastChecked: new Date().toISOString(),
48
53
  apiUrl: config_1.env.CARBON_VOICE_BASE_URL,
49
54
  };
55
+ const resourceMetadataUrl = config_1.env.MCP_RESOURCE_METADATA_URL;
56
+ const oauthTokenVerifier = (0, auth_1.createOAuthTokenVerifier)();
57
+ const authWithAbsoluteMetadata = (0, middleware_1.requireBearerAuthWithAbsoluteMetadata)({
58
+ verifier: oauthTokenVerifier,
59
+ requiredScopes: constants_1.REQUIRED_SCOPES,
60
+ resourceMetadataUrl,
61
+ });
50
62
  app.get('/health', (req, res) => {
51
63
  const response = {
52
64
  status: carbonVoiceApiHealth.isHealthy ? 'healthy' : 'degraded',
@@ -80,8 +92,8 @@ app.get('/info', (req, res) => {
80
92
  res.status(200).send('OK');
81
93
  });
82
94
  // Protected resource metadata (OAuth 2.1 RFC9728 compliant)
83
- app.get('/.well-known/oauth-protected-resource', _1.wellKnownCorsHeaders, _1.oauthProtectedResource);
84
- app.get('/.well-known/oauth-authorization-server', _1.wellKnownCorsHeaders, _1.oauthAuthorizationServer);
95
+ app.get('/.well-known/oauth-protected-resource', middleware_1.wellKnownCorsHeaders, middleware_1.oauthProtectedResource);
96
+ app.get('/.well-known/oauth-authorization-server', middleware_1.wellKnownCorsHeaders, middleware_1.oauthAuthorizationServer);
85
97
  // Handle HEAD requests without auth
86
98
  app.head('/', (req, res) => {
87
99
  const mcpProtocolVersion = req.headers['mcp-protocol-version'] || types_js_1.LATEST_PROTOCOL_VERSION;
@@ -92,9 +104,13 @@ app.head('/', (req, res) => {
92
104
  .end();
93
105
  });
94
106
  async function handleSessionRequest(req, res, session) {
95
- var _a;
107
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
96
108
  try {
97
109
  const sessionId = (0, utils_1.getOrCreateSessionId)(req);
110
+ (0, request_context_1.updateRequestContext)({
111
+ sessionId: sessionId || undefined,
112
+ 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,
113
+ });
98
114
  if (!session) {
99
115
  utils_2.logger.warn('Session not found for request', {
100
116
  sessionId,
@@ -110,11 +126,40 @@ async function handleSessionRequest(req, res, session) {
110
126
  return;
111
127
  }
112
128
  // Record tool call for metrics only when actually executing tools
113
- if (((_a = req.body) === null || _a === void 0 ? void 0 : _a.method) === 'tools/call') {
129
+ if (((_d = req.body) === null || _d === void 0 ? void 0 : _d.method) === 'tools/call') {
114
130
  session_1.sessionService.recordToolCall(sessionId);
115
131
  }
132
+ const isToolCall = ((_e = req.body) === null || _e === void 0 ? void 0 : _e.method) === 'tools/call';
133
+ const toolCallStart = isToolCall ? Date.now() : undefined;
134
+ if (isToolCall) {
135
+ transportDiagnostics.trackToolCall(sessionId, (_f = req.body) === null || _f === void 0 ? void 0 : _f.id, {
136
+ toolName: (_h = (_g = req.body) === null || _g === void 0 ? void 0 : _g.params) === null || _h === void 0 ? void 0 : _h.name,
137
+ traceId: (0, request_context_1.getTraceId)(),
138
+ userId: (_l = (_k = (_j = req.auth) === null || _j === void 0 ? void 0 : _j.extra) === null || _k === void 0 ? void 0 : _k.user) === null || _l === void 0 ? void 0 : _l.id,
139
+ });
140
+ utils_2.logger.info('TOOL_CALL_TRANSPORT_START', {
141
+ event: 'TOOL_CALL_TRANSPORT_START',
142
+ toolName: (_o = (_m = req.body) === null || _m === void 0 ? void 0 : _m.params) === null || _o === void 0 ? void 0 : _o.name,
143
+ jsonRpcId: (_p = req.body) === null || _p === void 0 ? void 0 : _p.id,
144
+ sessionId,
145
+ userId: (_s = (_r = (_q = req.auth) === null || _q === void 0 ? void 0 : _q.extra) === null || _r === void 0 ? void 0 : _r.user) === null || _s === void 0 ? void 0 : _s.id,
146
+ traceId: (0, request_context_1.getTraceId)(),
147
+ });
148
+ }
116
149
  await session.transport.handleRequest(req, res, req.body);
150
+ if (isToolCall && toolCallStart !== undefined) {
151
+ utils_2.logger.info('TOOL_CALL_TRANSPORT_AWAIT_RESOLVED', {
152
+ event: 'TOOL_CALL_TRANSPORT_AWAIT_RESOLVED',
153
+ toolName: (_u = (_t = req.body) === null || _t === void 0 ? void 0 : _t.params) === null || _u === void 0 ? void 0 : _u.name,
154
+ jsonRpcId: (_v = req.body) === null || _v === void 0 ? void 0 : _v.id,
155
+ awaitDurationMs: Date.now() - toolCallStart,
156
+ sessionId,
157
+ userId: (_y = (_x = (_w = req.auth) === null || _w === void 0 ? void 0 : _w.extra) === null || _x === void 0 ? void 0 : _x.user) === null || _y === void 0 ? void 0 : _y.id,
158
+ traceId: (0, request_context_1.getTraceId)(),
159
+ });
160
+ }
117
161
  if (req.method === 'DELETE') {
162
+ session_1.toolCallQueueService.clearSession(sessionId);
118
163
  session_1.sessionService.destroySession(sessionId);
119
164
  }
120
165
  }
@@ -145,12 +190,154 @@ async function handleSessionRequest(req, res, session) {
145
190
  }
146
191
  }
147
192
  }
148
- app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
149
- verifier: (0, auth_1.createOAuthTokenVerifier)(),
150
- requiredScopes: constants_1.REQUIRED_SCOPES,
151
- resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
152
- }), _1.addMcpSessionId, async (req, res, next) => {
153
- var _a, _b, _c, _d, _e, _f, _g;
193
+ /**
194
+ * TEMPORARY INCIDENT MITIGATION (Phase 3 guardrail)
195
+ *
196
+ * Why this is in streamable.ts for now:
197
+ * - This logic depends on request/response objects plus session/transport cleanup
198
+ * that currently live in this file.
199
+ * - We are keeping the hotfix close to the route boundary while validating in prod.
200
+ *
201
+ * Planned follow-up:
202
+ * - Extract this block into a dedicated transport orchestration module once production
203
+ * evidence confirms the incident is resolved.
204
+ * - Keep behavior identical during extraction (no functional changes).
205
+ */
206
+ const hasJsonRpcRequestId = (body) => {
207
+ if (!body || typeof body !== 'object') {
208
+ return false;
209
+ }
210
+ const candidate = body;
211
+ return candidate.id !== undefined && candidate.id !== null;
212
+ };
213
+ const getJsonRpcMethod = (body) => {
214
+ if (!body || typeof body !== 'object') {
215
+ return undefined;
216
+ }
217
+ const candidate = body;
218
+ return typeof candidate.method === 'string' ? candidate.method : undefined;
219
+ };
220
+ const destroySessionTransportState = (sessionId) => {
221
+ transportDiagnostics.clearSession(sessionId);
222
+ session_1.toolCallQueueService.clearSession(sessionId);
223
+ session_1.sessionService.destroySession(sessionId);
224
+ };
225
+ /**
226
+ * TEMPORARY SAFETY TIMEOUT FOR REUSED SESSION TRANSPORT CALLS.
227
+ *
228
+ * Removes indefinite hangs when transport.handleRequest() never resolves under
229
+ * high concurrency/race conditions. On timeout we fail deterministically and
230
+ * clear session-bound transport state so the client can reinitialize.
231
+ *
232
+ * Remove after:
233
+ * - Root cause is permanently fixed in transport/session lifecycle; and
234
+ * - production can run without timeout-driven cleanup for a sustained window.
235
+ */
236
+ const executeSessionRequestWithTimeout = async (req, res, session, sessionId) => {
237
+ var _a, _b, _c, _d, _e, _f;
238
+ const executionTimeoutMs = config_1.env.MCP_TRANSPORT_EXECUTION_TIMEOUT_MS;
239
+ const jsonRpcMethod = getJsonRpcMethod(req.body);
240
+ const jsonRpcId = hasJsonRpcRequestId(req.body)
241
+ ? req.body.id
242
+ : undefined;
243
+ const executionStartedAt = Date.now();
244
+ const handleRequestPromise = handleSessionRequest(req, res, session);
245
+ let timeoutHandle;
246
+ const timeoutPromise = new Promise((resolve) => {
247
+ timeoutHandle = setTimeout(() => resolve('timeout'), executionTimeoutMs);
248
+ });
249
+ // Detect client-side disconnect (e.g. the MCP client's ~120s HTTP timeout
250
+ // fires before the server can respond). This lets us clean up immediately
251
+ // instead of waiting for the server-side timeout and the subsequent
252
+ // MCP_TRANSPORT_SEND_ERROR cascade.
253
+ let removeDisconnectListener;
254
+ const clientDisconnectedPromise = new Promise((resolve) => {
255
+ const onClose = () => resolve('client_disconnected');
256
+ req.on('close', onClose);
257
+ removeDisconnectListener = () => req.off('close', onClose);
258
+ });
259
+ const outcome = await Promise.race([
260
+ handleRequestPromise.then(() => 'completed'),
261
+ timeoutPromise,
262
+ clientDisconnectedPromise,
263
+ ]);
264
+ if (timeoutHandle) {
265
+ clearTimeout(timeoutHandle);
266
+ }
267
+ removeDisconnectListener === null || removeDisconnectListener === void 0 ? void 0 : removeDisconnectListener();
268
+ if (outcome === 'completed') {
269
+ return;
270
+ }
271
+ if (outcome === 'client_disconnected') {
272
+ utils_2.logger.warn('MCP_CLIENT_DISCONNECTED', {
273
+ event: 'MCP_CLIENT_DISCONNECTED',
274
+ sessionId,
275
+ jsonRpcId,
276
+ jsonRpcMethod,
277
+ elapsedMs: Date.now() - executionStartedAt,
278
+ traceId: (0, request_context_1.getTraceId)(),
279
+ 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,
280
+ });
281
+ session_1.sessionService.recordError(sessionId);
282
+ destroySessionTransportState(sessionId);
283
+ // Connection is already gone — no response can be sent.
284
+ void handleRequestPromise.catch((error) => {
285
+ var _a, _b, _c;
286
+ utils_2.logger.warn('MCP_CLIENT_DISCONNECTED_LATE_REJECTION', {
287
+ event: 'MCP_CLIENT_DISCONNECTED_LATE_REJECTION',
288
+ sessionId,
289
+ jsonRpcId,
290
+ jsonRpcMethod,
291
+ traceId: (0, request_context_1.getTraceId)(),
292
+ 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,
293
+ error: error instanceof Error ? error.message : String(error),
294
+ });
295
+ });
296
+ return;
297
+ }
298
+ utils_2.logger.warn('MCP_TRANSPORT_EXECUTION_TIMEOUT', {
299
+ event: 'MCP_TRANSPORT_EXECUTION_TIMEOUT',
300
+ sessionId,
301
+ jsonRpcId,
302
+ jsonRpcMethod,
303
+ executionTimeoutMs,
304
+ elapsedMs: Date.now() - executionStartedAt,
305
+ traceId: (0, request_context_1.getTraceId)(),
306
+ 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,
307
+ });
308
+ session_1.sessionService.recordError(sessionId);
309
+ destroySessionTransportState(sessionId);
310
+ if (!res.headersSent) {
311
+ if (jsonRpcId !== undefined) {
312
+ res.status(200).json({
313
+ jsonrpc: '2.0',
314
+ error: {
315
+ code: -32001,
316
+ message: 'Request timed out while processing previous session transport work. Please retry.',
317
+ },
318
+ id: jsonRpcId,
319
+ });
320
+ }
321
+ else {
322
+ // Notifications are best-effort and do not require a JSON-RPC response.
323
+ res.status(202).end();
324
+ }
325
+ }
326
+ void handleRequestPromise.catch((error) => {
327
+ var _a, _b, _c;
328
+ utils_2.logger.warn('MCP_TRANSPORT_EXECUTION_LATE_REJECTION', {
329
+ event: 'MCP_TRANSPORT_EXECUTION_LATE_REJECTION',
330
+ sessionId,
331
+ jsonRpcId,
332
+ jsonRpcMethod,
333
+ traceId: (0, request_context_1.getTraceId)(),
334
+ 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,
335
+ error: error instanceof Error ? error.message : String(error),
336
+ });
337
+ });
338
+ };
339
+ app.post('/', middleware_1.authErrorLogger, authWithAbsoluteMetadata, middleware_1.addMcpSessionId, async (req, res, next) => {
340
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
154
341
  try {
155
342
  const sessionId = (0, utils_1.getOrCreateSessionId)(req);
156
343
  const session = session_1.sessionService.getSession(sessionId);
@@ -169,7 +356,136 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
169
356
  if (((_a = req.body) === null || _a === void 0 ? void 0 : _a.method) !== 'tools/call') {
170
357
  session_1.sessionService.recordInteraction(sessionId);
171
358
  }
172
- await handleSessionRequest(req, res, session);
359
+ transportDiagnostics.attach(session.transport);
360
+ const shouldSerializeSessionRequest = req.method === 'POST' &&
361
+ !(0, types_js_1.isInitializeRequest)(req.body);
362
+ if (shouldSerializeSessionRequest) {
363
+ const queueTimeoutMs = config_1.env.MCP_SESSION_REQUEST_QUEUE_TIMEOUT_MS;
364
+ const jsonRpcMethod = getJsonRpcMethod(req.body);
365
+ const jsonRpcId = hasJsonRpcRequestId(req.body)
366
+ ? req.body.id
367
+ : undefined;
368
+ let releaseToolCallSlot;
369
+ try {
370
+ const { release, waitDurationMs } = await session_1.toolCallQueueService.acquire(sessionId, queueTimeoutMs);
371
+ releaseToolCallSlot = release;
372
+ utils_2.logger.info('SESSION_REQUEST_QUEUE_ACQUIRED', {
373
+ event: 'SESSION_REQUEST_QUEUE_ACQUIRED',
374
+ sessionId,
375
+ jsonRpcId,
376
+ jsonRpcMethod,
377
+ toolName: (_c = (_b = req.body) === null || _b === void 0 ? void 0 : _b.params) === null || _c === void 0 ? void 0 : _c.name,
378
+ waitDurationMs,
379
+ queueTimeoutMs,
380
+ traceId: (0, request_context_1.getTraceId)(),
381
+ 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,
382
+ });
383
+ if (jsonRpcMethod === 'tools/call') {
384
+ // Backward compatible event kept for existing dashboards.
385
+ utils_2.logger.info('TOOL_CALL_QUEUE_ACQUIRED', {
386
+ event: 'TOOL_CALL_QUEUE_ACQUIRED',
387
+ sessionId,
388
+ jsonRpcId,
389
+ toolName: (_h = (_g = req.body) === null || _g === void 0 ? void 0 : _g.params) === null || _h === void 0 ? void 0 : _h.name,
390
+ waitDurationMs,
391
+ queueTimeoutMs,
392
+ traceId: (0, request_context_1.getTraceId)(),
393
+ userId: (_l = (_k = (_j = req.auth) === null || _j === void 0 ? void 0 : _j.extra) === null || _k === void 0 ? void 0 : _k.user) === null || _l === void 0 ? void 0 : _l.id,
394
+ });
395
+ }
396
+ // Re-fetch the session after waiting in queue: the previous
397
+ // request may have timed out and destroyed the session while
398
+ // this request was waiting for its slot.
399
+ const freshSession = session_1.sessionService.getSession(sessionId);
400
+ if (!freshSession) {
401
+ const jsonRpcId2 = hasJsonRpcRequestId(req.body)
402
+ ? req.body.id
403
+ : undefined;
404
+ utils_2.logger.warn('SESSION_DESTROYED_WHILE_QUEUED', {
405
+ event: 'SESSION_DESTROYED_WHILE_QUEUED',
406
+ sessionId,
407
+ jsonRpcId: jsonRpcId2,
408
+ jsonRpcMethod,
409
+ traceId: (0, request_context_1.getTraceId)(),
410
+ userId: (_p = (_o = (_m = req.auth) === null || _m === void 0 ? void 0 : _m.extra) === null || _o === void 0 ? void 0 : _o.user) === null || _p === void 0 ? void 0 : _p.id,
411
+ });
412
+ if (!res.headersSent) {
413
+ if (jsonRpcId2 !== undefined) {
414
+ res.status(200).json({
415
+ jsonrpc: '2.0',
416
+ error: {
417
+ code: -32001,
418
+ message: 'Session was destroyed while request was queued. Please reinitialize.',
419
+ },
420
+ id: jsonRpcId2,
421
+ });
422
+ }
423
+ else {
424
+ res.status(404).json({
425
+ jsonrpc: '2.0',
426
+ error: {
427
+ code: 404,
428
+ message: 'Session not found. Please reinitialize.',
429
+ },
430
+ id: null,
431
+ });
432
+ }
433
+ }
434
+ return;
435
+ }
436
+ await executeSessionRequestWithTimeout(req, res, freshSession, sessionId);
437
+ }
438
+ catch (error) {
439
+ if (error instanceof session_1.ToolCallQueueTimeoutError) {
440
+ utils_2.logger.warn('SESSION_REQUEST_QUEUE_TIMEOUT', {
441
+ event: 'SESSION_REQUEST_QUEUE_TIMEOUT',
442
+ sessionId,
443
+ jsonRpcId,
444
+ jsonRpcMethod,
445
+ toolName: (_r = (_q = req.body) === null || _q === void 0 ? void 0 : _q.params) === null || _r === void 0 ? void 0 : _r.name,
446
+ queueTimeoutMs,
447
+ traceId: (0, request_context_1.getTraceId)(),
448
+ userId: (_u = (_t = (_s = req.auth) === null || _s === void 0 ? void 0 : _s.extra) === null || _t === void 0 ? void 0 : _t.user) === null || _u === void 0 ? void 0 : _u.id,
449
+ });
450
+ if (jsonRpcMethod === 'tools/call') {
451
+ // Backward compatible event kept for existing dashboards.
452
+ utils_2.logger.warn('TOOL_CALL_QUEUE_TIMEOUT', {
453
+ event: 'TOOL_CALL_QUEUE_TIMEOUT',
454
+ sessionId,
455
+ jsonRpcId,
456
+ toolName: (_w = (_v = req.body) === null || _v === void 0 ? void 0 : _v.params) === null || _w === void 0 ? void 0 : _w.name,
457
+ queueTimeoutMs,
458
+ traceId: (0, request_context_1.getTraceId)(),
459
+ userId: (_z = (_y = (_x = req.auth) === null || _x === void 0 ? void 0 : _x.extra) === null || _y === void 0 ? void 0 : _y.user) === null || _z === void 0 ? void 0 : _z.id,
460
+ });
461
+ }
462
+ if (!res.headersSent) {
463
+ if (jsonRpcId !== undefined) {
464
+ res.status(200).json({
465
+ jsonrpc: '2.0',
466
+ error: {
467
+ code: -32001,
468
+ message: 'Request timed out while waiting for previous request in this session.',
469
+ },
470
+ id: jsonRpcId,
471
+ });
472
+ }
473
+ else {
474
+ res.status(202).end();
475
+ }
476
+ }
477
+ }
478
+ else {
479
+ throw error;
480
+ }
481
+ }
482
+ finally {
483
+ releaseToolCallSlot === null || releaseToolCallSlot === void 0 ? void 0 : releaseToolCallSlot();
484
+ }
485
+ }
486
+ else {
487
+ await handleSessionRequest(req, res, session);
488
+ }
173
489
  return;
174
490
  }
175
491
  }
@@ -193,14 +509,21 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
193
509
  enableJsonResponse: true,
194
510
  });
195
511
  transport.onclose = () => {
196
- utils_2.logger.debug('🔌 Transport closed');
512
+ utils_2.logger.debug('🔌 Transport closed', {
513
+ event: 'MCP_TRANSPORT_CLOSED',
514
+ sessionId: transport.sessionId,
515
+ });
197
516
  if (transport.sessionId) {
517
+ transportDiagnostics.clearSession(transport.sessionId);
518
+ session_1.toolCallQueueService.clearSession(transport.sessionId);
198
519
  session_1.sessionService.destroySession(transport.sessionId);
199
520
  }
200
521
  };
201
522
  // Add error handling for the transport
202
523
  transport.onerror = (error) => {
203
524
  utils_2.logger.error('🔴 Transport error', {
525
+ event: 'MCP_TRANSPORT_ERROR',
526
+ sessionId: transport.sessionId,
204
527
  error: {
205
528
  message: error.message,
206
529
  name: error.name,
@@ -211,6 +534,7 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
211
534
  session_1.sessionService.recordError(transport.sessionId);
212
535
  }
213
536
  };
537
+ transportDiagnostics.attach(transport);
214
538
  await server_1.default.connect(transport);
215
539
  await transport.handleRequest(req, res, req.body);
216
540
  return;
@@ -218,7 +542,7 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
218
542
  // No session ID and no initialize request
219
543
  utils_2.logger.warn('❌ Session ID not found', {
220
544
  sessionId,
221
- 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,
545
+ userId: (_2 = (_1 = (_0 = req.auth) === null || _0 === void 0 ? void 0 : _0.extra) === null || _1 === void 0 ? void 0 : _1.user) === null || _2 === void 0 ? void 0 : _2.id,
222
546
  headers: req.headers,
223
547
  });
224
548
  res.status(404).json({
@@ -240,30 +564,35 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
240
564
  url: req.url,
241
565
  sessionId: (0, utils_1.getOrCreateSessionId)(req),
242
566
  hasInitializeRequest: (0, types_js_1.isInitializeRequest)(req.body),
243
- 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,
567
+ userId: (_5 = (_4 = (_3 = req.auth) === null || _3 === void 0 ? void 0 : _3.extra) === null || _4 === void 0 ? void 0 : _4.user) === null || _5 === void 0 ? void 0 : _5.id,
244
568
  });
245
569
  next(error);
246
570
  }
247
571
  });
248
572
  // GET/DELETE : server-to-client (SSE) and session termination
249
- app.get('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
250
- verifier: (0, auth_1.createOAuthTokenVerifier)(),
251
- requiredScopes: constants_1.REQUIRED_SCOPES,
252
- resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
253
- }), _1.addMcpSessionId, handleSessionRequestGetDelete);
254
- app.delete('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
255
- verifier: (0, auth_1.createOAuthTokenVerifier)(),
256
- requiredScopes: constants_1.REQUIRED_SCOPES,
257
- resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
258
- }), _1.addMcpSessionId, handleSessionRequestGetDelete);
573
+ app.get('/', middleware_1.authErrorLogger, authWithAbsoluteMetadata, middleware_1.addMcpSessionId, handleSessionRequestGetDelete);
574
+ app.delete('/', middleware_1.authErrorLogger, authWithAbsoluteMetadata, middleware_1.addMcpSessionId, handleSessionRequestGetDelete);
259
575
  // GET/DELETE : server-to-client (SSE) and session termination
260
576
  async function handleSessionRequestGetDelete(req, res) {
261
- var _a;
577
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
262
578
  try {
263
579
  const sessionId = (0, utils_1.getOrCreateSessionId)(req);
264
580
  const session = session_1.sessionService.getSession(sessionId);
265
581
  if (!session) {
266
- utils_2.logger.warn('Invalid or missing session ID', { sessionId });
582
+ const isSseGet = req.method === 'GET';
583
+ utils_2.logger.warn(isSseGet
584
+ ? 'MCP SSE GET: session not in store (404 — client should reinitialize)'
585
+ : 'Session not in store (404)', {
586
+ event: 'MCP_SSE_SESSION_GONE',
587
+ reason: 'not_in_store',
588
+ httpMethod: req.method,
589
+ sessionId,
590
+ 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,
591
+ traceId: (0, request_context_1.getTraceId)(),
592
+ hint: isSseGet
593
+ ? 'Idle TTL elapsed, max wall-clock age reached, cleanup, or never initialized.'
594
+ : undefined,
595
+ });
267
596
  res.status(404).json({
268
597
  jsonrpc: '2.0',
269
598
  error: {
@@ -276,7 +605,14 @@ async function handleSessionRequestGetDelete(req, res) {
276
605
  }
277
606
  // Check if session is expired
278
607
  if (session_1.sessionService.isSessionExpired(sessionId)) {
279
- utils_2.logger.warn('Session expired, destroying', { sessionId });
608
+ utils_2.logger.warn('MCP session expired (metrics); destroying', {
609
+ event: 'MCP_SSE_SESSION_GONE',
610
+ reason: 'expired',
611
+ httpMethod: req.method,
612
+ sessionId,
613
+ 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,
614
+ traceId: (0, request_context_1.getTraceId)(),
615
+ });
280
616
  session_1.sessionService.destroySession(sessionId);
281
617
  res.status(404).json({
282
618
  jsonrpc: '2.0',
@@ -288,21 +624,26 @@ async function handleSessionRequestGetDelete(req, res) {
288
624
  });
289
625
  return;
290
626
  }
627
+ (0, request_context_1.updateRequestContext)({
628
+ sessionId: sessionId || undefined,
629
+ userId: (_j = (_h = (_g = req.auth) === null || _g === void 0 ? void 0 : _g.extra) === null || _h === void 0 ? void 0 : _h.user) === null || _j === void 0 ? void 0 : _j.id,
630
+ });
291
631
  // Record interaction for metrics
292
632
  // Don't record interaction for tool calls as they'll be recorded separately
293
- if (((_a = req.body) === null || _a === void 0 ? void 0 : _a.method) !== 'tools/call') {
633
+ if (((_k = req.body) === null || _k === void 0 ? void 0 : _k.method) !== 'tools/call') {
294
634
  session_1.sessionService.recordInteraction(sessionId);
295
635
  }
296
636
  await session.transport.handleRequest(req, res, req.body);
297
637
  if (req.method === 'DELETE') {
638
+ session_1.toolCallQueueService.clearSession(sessionId);
298
639
  session_1.sessionService.destroySession(sessionId);
299
640
  }
300
641
  }
301
642
  catch (err) {
302
- const sessionId = (0, utils_1.getOrCreateSessionId)(req);
643
+ const sessionIdForErr = (0, utils_1.getOrCreateSessionId)(req);
303
644
  // Record error in session metrics
304
- if (sessionId) {
305
- session_1.sessionService.recordError(sessionId);
645
+ if (sessionIdForErr) {
646
+ session_1.sessionService.recordError(sessionIdForErr);
306
647
  }
307
648
  utils_2.logger.error('❌ Error in handleSessionRequestGetDelete', {
308
649
  error: {
@@ -311,7 +652,7 @@ async function handleSessionRequestGetDelete(req, res) {
311
652
  },
312
653
  method: req.method,
313
654
  url: req.url,
314
- sessionId,
655
+ sessionId: sessionIdForErr,
315
656
  });
316
657
  if (!res.headersSent) {
317
658
  res.status(500).json({
@@ -368,6 +709,7 @@ if (!(0, config_1.isTestEnvironment)()) {
368
709
  ttlMs: sessionConfig.ttlMs,
369
710
  maxSessions: sessionConfig.maxSessions,
370
711
  cleanupIntervalMs: sessionConfig.cleanupIntervalMs,
712
+ maxWallClockAgeMs: sessionConfig.maxWallClockAgeMs,
371
713
  },
372
714
  });
373
715
  });