@carbonvoice/cv-mcp-server 2.7.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 +387 -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 +13 -5
@@ -4,41 +4,61 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
7
9
  const cors_1 = __importDefault(require("cors"));
8
10
  const express_1 = __importDefault(require("express"));
9
11
  const helmet_1 = __importDefault(require("helmet"));
10
- const bearerAuth_js_1 = require("@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js");
12
+ const serve_favicon_1 = __importDefault(require("serve-favicon"));
11
13
  const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
12
14
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
13
- const _1 = require(".");
14
15
  const constants_1 = require("./constants");
16
+ const transport_send_diagnostics_1 = require("./diagnostics/transport-send-diagnostics");
17
+ const middleware_1 = require("./middleware");
15
18
  const session_1 = require("./session");
16
19
  const session_config_1 = require("./session/session.config");
17
20
  const utils_1 = require("./utils");
21
+ const request_context_1 = require("./utils/request-context");
18
22
  const auth_1 = require("../../auth");
19
23
  const config_1 = require("../../config");
20
24
  const cv_api_1 = require("../../cv-api");
21
25
  const server_1 = __importDefault(require("../../server"));
22
26
  const utils_2 = require("../../utils");
23
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
+ });
24
32
  app.set('x-powered-by', false);
25
33
  // Trust proxy for rate limiting - only trust localhost and private networks
26
34
  app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
35
+ // Serve favicon
36
+ const faviconPath = path_1.default.join(process.cwd(), 'public', 'favicon.ico');
37
+ if (fs_1.default.existsSync(faviconPath)) {
38
+ app.use((0, serve_favicon_1.default)(faviconPath));
39
+ }
27
40
  // app.use(standardHeaders);
28
41
  app.use((0, cors_1.default)());
29
42
  // Security middlewares
30
43
  app.use((0, helmet_1.default)());
31
- app.use(_1.rateLimitMiddleware);
44
+ app.use(middleware_1.rateLimitMiddleware);
32
45
  app.use(express_1.default.json({ limit: '1mb' }));
33
46
  // Add request ID middleware
34
- app.use(_1.addRequestIdMiddleware);
47
+ app.use(middleware_1.addRequestIdMiddleware);
35
48
  // Request logging middleware (includes session metrics)
36
- app.use(_1.logRequest);
49
+ app.use(middleware_1.logRequest);
37
50
  let carbonVoiceApiHealth = {
38
51
  isHealthy: (0, config_1.isTestEnvironment)() ? true : false, // Assume healthy in test mode
39
52
  lastChecked: new Date().toISOString(),
40
53
  apiUrl: config_1.env.CARBON_VOICE_BASE_URL,
41
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
+ });
42
62
  app.get('/health', (req, res) => {
43
63
  const response = {
44
64
  status: carbonVoiceApiHealth.isHealthy ? 'healthy' : 'degraded',
@@ -72,8 +92,8 @@ app.get('/info', (req, res) => {
72
92
  res.status(200).send('OK');
73
93
  });
74
94
  // Protected resource metadata (OAuth 2.1 RFC9728 compliant)
75
- app.get('/.well-known/oauth-protected-resource', _1.wellKnownCorsHeaders, _1.oauthProtectedResource);
76
- 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);
77
97
  // Handle HEAD requests without auth
78
98
  app.head('/', (req, res) => {
79
99
  const mcpProtocolVersion = req.headers['mcp-protocol-version'] || types_js_1.LATEST_PROTOCOL_VERSION;
@@ -84,9 +104,13 @@ app.head('/', (req, res) => {
84
104
  .end();
85
105
  });
86
106
  async function handleSessionRequest(req, res, session) {
87
- 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;
88
108
  try {
89
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
+ });
90
114
  if (!session) {
91
115
  utils_2.logger.warn('Session not found for request', {
92
116
  sessionId,
@@ -102,11 +126,40 @@ async function handleSessionRequest(req, res, session) {
102
126
  return;
103
127
  }
104
128
  // Record tool call for metrics only when actually executing tools
105
- 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') {
106
130
  session_1.sessionService.recordToolCall(sessionId);
107
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
+ }
108
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
+ }
109
161
  if (req.method === 'DELETE') {
162
+ session_1.toolCallQueueService.clearSession(sessionId);
110
163
  session_1.sessionService.destroySession(sessionId);
111
164
  }
112
165
  }
@@ -137,12 +190,154 @@ async function handleSessionRequest(req, res, session) {
137
190
  }
138
191
  }
139
192
  }
140
- app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
141
- verifier: (0, auth_1.createOAuthTokenVerifier)(),
142
- requiredScopes: constants_1.REQUIRED_SCOPES,
143
- resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
144
- }), _1.addMcpSessionId, async (req, res, next) => {
145
- 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;
146
341
  try {
147
342
  const sessionId = (0, utils_1.getOrCreateSessionId)(req);
148
343
  const session = session_1.sessionService.getSession(sessionId);
@@ -161,7 +356,136 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
161
356
  if (((_a = req.body) === null || _a === void 0 ? void 0 : _a.method) !== 'tools/call') {
162
357
  session_1.sessionService.recordInteraction(sessionId);
163
358
  }
164
- 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
+ }
165
489
  return;
166
490
  }
167
491
  }
@@ -185,14 +509,21 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
185
509
  enableJsonResponse: true,
186
510
  });
