@carbonvoice/cv-mcp-server 2.8.0 → 2.8.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/env.js +152 -2
- package/dist/config/env.js.map +1 -1
- package/dist/cv-api.js +15 -0
- package/dist/cv-api.js.map +1 -1
- package/dist/interfaces/index.js +1 -0
- package/dist/interfaces/index.js.map +1 -1
- package/dist/interfaces/user-info.interface.js +3 -0
- package/dist/interfaces/user-info.interface.js.map +1 -0
- package/dist/server.js +582 -554
- package/dist/server.js.map +1 -1
- package/dist/transports/http/diagnostics/transport-send-diagnostics.js +115 -0
- package/dist/transports/http/diagnostics/transport-send-diagnostics.js.map +1 -0
- package/dist/transports/http/middleware/add-request-id.middlware.js +2 -2
- package/dist/transports/http/middleware/add-request-id.middlware.js.map +1 -1
- package/dist/transports/http/middleware/index.js +1 -0
- package/dist/transports/http/middleware/index.js.map +1 -1
- package/dist/transports/http/middleware/log-request.middlware.js +62 -11
- package/dist/transports/http/middleware/log-request.middlware.js.map +1 -1
- package/dist/transports/http/middleware/require-bearer-auth-with-absolute-metadata.middleware.js +72 -0
- package/dist/transports/http/middleware/require-bearer-auth-with-absolute-metadata.middleware.js.map +1 -0
- package/dist/transports/http/session/index.js +4 -1
- package/dist/transports/http/session/index.js.map +1 -1
- package/dist/transports/http/session/session.config.js +10 -5
- package/dist/transports/http/session/session.config.js.map +1 -1
- package/dist/transports/http/session/session.logger.js +22 -0
- package/dist/transports/http/session/session.logger.js.map +1 -1
- package/dist/transports/http/session/session.service.js +55 -22
- package/dist/transports/http/session/session.service.js.map +1 -1
- package/dist/transports/http/session/session.types.js.map +1 -1
- package/dist/transports/http/session/tool-call-queue.service.js +73 -0
- package/dist/transports/http/session/tool-call-queue.service.js.map +1 -0
- package/dist/transports/http/streamable-stateless.js +288 -0
- package/dist/transports/http/streamable-stateless.js.map +1 -0
- package/dist/transports/http/streamable.js +379 -37
- package/dist/transports/http/streamable.js.map +1 -1
- package/dist/transports/http/utils/request-context.js +3 -2
- package/dist/transports/http/utils/request-context.js.map +1 -1
- package/dist/utils/axios-instance.js +12 -0
- package/dist/utils/axios-instance.js.map +1 -1
- package/dist/utils/format-bytes-human.js +18 -0
- package/dist/utils/format-bytes-human.js.map +1 -0
- package/dist/utils/format-to-mcp-tool-response.js +46 -10
- package/dist/utils/format-to-mcp-tool-response.js.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.js +6 -19
- package/dist/utils/logger.js.map +1 -1
- 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(
|
|
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(
|
|
47
|
+
app.use(middleware_1.addRequestIdMiddleware);
|
|
43
48
|
// Request logging middleware (includes session metrics)
|
|
44
|
-
app.use(
|
|
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',
|
|
84
|
-
app.get('/.well-known/oauth-authorization-server',
|
|
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 (((
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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: (
|
|
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: (
|
|
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('/',
|
|
250
|
-
|
|
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
|
-
|
|
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('
|
|
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 (((
|
|
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
|
|
643
|
+
const sessionIdForErr = (0, utils_1.getOrCreateSessionId)(req);
|
|
303
644
|
// Record error in session metrics
|
|
304
|
-
if (
|
|
305
|
-
session_1.sessionService.recordError(
|
|
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
|
});
|