@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.
- 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 +387 -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 +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
|
|
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(
|
|
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(
|
|
47
|
+
app.use(middleware_1.addRequestIdMiddleware);
|
|
35
48
|
// Request logging middleware (includes session metrics)
|
|
36
|
-
app.use(
|
|
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',
|
|
76
|
-
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);
|
|
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 (((
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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: (
|
|
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: (
|
|
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('/',
|
|
242
|
-
|
|
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
|
-
|
|
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('
|
|
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 (((
|
|
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
|
|
643
|
+
const sessionIdForErr = (0, utils_1.getOrCreateSessionId)(req);
|
|
295
644
|
// Record error in session metrics
|
|
296
|
-
if (
|
|
297
|
-
session_1.sessionService.recordError(
|
|
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
|
});
|