@carbonvoice/cv-mcp-server 1.1.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/auth.service.js +79 -0
- package/dist/auth/auth.service.js.map +1 -0
- package/dist/auth/index.js +15 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/interfaces/auth.types.js +3 -0
- package/dist/auth/interfaces/auth.types.js.map +1 -0
- package/dist/auth/interfaces/index.js +1 -0
- package/dist/auth/interfaces/index.js.map +1 -1
- package/dist/cv-api.js +13 -5
- package/dist/cv-api.js.map +1 -1
- package/dist/server.js +143 -31
- package/dist/server.js.map +1 -1
- package/dist/transports/http/interfaces/index.js +3 -0
- package/dist/transports/http/interfaces/index.js.map +1 -0
- package/dist/transports/http/middleware/add-mcp-session-id.middleware.js +20 -13
- package/dist/transports/http/middleware/add-mcp-session-id.middleware.js.map +1 -1
- package/dist/transports/http/middleware/add-request-id.middlware.js +13 -4
- package/dist/transports/http/middleware/add-request-id.middlware.js.map +1 -1
- package/dist/transports/http/middleware/log-request.middlware.js +27 -11
- package/dist/transports/http/middleware/log-request.middlware.js.map +1 -1
- package/dist/transports/http/session/index.js +34 -0
- package/dist/transports/http/session/index.js.map +1 -0
- package/dist/transports/http/session/session-cleanup.js +60 -0
- package/dist/transports/http/session/session-cleanup.js.map +1 -0
- package/dist/transports/http/session/session-manager.js +52 -0
- package/dist/transports/http/session/session-manager.js.map +1 -0
- package/dist/transports/http/session/session.config.js +29 -0
- package/dist/transports/http/session/session.config.js.map +1 -0
- package/dist/transports/http/session/session.logger.js +68 -0
- package/dist/transports/http/session/session.logger.js.map +1 -0
- package/dist/transports/http/session/session.service.js +196 -0
- package/dist/transports/http/session/session.service.js.map +1 -0
- package/dist/transports/http/session/session.types.js +31 -0
- package/dist/transports/http/session/session.types.js.map +1 -0
- package/dist/transports/http/streamable.js +184 -427
- package/dist/transports/http/streamable.js.map +1 -1
- package/dist/transports/http/utils/request-context.js +78 -0
- package/dist/transports/http/utils/request-context.js.map +1 -0
- package/dist/utils/axios-instance.js +34 -22
- package/dist/utils/axios-instance.js.map +1 -1
- package/dist/utils/get-process-uptime.js +13 -0
- package/dist/utils/get-process-uptime.js.map +1 -0
- package/dist/utils/index.js +2 -2
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.js +34 -11
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/{format-time-to-human.js → time-to-human.js} +17 -7
- package/dist/utils/time-to-human.js.map +1 -0
- package/package.json +1 -1
- package/dist/auth/auth-middleware.js +0 -203
- package/dist/auth/auth-middleware.js.map +0 -1
- package/dist/utils/format-time-to-human.js.map +0 -1
- package/dist/utils/format-uptime.js +0 -13
- package/dist/utils/format-uptime.js.map +0 -1
|
@@ -13,46 +13,15 @@ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamable
|
|
|
13
13
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
14
14
|
const _1 = require(".");
|
|
15
15
|
const constants_1 = require("./constants");
|
|
16
|
+
const session_1 = require("./session");
|
|
17
|
+
const session_config_1 = require("./session/session.config");
|
|
16
18
|
const utils_1 = require("./utils");
|
|
17
|
-
const
|
|
19
|
+
const auth_1 = require("../../auth");
|
|
18
20
|
const config_1 = require("../../config");
|
|
19
|
-
const constants_2 = require("../../constants");
|
|
20
21
|
const cv_api_1 = require("../../cv-api");
|
|
21
22
|
const server_1 = __importDefault(require("../../server"));
|
|
22
23
|
const utils_2 = require("../../utils");
|
|
23
24
|
const app = (0, express_1.default)();
|
|
24
|
-
const SESSION_TTL_MS = 1000 * 60 * 60 * 1; // 1 hour
|
|
25
|
-
/**
|
|
26
|
-
* Sets standard headers for all requests.
|
|
27
|
-
* @param _req - The request.
|
|
28
|
-
* @param res - The response.
|
|
29
|
-
* @param next - The next handler.
|
|
30
|
-
*/
|
|
31
|
-
function standardHeaders(_req, res, next) {
|
|
32
|
-
// Disables all caching
|
|
33
|
-
res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
34
|
-
res.set('Pragma', 'no-cache');
|
|
35
|
-
// if (getConfig().baseUrl.startsWith('https://')) {
|
|
36
|
-
// // Only connect to this site and subdomains via HTTPS for the next two years
|
|
37
|
-
// // and also include in the preload list
|
|
38
|
-
// res.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
|
|
39
|
-
// }
|
|
40
|
-
// Set Content Security Policy
|
|
41
|
-
// As an API server, block everything
|
|
42
|
-
// See: https://stackoverflow.com/a/45631261/2051724
|
|
43
|
-
res.set('Content-Security-Policy', "default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none';");
|
|
44
|
-
// Disable browser features
|
|
45
|
-
res.set('Permissions-Policy', 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), interest-cohort=()');
|
|
46
|
-
// Never send the Referer header
|
|
47
|
-
res.set('Referrer-Policy', 'no-referrer');
|
|
48
|
-
// Prevent browsers from incorrectly detecting non-scripts as scripts
|
|
49
|
-
res.set('X-Content-Type-Options', 'nosniff');
|
|
50
|
-
// Disallow attempts to iframe site
|
|
51
|
-
res.set('X-Frame-Options', 'DENY');
|
|
52
|
-
// Block pages from loading when they detect reflected XSS attacks
|
|
53
|
-
res.set('X-XSS-Protection', '1; mode=block');
|
|
54
|
-
next();
|
|
55
|
-
}
|
|
56
25
|
app.set('x-powered-by', false);
|
|
57
26
|
// Trust proxy for rate limiting - only trust localhost and private networks
|
|
58
27
|
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
|
|
@@ -69,75 +38,18 @@ app.use((0, express_rate_limit_1.default)({
|
|
|
69
38
|
app.use(express_1.default.json({ limit: '1mb' }));
|
|
70
39
|
// Add request ID middleware
|
|
71
40
|
app.use(_1.addRequestIdMiddleware);
|
|
72
|
-
// Request logging middleware
|
|
41
|
+
// Request logging middleware (includes session metrics)
|
|
73
42
|
app.use(_1.logRequest);
|
|
74
43
|
let carbonVoiceApiHealth = {
|
|
75
44
|
isHealthy: false,
|
|
76
45
|
lastChecked: new Date().toISOString(),
|
|
46
|
+
apiUrl: config_1.env.CARBON_VOICE_BASE_URL,
|
|
77
47
|
};
|
|
78
|
-
const sessions = new Map();
|
|
79
|
-
function createSession(transport, req, sessionId) {
|
|
80
|
-
var _a, _b, _c, _d, _e, _f;
|
|
81
|
-
// Should never happen
|
|
82
|
-
if (!((_b = (_a = req.auth) === null || _a === void 0 ? void 0 : _a.extra) === null || _b === void 0 ? void 0 : _b.user)) {
|
|
83
|
-
throw new Error('User not found in session creation');
|
|
84
|
-
}
|
|
85
|
-
// const sessionId = getOrCreateSessionId(req);
|
|
86
|
-
// Clean up after TTL
|
|
87
|
-
const timeout = setTimeout(() => {
|
|
88
|
-
utils_2.logger.info('⏰ Session timeout triggered', { sessionId });
|
|
89
|
-
destroySession(sessionId);
|
|
90
|
-
}, SESSION_TTL_MS);
|
|
91
|
-
const userId = (_d = (_c = req.auth) === null || _c === void 0 ? void 0 : _c.extra) === null || _d === void 0 ? void 0 : _d.user.id;
|
|
92
|
-
sessions.set(sessionId, {
|
|
93
|
-
transport,
|
|
94
|
-
timeout,
|
|
95
|
-
userId,
|
|
96
|
-
metrics: {
|
|
97
|
-
sessionId,
|
|
98
|
-
userId,
|
|
99
|
-
createdAt: new Date(),
|
|
100
|
-
expiresAt: new Date(Date.now() + SESSION_TTL_MS),
|
|
101
|
-
totalInteractions: 0,
|
|
102
|
-
totalToolCalls: 0,
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
utils_2.logger.info('🆕 Session created', {
|
|
106
|
-
sessionId,
|
|
107
|
-
userId: (_f = (_e = req.auth) === null || _e === void 0 ? void 0 : _e.extra) === null || _f === void 0 ? void 0 : _f.user.id,
|
|
108
|
-
});
|
|
109
|
-
return sessionId;
|
|
110
|
-
}
|
|
111
|
-
function destroySession(sessionId) {
|
|
112
|
-
utils_2.logger.info('🔚 Destroying session', { sessionId });
|
|
113
|
-
const session = sessions.get(sessionId);
|
|
114
|
-
if (session && !session.destroying) {
|
|
115
|
-
// Mark session as being destroyed to prevent recursive calls
|
|
116
|
-
session.destroying = true;
|
|
117
|
-
const {} = session.metrics;
|
|
118
|
-
clearTimeout(session.timeout);
|
|
119
|
-
session.transport.close();
|
|
120
|
-
sessions.delete(sessionId);
|
|
121
|
-
const durationInSeconds = (new Date().getTime() - session.metrics.createdAt.getTime()) / 1000;
|
|
122
|
-
utils_2.logger.info('❌ Session destroyed', {
|
|
123
|
-
sessionId,
|
|
124
|
-
duration: (0, utils_2.formatTimeToHuman)(durationInSeconds),
|
|
125
|
-
createdAt: session.metrics.createdAt.toISOString(),
|
|
126
|
-
expiresAt: session.metrics.expiresAt.toISOString(),
|
|
127
|
-
totalInteractions: session.metrics.totalInteractions,
|
|
128
|
-
totalToolCalls: session.metrics.totalToolCalls,
|
|
129
|
-
userId: session.userId,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
else if (session === null || session === void 0 ? void 0 : session.destroying) {
|
|
133
|
-
utils_2.logger.debug('Session already being destroyed, skipping', { sessionId });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
48
|
app.get('/health', (req, res) => {
|
|
137
49
|
const response = {
|
|
138
50
|
status: carbonVoiceApiHealth.isHealthy ? 'healthy' : 'degraded',
|
|
139
51
|
timestamp: new Date().toISOString(),
|
|
140
|
-
uptime: (0, utils_2.
|
|
52
|
+
uptime: (0, utils_2.getProcessUptime)(),
|
|
141
53
|
dependencies: {
|
|
142
54
|
carbonVoiceApi: {
|
|
143
55
|
status: carbonVoiceApiHealth.isHealthy ? 'healthy' : 'unhealthy',
|
|
@@ -177,111 +89,142 @@ app.head('/', _1.logRequest, (req, res) => {
|
|
|
177
89
|
.set('MCP-Protocol-Version', mcpProtocolVersion)
|
|
178
90
|
.end();
|
|
179
91
|
});
|
|
180
|
-
// Single shared transport for all requests
|
|
181
|
-
// const sharedTransport = new StreamableHTTPServerTransport({
|
|
182
|
-
// sessionIdGenerator: undefined, // Stateless
|
|
183
|
-
// enableJsonResponse: true,
|
|
184
|
-
// });
|
|
185
|
-
// Add error handling
|
|
186
|
-
// sharedTransport.onerror = (error) => {
|
|
187
|
-
// logger.error('🚨 Transport error', { error: error.message });
|
|
188
|
-
// };
|
|
189
|
-
// sharedTransport.onclose = () => {
|
|
190
|
-
// logger.info('🔴 Transport closed for all requests');
|
|
191
|
-
// };
|
|
192
92
|
async function handleSessionRequest(req, res, session) {
|
|
193
|
-
var _a
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
93
|
+
var _a;
|
|
94
|
+
try {
|
|
95
|
+
const sessionId = (0, utils_1.getOrCreateSessionId)(req);
|
|
96
|
+
if (!session) {
|
|
97
|
+
utils_2.logger.warn('Session not found for request', {
|
|
98
|
+
sessionId,
|
|
99
|
+
});
|
|
100
|
+
res.status(404).json({
|
|
101
|
+
jsonrpc: '2.0',
|
|
102
|
+
error: {
|
|
103
|
+
code: 404,
|
|
104
|
+
message: 'Session not found. Please reinitialize.',
|
|
105
|
+
},
|
|
106
|
+
id: null,
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Record tool call for metrics only when actually executing tools
|
|
111
|
+
if (((_a = req.body) === null || _a === void 0 ? void 0 : _a.method) === 'tools/call') {
|
|
112
|
+
session_1.sessionService.recordToolCall(sessionId);
|
|
113
|
+
}
|
|
114
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
115
|
+
if (req.method === 'DELETE') {
|
|
116
|
+
session_1.sessionService.destroySession(sessionId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const sessionId = (0, utils_1.getOrCreateSessionId)(req);
|
|
121
|
+
// Record error in session metrics
|
|
122
|
+
if (sessionId) {
|
|
123
|
+
session_1.sessionService.recordError(sessionId);
|
|
124
|
+
}
|
|
125
|
+
utils_2.logger.error('❌ Error in handleSessionRequest', {
|
|
202
126
|
error: {
|
|
203
|
-
|
|
204
|
-
|
|
127
|
+
message: err instanceof Error ? err.message : String(err),
|
|
128
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
205
129
|
},
|
|
206
|
-
|
|
130
|
+
method: req.method,
|
|
131
|
+
url: req.url,
|
|
132
|
+
sessionId,
|
|
207
133
|
});
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
134
|
+
if (!res.headersSent) {
|
|
135
|
+
res.status(500).json({
|
|
136
|
+
jsonrpc: '2.0',
|
|
137
|
+
error: {
|
|
138
|
+
code: -32603,
|
|
139
|
+
message: 'Internal server error',
|
|
140
|
+
},
|
|
141
|
+
id: null,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
213
144
|
}
|
|
214
|
-
utils_2.logger.info('🔁 Reusing session', session.metrics);
|
|
215
|
-
return session.transport.handleRequest(req, res, req.body);
|
|
216
145
|
}
|
|
217
146
|
app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
|
|
218
|
-
verifier: (0,
|
|
147
|
+
verifier: (0, auth_1.createOAuthTokenVerifier)(),
|
|
219
148
|
requiredScopes: constants_1.REQUIRED_SCOPES,
|
|
220
149
|
resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
|
|
221
150
|
}), _1.addMcpSessionId, async (req, res, next) => {
|
|
222
|
-
var _a, _b, _c, _d, _e, _f;
|
|
151
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
223
152
|
try {
|
|
224
153
|
const sessionId = (0, utils_1.getOrCreateSessionId)(req);
|
|
225
|
-
const session =
|
|
154
|
+
const session = session_1.sessionService.getSession(sessionId);
|
|
226
155
|
// Reuse existing session
|
|
227
156
|
if (session) {
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
157
|
+
// Check if session is expired
|
|
158
|
+
if (session_1.sessionService.isSessionExpired(sessionId)) {
|
|
159
|
+
utils_2.logger.warn('Session expired, destroying and creating new one', {
|
|
160
|
+
sessionId,
|
|
161
|
+
});
|
|
162
|
+
session_1.sessionService.destroySession(sessionId);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Record interaction and reuse session
|
|
166
|
+
// Don't record interaction for tool calls as they'll be recorded separately
|
|
167
|
+
if (((_a = req.body) === null || _a === void 0 ? void 0 : _a.method) !== 'tools/call') {
|
|
168
|
+
session_1.sessionService.recordInteraction(sessionId);
|
|
169
|
+
}
|
|
170
|
+
await handleSessionRequest(req, res, session);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
235
173
|
}
|
|
236
174
|
// Create New Session
|
|
237
175
|
if ((0, types_js_1.isInitializeRequest)(req.body)) {
|
|
238
176
|
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
239
177
|
sessionIdGenerator: () => (0, utils_1.getOrCreateSessionId)(req),
|
|
240
|
-
// sessionIdGenerator: undefined,
|
|
241
178
|
onsessioninitialized: (sessionId) => {
|
|
242
|
-
// const user = req.auth?.extra?.user;
|
|
243
179
|
var _a, _b, _c;
|
|
244
|
-
utils_2.logger.info('
|
|
180
|
+
utils_2.logger.info('New session initialized', {
|
|
245
181
|
sessionId,
|
|
246
|
-
requestorHeaders: (0, utils_2.obfuscateAuthHeaders)(req.headers),
|
|
247
182
|
userId: (_c = (_b = (_a = req.auth) === null || _a === void 0 ? void 0 : _a.extra) === null || _b === void 0 ? void 0 : _b.user) === null || _c === void 0 ? void 0 : _c.id,
|
|
248
183
|
});
|
|
249
|
-
|
|
184
|
+
try {
|
|
185
|
+
session_1.sessionService.createSession(transport, req, sessionId);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
utils_2.logger.error('Failed to create session', {
|
|
189
|
+
sessionId,
|
|
190
|
+
error: error instanceof Error ? error.message : String(error),
|
|
191
|
+
});
|
|
192
|
+
// Close transport if session creation fails
|
|
193
|
+
transport.close();
|
|
194
|
+
}
|
|
250
195
|
},
|
|
251
196
|
enableJsonResponse: true,
|
|
252
197
|
});
|
|
253
198
|
transport.onclose = () => {
|
|
254
|
-
utils_2.logger.
|
|
199
|
+
utils_2.logger.debug('Transport closed', {
|
|
255
200
|
sessionId: transport.sessionId,
|
|
256
|
-
hasSessionId: !!transport.sessionId,
|
|
257
201
|
});
|
|
258
|
-
if (transport.sessionId)
|
|
259
|
-
destroySession(transport.sessionId);
|
|
202
|
+
if (transport.sessionId) {
|
|
203
|
+
session_1.sessionService.destroySession(transport.sessionId);
|
|
204
|
+
}
|
|
260
205
|
};
|
|
261
206
|
// Add error handling for the transport
|
|
262
207
|
transport.onerror = (error) => {
|
|
263
|
-
|
|
264
|
-
utils_2.logger.error('🚨 Transport error occurred', {
|
|
208
|
+
utils_2.logger.error('Transport error', {
|
|
265
209
|
sessionId: transport.sessionId,
|
|
266
|
-
sessionMetrics: (_a = sessions.get(transport.sessionId)) === null || _a === void 0 ? void 0 : _a.metrics,
|
|
267
210
|
error: {
|
|
268
211
|
message: error.message,
|
|
269
212
|
name: error.name,
|
|
270
|
-
stack: error.stack,
|
|
271
213
|
},
|
|
272
214
|
});
|
|
215
|
+
// Record error in session metrics
|
|
216
|
+
if (transport.sessionId) {
|
|
217
|
+
session_1.sessionService.recordError(transport.sessionId);
|
|
218
|
+
}
|
|
273
219
|
};
|
|
274
220
|
await server_1.default.connect(transport);
|
|
275
221
|
await transport.handleRequest(req, res, req.body);
|
|
276
222
|
return;
|
|
277
|
-
// const session = sessions.get(transport!.sessionId!);
|
|
278
|
-
// await handleSessionRequest(req, res, session!);
|
|
279
223
|
}
|
|
280
224
|
// No session ID and no initialize request
|
|
281
|
-
// Should never happen since we are gonna always have a session ID
|
|
282
225
|
utils_2.logger.warn('❌ Session ID not found', {
|
|
283
226
|
sessionId,
|
|
284
|
-
userId: (
|
|
227
|
+
userId: (_d = (_c = (_b = req.auth) === null || _b === void 0 ? void 0 : _b.extra) === null || _c === void 0 ? void 0 : _c.user) === null || _d === void 0 ? void 0 : _d.id,
|
|
285
228
|
headers: req.headers,
|
|
286
229
|
});
|
|
287
230
|
res.status(404).json({
|
|
@@ -292,9 +235,6 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
|
|
|
292
235
|
},
|
|
293
236
|
id: null,
|
|
294
237
|
});
|
|
295
|
-
return;
|
|
296
|
-
// await handleSessionRequest(req, res, sessions.get(sessionId));
|
|
297
|
-
// await sharedTransport.handleRequest(req, res, req.body);
|
|
298
238
|
}
|
|
299
239
|
catch (error) {
|
|
300
240
|
utils_2.logger.error('❌ Error in POST handler', {
|
|
@@ -306,17 +246,28 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
|
|
|
306
246
|
url: req.url,
|
|
307
247
|
sessionId: (0, utils_1.getOrCreateSessionId)(req),
|
|
308
248
|
hasInitializeRequest: (0, types_js_1.isInitializeRequest)(req.body),
|
|
309
|
-
userId: (
|
|
249
|
+
userId: (_g = (_f = (_e = req.auth) === null || _e === void 0 ? void 0 : _e.extra) === null || _f === void 0 ? void 0 : _f.user) === null || _g === void 0 ? void 0 : _g.id,
|
|
310
250
|
});
|
|
311
251
|
next(error);
|
|
312
252
|
}
|
|
313
253
|
});
|
|
314
254
|
// GET/DELETE : server-to-client (SSE) and session termination
|
|
255
|
+
app.get('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
|
|
256
|
+
verifier: (0, auth_1.createOAuthTokenVerifier)(),
|
|
257
|
+
requiredScopes: constants_1.REQUIRED_SCOPES,
|
|
258
|
+
resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
|
|
259
|
+
}), _1.addMcpSessionId, handleSessionRequestGetDelete);
|
|
260
|
+
app.delete('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
|
|
261
|
+
verifier: (0, auth_1.createOAuthTokenVerifier)(),
|
|
262
|
+
requiredScopes: constants_1.REQUIRED_SCOPES,
|
|
263
|
+
resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
|
|
264
|
+
}), _1.addMcpSessionId, handleSessionRequestGetDelete);
|
|
315
265
|
// GET/DELETE : server-to-client (SSE) and session termination
|
|
316
266
|
async function handleSessionRequestGetDelete(req, res) {
|
|
267
|
+
var _a;
|
|
317
268
|
try {
|
|
318
269
|
const sessionId = (0, utils_1.getOrCreateSessionId)(req);
|
|
319
|
-
const session =
|
|
270
|
+
const session = session_1.sessionService.getSession(sessionId);
|
|
320
271
|
if (!session) {
|
|
321
272
|
utils_2.logger.warn('Invalid or missing session ID', { sessionId });
|
|
322
273
|
res.status(404).json({
|
|
@@ -329,276 +280,97 @@ async function handleSessionRequestGetDelete(req, res) {
|
|
|
329
280
|
});
|
|
330
281
|
return;
|
|
331
282
|
}
|
|
283
|
+
// Check if session is expired
|
|
284
|
+
if (session_1.sessionService.isSessionExpired(sessionId)) {
|
|
285
|
+
utils_2.logger.warn('Session expired, destroying', { sessionId });
|
|
286
|
+
session_1.sessionService.destroySession(sessionId);
|
|
287
|
+
res.status(404).json({
|
|
288
|
+
jsonrpc: '2.0',
|
|
289
|
+
error: {
|
|
290
|
+
code: 404,
|
|
291
|
+
message: 'Session expired. Please reinitialize.',
|
|
292
|
+
},
|
|
293
|
+
id: null,
|
|
294
|
+
});
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// Record interaction for metrics
|
|
298
|
+
// Don't record interaction for tool calls as they'll be recorded separately
|
|
299
|
+
if (((_a = req.body) === null || _a === void 0 ? void 0 : _a.method) !== 'tools/call') {
|
|
300
|
+
session_1.sessionService.recordInteraction(sessionId);
|
|
301
|
+
}
|
|
332
302
|
await session.transport.handleRequest(req, res, req.body);
|
|
333
303
|
if (req.method === 'DELETE') {
|
|
334
|
-
destroySession(sessionId);
|
|
304
|
+
session_1.sessionService.destroySession(sessionId);
|
|
335
305
|
}
|
|
336
306
|
}
|
|
337
307
|
catch (err) {
|
|
308
|
+
const sessionId = (0, utils_1.getOrCreateSessionId)(req);
|
|
309
|
+
// Record error in session metrics
|
|
310
|
+
if (sessionId) {
|
|
311
|
+
session_1.sessionService.recordError(sessionId);
|
|
312
|
+
}
|
|
338
313
|
utils_2.logger.error('❌ Error in handleSessionRequestGetDelete', {
|
|
339
314
|
error: {
|
|
340
315
|
message: err instanceof Error ? err.message : String(err),
|
|
341
316
|
stack: err instanceof Error ? err.stack : undefined,
|
|
342
317
|
},
|
|
318
|
+
method: req.method,
|
|
319
|
+
url: req.url,
|
|
320
|
+
sessionId,
|
|
343
321
|
});
|
|
322
|
+
if (!res.headersSent) {
|
|
323
|
+
res.status(500).json({
|
|
324
|
+
jsonrpc: '2.0',
|
|
325
|
+
error: {
|
|
326
|
+
code: -32603,
|
|
327
|
+
message: 'Internal server error',
|
|
328
|
+
},
|
|
329
|
+
id: null,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
344
332
|
}
|
|
345
333
|
}
|
|
346
|
-
// GET/DELETE : server-to-client (SSE) and session termination
|
|
347
|
-
app.get('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
|
|
348
|
-
verifier: (0, auth_middleware_1.createOAuthTokenVerifier)(),
|
|
349
|
-
requiredScopes: constants_1.REQUIRED_SCOPES,
|
|
350
|
-
resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
|
|
351
|
-
}), _1.addMcpSessionId, handleSessionRequestGetDelete);
|
|
352
|
-
app.delete('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
|
|
353
|
-
verifier: (0, auth_middleware_1.createOAuthTokenVerifier)(),
|
|
354
|
-
requiredScopes: constants_1.REQUIRED_SCOPES,
|
|
355
|
-
resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
|
|
356
|
-
}), _1.addMcpSessionId, handleSessionRequestGetDelete);
|
|
357
|
-
// app.get(
|
|
358
|
-
// '/',
|
|
359
|
-
// authErrorLogger,
|
|
360
|
-
// requireBearerAuth({
|
|
361
|
-
// verifier: createOAuthTokenVerifier(),
|
|
362
|
-
// requiredScopes: REQUIRED_SCOPES,
|
|
363
|
-
// resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
|
|
364
|
-
// }),
|
|
365
|
-
// addMcpSessionId,
|
|
366
|
-
// (req: AuthenticatedRequest, res: Response) => {
|
|
367
|
-
// return handleSessionRequest(
|
|
368
|
-
// req,
|
|
369
|
-
// res,
|
|
370
|
-
// sessions.get(getOrCreateSessionId(req)),
|
|
371
|
-
// );
|
|
372
|
-
// },
|
|
373
|
-
// );
|
|
374
|
-
// app.delete(
|
|
375
|
-
// '/',
|
|
376
|
-
// authErrorLogger,
|
|
377
|
-
// requireBearerAuth({
|
|
378
|
-
// verifier: createOAuthTokenVerifier(),
|
|
379
|
-
// requiredScopes: REQUIRED_SCOPES,
|
|
380
|
-
// resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
|
|
381
|
-
// }),
|
|
382
|
-
// addMcpSessionId,
|
|
383
|
-
// (req: AuthenticatedRequest, res: Response) => {
|
|
384
|
-
// destroySession(getOrCreateSessionId(req));
|
|
385
|
-
// },
|
|
386
|
-
// );
|
|
387
|
-
// For stateless servers, GET should return 405 Method Not Allowed
|
|
388
|
-
// app.get('/', (req, res) => {
|
|
389
|
-
// logger.warn('GET request to (not supported in stateless mode)');
|
|
390
|
-
// res.writeHead(405, {
|
|
391
|
-
// 'Content-Type': 'application/json',
|
|
392
|
-
// Allow: 'POST',
|
|
393
|
-
// });
|
|
394
|
-
// res.end(
|
|
395
|
-
// JSON.stringify({
|
|
396
|
-
// jsonrpc: '2.0',
|
|
397
|
-
// error: {
|
|
398
|
-
// code: -32601, // "Method not found"
|
|
399
|
-
// message: 'GET not supported in stateless mode; use POST at ',
|
|
400
|
-
// },
|
|
401
|
-
// id: null,
|
|
402
|
-
// }),
|
|
403
|
-
// );
|
|
404
|
-
// return;
|
|
405
|
-
// });
|
|
406
|
-
// For stateless servers, DELETE should return 405 Method Not Allowed
|
|
407
|
-
// app.delete('/', (req, res) => {
|
|
408
|
-
// logger.warn('DELETE request to (not supported in stateless mode)');
|
|
409
|
-
// res.status(405).json({
|
|
410
|
-
// jsonrpc: '2.0',
|
|
411
|
-
// error: {
|
|
412
|
-
// code: -32000,
|
|
413
|
-
// message: "Method not allowed. Stateless server doesn't support sessions.",
|
|
414
|
-
// },
|
|
415
|
-
// id: null,
|
|
416
|
-
// });
|
|
417
|
-
// });
|
|
418
|
-
// POST : client-to-server (with authentication)
|
|
419
|
-
// app.post(
|
|
420
|
-
// 'x',
|
|
421
|
-
// async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
|
422
|
-
// logger.warn('Verifying access token');
|
|
423
|
-
// verifyAccessToken(req, res, next);
|
|
424
|
-
// logger.warn('Access token verified');
|
|
425
|
-
// try {
|
|
426
|
-
// // if (req.headers) {
|
|
427
|
-
// // throw new UnauthorizedException();
|
|
428
|
-
// // }
|
|
429
|
-
// const sessionId = getOrCreateSessionId(req);
|
|
430
|
-
// // Reuse existing session
|
|
431
|
-
// if (sessionId && sessions.has(sessionId)) {
|
|
432
|
-
// logger.info('Reusing session', { sessionId });
|
|
433
|
-
// await sessions
|
|
434
|
-
// .get(sessionId)!
|
|
435
|
-
// .transport!.handleRequest(req, res, req.body);
|
|
436
|
-
// return;
|
|
437
|
-
// }
|
|
438
|
-
// // Create New Session
|
|
439
|
-
// if (!sessionId && isInitializeRequest(req.body)) {
|
|
440
|
-
// const requestorHeaders = req.headers;
|
|
441
|
-
// // TODO: The place I can see info about client is: user-agent
|
|
442
|
-
// const transport = new StreamableHTTPServerTransport({
|
|
443
|
-
// sessionIdGenerator: () => randomUUID(),
|
|
444
|
-
// onsessioninitialized: (transportSessionId: string) => {
|
|
445
|
-
// logger.info('New session initialized', {
|
|
446
|
-
// sessionId: transportSessionId,
|
|
447
|
-
// requestorHeaders,
|
|
448
|
-
// userId: req.auth?.extra?.user?.id,
|
|
449
|
-
// });
|
|
450
|
-
// const sessionId = createSession(transport!, req.auth?.extra?.user);
|
|
451
|
-
// if (req.auth?.extra?.user) {
|
|
452
|
-
// setUserContext(sessionId, req.auth?.extra?.user);
|
|
453
|
-
// }
|
|
454
|
-
// },
|
|
455
|
-
// enableJsonResponse: true,
|
|
456
|
-
// });
|
|
457
|
-
// transport.onclose = () => {
|
|
458
|
-
// if (transport!.sessionId) destroySession(transport!.sessionId);
|
|
459
|
-
// };
|
|
460
|
-
// await server.connect(transport);
|
|
461
|
-
// await transport.handleRequest(req, res, req.body);
|
|
462
|
-
// return;
|
|
463
|
-
// }
|
|
464
|
-
// // No session ID and no initialize request
|
|
465
|
-
// logger.warn('Bad request: No valid session ID provided', {
|
|
466
|
-
// sessionId,
|
|
467
|
-
// headers: req.headers,
|
|
468
|
-
// });
|
|
469
|
-
// res.status(404).json({
|
|
470
|
-
// jsonrpc: '2.0',
|
|
471
|
-
// error: {
|
|
472
|
-
// code: 404,
|
|
473
|
-
// message:
|
|
474
|
-
// 'Session Not Found or Expired. Please reinitialize with a new session ID.',
|
|
475
|
-
// },
|
|
476
|
-
// id: null,
|
|
477
|
-
// });
|
|
478
|
-
// } catch (err) {
|
|
479
|
-
// logger.error('Error handling request', { error: err });
|
|
480
|
-
// return next(err);
|
|
481
|
-
// }
|
|
482
|
-
// },
|
|
483
|
-
// );
|
|
484
|
-
// GET/DELETE : server-to-client (SSE) and session termination
|
|
485
|
-
// async function handleSessionRequestWasWorking(
|
|
486
|
-
// req: AuthenticatedRequest,
|
|
487
|
-
// res: Response,
|
|
488
|
-
// next: NextFunction,
|
|
489
|
-
// ) {
|
|
490
|
-
// try {
|
|
491
|
-
// // const sessionId = getOrCreateSessionId(req);
|
|
492
|
-
// // if (!sessionId || !sessions.has(sessionId)) {
|
|
493
|
-
// // logger.warn('Invalid or missing session ID', { sessionId });
|
|
494
|
-
// // res.status(404).json({
|
|
495
|
-
// // jsonrpc: '2.0',
|
|
496
|
-
// // error: {
|
|
497
|
-
// // code: 404,
|
|
498
|
-
// // message: 'Session not found. Please reinitialize.',
|
|
499
|
-
// // },
|
|
500
|
-
// // id: null,
|
|
501
|
-
// // });
|
|
502
|
-
// // return;
|
|
503
|
-
// // }
|
|
504
|
-
// // Add SSE-specific headers for GET requests (which are used for SSE)
|
|
505
|
-
// if (req.method === 'GET') {
|
|
506
|
-
// res.setHeader('Content-Type', 'text/event-stream');
|
|
507
|
-
// res.setHeader('Cache-Control', 'no-cache');
|
|
508
|
-
// res.setHeader('Connection', 'keep-alive');
|
|
509
|
-
// res.setHeader('Access-Control-Allow-Origin', '*');
|
|
510
|
-
// res.setHeader('Access-Control-Allow-Headers', 'Cache-Control');
|
|
511
|
-
// res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
512
|
-
// }
|
|
513
|
-
// // const transport = sessions.get(sessionId)!.transport;
|
|
514
|
-
// await sharedTransport.handleRequest(req, res, req.body);
|
|
515
|
-
// // if (req.method === 'DELETE') {
|
|
516
|
-
// // // destroySession(sessionId);
|
|
517
|
-
// // }
|
|
518
|
-
// } catch (err) {
|
|
519
|
-
// logger.error('❌ Error in handleSessionRequest', {
|
|
520
|
-
// error: {
|
|
521
|
-
// message: err instanceof Error ? err.message : String(err),
|
|
522
|
-
// stack: err instanceof Error ? err.stack : undefined,
|
|
523
|
-
// },
|
|
524
|
-
// method: req.method,
|
|
525
|
-
// url: req.url,
|
|
526
|
-
// sessionId: getOrCreateSessionId(req),
|
|
527
|
-
// });
|
|
528
|
-
// next(err);
|
|
529
|
-
// }
|
|
530
|
-
// }
|
|
531
|
-
// app.get('/oauth/authorize', (req, res) => {
|
|
532
|
-
// logger.info('OAuth authorize!!!', {
|
|
533
|
-
// url: req.url,
|
|
534
|
-
// method: req.method,
|
|
535
|
-
// headers: req.headers,
|
|
536
|
-
// });
|
|
537
|
-
// throw new Error('Not implemented');
|
|
538
|
-
// });
|
|
539
|
-
// Error handler middleware
|
|
540
|
-
// app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
|
|
541
|
-
// if (err instanceof HttpError) {
|
|
542
|
-
// res.status(err.status).json({
|
|
543
|
-
// jsonrpc: '2.0',
|
|
544
|
-
// error: { code: err.status, message: err.message },
|
|
545
|
-
// id: null,
|
|
546
|
-
// });
|
|
547
|
-
// return;
|
|
548
|
-
// }
|
|
549
|
-
// logger.error('Unhandled error', {
|
|
550
|
-
// error: { ...err, stack: err.stack },
|
|
551
|
-
// errorMessage: err.message,
|
|
552
|
-
// url: req.url,
|
|
553
|
-
// method: req.method,
|
|
554
|
-
// });
|
|
555
|
-
// if (!res.headersSent) {
|
|
556
|
-
// res.status(500).json({
|
|
557
|
-
// jsonrpc: '2.0',
|
|
558
|
-
// error: { code: -32603, message: 'Internal server error' },
|
|
559
|
-
// id: null,
|
|
560
|
-
// });
|
|
561
|
-
// }
|
|
562
|
-
// });
|
|
563
334
|
// Graceful shutdown
|
|
564
335
|
function shutdown() {
|
|
565
336
|
utils_2.logger.info('Shutting down server...');
|
|
566
337
|
// Clear the heartbeat interval
|
|
567
338
|
clearInterval(heartbeatInterval);
|
|
568
|
-
// Clean up all sessions
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
339
|
+
// Clean up all sessions using the enhanced service
|
|
340
|
+
const totalSessions = session_1.sessionService.getSessionCount();
|
|
341
|
+
utils_2.logger.info('Cleaning up sessions before shutdown', { totalSessions });
|
|
342
|
+
session_1.sessionService.clearAllSessions();
|
|
572
343
|
process.exit(0);
|
|
573
344
|
}
|
|
574
345
|
process.on('SIGINT', shutdown);
|
|
575
346
|
process.on('SIGTERM', shutdown);
|
|
576
347
|
process.once('SIGUSR2', () => {
|
|
577
348
|
utils_2.logger.info('SIGUSR2 received, shutting down...');
|
|
578
|
-
|
|
579
|
-
// After cleanup, re-emit the signal to let nodemon restart the process
|
|
349
|
+
shutdown();
|
|
580
350
|
process.kill(process.pid, 'SIGUSR2');
|
|
581
351
|
});
|
|
582
352
|
// Start server
|
|
583
353
|
const PORT = config_1.env.PORT || 3005;
|
|
584
354
|
const serverInstance = app.listen(PORT, async () => {
|
|
585
|
-
// Connect server once at startup
|
|
586
|
-
// await server.connect(sharedTransport);
|
|
587
355
|
// Initialize Carbon Voice API health status
|
|
588
|
-
const
|
|
356
|
+
const status = await (0, cv_api_1.getCarbonVoiceApiStatus)();
|
|
589
357
|
carbonVoiceApiHealth = {
|
|
590
|
-
|
|
358
|
+
...status,
|
|
591
359
|
lastChecked: new Date().toISOString(),
|
|
592
360
|
};
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
totalSessions: sessions.size,
|
|
361
|
+
// Initialize and start session cleanup service
|
|
362
|
+
const sessionConfig = session_config_1.SessionConfig.fromEnv();
|
|
363
|
+
const sessionLogger = new session_1.SessionLogger();
|
|
364
|
+
const sessionCleanupService = new session_1.SessionCleanupService(session_1.sessionService, sessionConfig, sessionLogger);
|
|
365
|
+
sessionCleanupService.start();
|
|
366
|
+
utils_2.logger.info('🚀 HTTP MCP Server started', {
|
|
367
|
+
port: PORT,
|
|
601
368
|
carbonVoiceApiHealth,
|
|
369
|
+
sessionConfig: {
|
|
370
|
+
ttlMs: sessionConfig.ttlMs,
|
|
371
|
+
maxSessions: sessionConfig.maxSessions,
|
|
372
|
+
cleanupIntervalMs: sessionConfig.cleanupIntervalMs,
|
|
373
|
+
},
|
|
602
374
|
});
|
|
603
375
|
});
|
|
604
376
|
serverInstance.on('error', (error) => {
|
|
@@ -611,7 +383,7 @@ process.on('uncaughtException', (error) => {
|
|
|
611
383
|
stack: error.stack,
|
|
612
384
|
name: error.name,
|
|
613
385
|
processId: process.pid,
|
|
614
|
-
totalSessions:
|
|
386
|
+
totalSessions: session_1.sessionService.getAllSessionIds().length,
|
|
615
387
|
});
|
|
616
388
|
});
|
|
617
389
|
process.on('unhandledRejection', (reason, promise) => {
|
|
@@ -620,42 +392,27 @@ process.on('unhandledRejection', (reason, promise) => {
|
|
|
620
392
|
stack: reason instanceof Error ? reason.stack : undefined,
|
|
621
393
|
promise: promise.toString(),
|
|
622
394
|
processId: process.pid,
|
|
623
|
-
totalSessions:
|
|
395
|
+
totalSessions: session_1.sessionService.getAllSessionIds().length,
|
|
624
396
|
});
|
|
625
397
|
});
|
|
626
398
|
// Log every 30 seconds to show the server is alive
|
|
627
399
|
const heartbeatInterval = setInterval(async () => {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
carbonVoiceApiHealth = {
|
|
646
|
-
isHealthy: false,
|
|
647
|
-
lastChecked: new Date().toISOString(),
|
|
648
|
-
error: error instanceof Error
|
|
649
|
-
? error.message
|
|
650
|
-
: 'Unknown error checking API health',
|
|
651
|
-
};
|
|
652
|
-
utils_2.logger.warn('💓 Server heartbeat - Carbon Voice API check failed', {
|
|
653
|
-
totalSessions: sessions.size,
|
|
654
|
-
isCarbonVoiceApiWorking: false,
|
|
655
|
-
uptime: (0, utils_2.formatProcessUptime)(),
|
|
656
|
-
memoryUsage: process.memoryUsage(),
|
|
657
|
-
error: carbonVoiceApiHealth.error,
|
|
658
|
-
});
|
|
659
|
-
}
|
|
400
|
+
const status = await (0, cv_api_1.getCarbonVoiceApiStatus)();
|
|
401
|
+
// Update Carbon Voice API health cache
|
|
402
|
+
carbonVoiceApiHealth = {
|
|
403
|
+
isHealthy: status.isHealthy,
|
|
404
|
+
apiUrl: status.apiUrl,
|
|
405
|
+
lastChecked: new Date().toISOString(),
|
|
406
|
+
...(status.error && { error: status.error }),
|
|
407
|
+
};
|
|
408
|
+
const icon = carbonVoiceApiHealth.isHealthy ? '🟢' : '🔴';
|
|
409
|
+
const logArgs = {
|
|
410
|
+
totalSessions: session_1.sessionService.getAllSessionIds().length,
|
|
411
|
+
isCarbonVoiceApiWorking: carbonVoiceApiHealth.isHealthy,
|
|
412
|
+
uptime: (0, utils_2.getProcessUptime)(),
|
|
413
|
+
memoryUsage: process.memoryUsage(),
|
|
414
|
+
carbonVoiceApiHealth,
|
|
415
|
+
};
|
|
416
|
+
utils_2.logger.info(`💓 Server heartbeat ${icon}`, logArgs);
|
|
660
417
|
}, 30000);
|
|
661
418
|
//# sourceMappingURL=streamable.js.map
|