187
511
  transport.onclose = () => {
188
- utils_2.logger.debug('🔌 Transport closed');
512
+ utils_2.logger.debug('🔌 Transport closed', {
513
+ event: 'MCP_TRANSPORT_CLOSED',
514
+ sessionId: transport.sessionId,
515
+ });
189
516
  if (transport.sessionId) {
517
+ transportDiagnostics.clearSession(transport.sessionId);
518
+ session_1.toolCallQueueService.clearSession(transport.sessionId);
190
519
  session_1.sessionService.destroySession(transport.sessionId);
191
520
  }
192
521
  };
193
522
  // Add error handling for the transport
194
523
  transport.onerror = (error) => {
195
524
  utils_2.logger.error('🔴 Transport error', {
525
+ event: 'MCP_TRANSPORT_ERROR',
526
+ sessionId: transport.sessionId,
196
527
  error: {
197
528
  message: error.message,
198
529
  name: error.name,
@@ -203,6 +534,7 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
203
534
  session_1.sessionService.recordError(transport.sessionId);
204
535
  }
205
536
  };
537
+ transportDiagnostics.attach(transport);
206
538
  await server_1.default.connect(transport);
207
539
  await transport.handleRequest(req, res, req.body);
208
540
  return;
@@ -210,7 +542,7 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
210
542
  // No session ID and no initialize request
211
543
  utils_2.logger.warn('❌ Session ID not found', {
212
544
  sessionId,
213
- 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,
214
546
  headers: req.headers,
215
547
  });
216
548
  res.status(404).json({
@@ -232,30 +564,35 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
232
564
  url: req.url,
233
565
  sessionId: (0, utils_1.getOrCreateSessionId)(req),
234
566
  hasInitializeRequest: (0, types_js_1.isInitializeRequest)(req.body),
235
- 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,
236
568
  });
237
569
  next(error);
238
570
  }
239
571
  });
240
572
  // GET/DELETE : server-to-client (SSE) and session termination
241
- app.get('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
242
- verifier: (0, auth_1.createOAuthTokenVerifier)(),
243
- requiredScopes: constants_1.REQUIRED_SCOPES,
244
- resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
245
- }), _1.addMcpSessionId, handleSessionRequestGetDelete);
246
- app.delete('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
247
- verifier: (0, auth_1.createOAuthTokenVerifier)(),
248
- requiredScopes: constants_1.REQUIRED_SCOPES,
249
- resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
250
- }), _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);
251
575
  // GET/DELETE : server-to-client (SSE) and session termination
252
576
  async function handleSessionRequestGetDelete(req, res) {
253
- var _a;
577
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
254
578
  try {
255
579
  const sessionId = (0, utils_1.getOrCreateSessionId)(req);
256
580
  const session = session_1.sessionService.getSession(sessionId);
257
581
  if (!session) {
258
- 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
+ });
259
596
  res.status(404).json({
260
597
  jsonrpc: '2.0',
261
598
  error: {
@@ -268,7 +605,14 @@ async function handleSessionRequestGetDelete(req, res) {
268
605
  }
269
606
  // Check if session is expired
270
607
  if (session_1.sessionService.isSessionExpired(sessionId)) {
271
- 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
+ });
272
616
  session_1.sessionService.destroySession(sessionId);
273
617
  res.status(404).json({
274
618
  jsonrpc: '2.0',
@@ -280,21 +624,26 @@ async function handleSessionRequestGetDelete(req, res) {
280
624
  });
281
625
  return;
282
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
+ });
283
631
  // Record interaction for metrics
284
632
  // Don't record interaction for tool calls as they'll be recorded separately
285
- 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') {
286
634
  session_1.sessionService.recordInteraction(sessionId);
287
635
  }
288
636
  await session.transport.handleRequest(req, res, req.body);
289
637
  if (req.method === 'DELETE') {
638
+ session_1.toolCallQueueService.clearSession(sessionId);
290
639
  session_1.sessionService.destroySession(sessionId);
291
640
  }
292
641
  }
293
642
  catch (err) {
294
- const sessionId = (0, utils_1.getOrCreateSessionId)(req);
643
+ const sessionIdForErr = (0, utils_1.getOrCreateSessionId)(req);
295
644
  // Record error in session metrics
296
- if (sessionId) {
297
- session_1.sessionService.recordError(sessionId);
645
+ if (sessionIdForErr) {
646
+ session_1.sessionService.recordError(sessionIdForErr);
298
647
  }
299
648
  utils_2.logger.error('❌ Error in handleSessionRequestGetDelete', {
300
649
  error: {
@@ -303,7 +652,7 @@ async function handleSessionRequestGetDelete(req, res) {
303
652
  },
304
653
  method: req.method,
305
654
  url: req.url,
306
- sessionId,
655
+ sessionId: sessionIdForErr,
307
656
  });
308
657
  if (!res.headersSent) {
309
658
  res.status(500).json({
@@ -360,6 +709,7 @@ if (!(0, config_1.isTestEnvironment)()) {
360
709
  ttlMs: sessionConfig.ttlMs,
361
710
  maxSessions: sessionConfig.maxSessions,
362
711
  cleanupIntervalMs: sessionConfig.cleanupIntervalMs,
712
+ maxWallClockAgeMs: sessionConfig.maxWallClockAgeMs,
363
713
  },
364
714
  });
365
715
  });