@carbonvoice/cv-mcp-server 1.1.2 → 2.0.1
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 +30 -30
- 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/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 +77 -0
- package/dist/transports/http/session/session.logger.js.map +1 -0
- package/dist/transports/http/session/session.service.js +192 -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 +172 -422
- 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 +7 -10
- package/dist/utils/axios-instance.js.map +1 -1
- package/dist/utils/logger.js +34 -11
- package/dist/utils/logger.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +253 -11
- package/dist/auth/auth-middleware.js +0 -203
- package/dist/auth/auth-middleware.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']);
|
|
@@ -74,65 +43,8 @@ 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',
|
|
@@ -177,108 +89,136 @@ 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
151
|
var _a, _b, _c, _d, _e, _f;
|
|
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
|
+
session_1.sessionService.recordInteraction(sessionId);
|
|
167
|
+
await handleSessionRequest(req, res, session);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
235
170
|
}
|
|
236
171
|
// Create New Session
|
|
237
172
|
if ((0, types_js_1.isInitializeRequest)(req.body)) {
|
|
238
173
|
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
239
174
|
sessionIdGenerator: () => (0, utils_1.getOrCreateSessionId)(req),
|
|
240
|
-
// sessionIdGenerator: undefined,
|
|
241
175
|
onsessioninitialized: (sessionId) => {
|
|
242
|
-
// const user = req.auth?.extra?.user;
|
|
243
176
|
var _a, _b, _c;
|
|
244
|
-
utils_2.logger.info('
|
|
177
|
+
utils_2.logger.info('New session initialized', {
|
|
245
178
|
sessionId,
|
|
246
|
-
requestorHeaders: (0, utils_2.obfuscateAuthHeaders)(req.headers),
|
|
247
179
|
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
180
|
});
|
|
249
|
-
|
|
181
|
+
try {
|
|
182
|
+
session_1.sessionService.createSession(transport, req, sessionId);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
utils_2.logger.error('Failed to create session', {
|
|
186
|
+
sessionId,
|
|
187
|
+
error: error instanceof Error ? error.message : String(error),
|
|
188
|
+
});
|
|
189
|
+
// Close transport if session creation fails
|
|
190
|
+
transport.close();
|
|
191
|
+
}
|
|
250
192
|
},
|
|
251
193
|
enableJsonResponse: true,
|
|
252
194
|
});
|
|
253
195
|
transport.onclose = () => {
|
|
254
|
-
utils_2.logger.
|
|
196
|
+
utils_2.logger.debug('Transport closed', {
|
|
255
197
|
sessionId: transport.sessionId,
|
|
256
|
-
hasSessionId: !!transport.sessionId,
|
|
257
198
|
});
|
|
258
|
-
if (transport.sessionId)
|
|
259
|
-
destroySession(transport.sessionId);
|
|
199
|
+
if (transport.sessionId) {
|
|
200
|
+
session_1.sessionService.destroySession(transport.sessionId);
|
|
201
|
+
}
|
|
260
202
|
};
|
|
261
203
|
// Add error handling for the transport
|
|
262
204
|
transport.onerror = (error) => {
|
|
263
|
-
|
|
264
|
-
utils_2.logger.error('🚨 Transport error occurred', {
|
|
205
|
+
utils_2.logger.error('Transport error', {
|
|
265
206
|
sessionId: transport.sessionId,
|
|
266
|
-
sessionMetrics: (_a = sessions.get(transport.sessionId)) === null || _a === void 0 ? void 0 : _a.metrics,
|
|
267
207
|
error: {
|
|
268
208
|
message: error.message,
|
|
269
209
|
name: error.name,
|
|
270
|
-
stack: error.stack,
|
|
271
210
|
},
|
|
272
211
|
});
|
|
212
|
+
// Record error in session metrics
|
|
213
|
+
if (transport.sessionId) {
|
|
214
|
+
session_1.sessionService.recordError(transport.sessionId);
|
|
215
|
+
}
|
|
273
216
|
};
|
|
274
217
|
await server_1.default.connect(transport);
|
|
275
218
|
await transport.handleRequest(req, res, req.body);
|
|
276
219
|
return;
|
|
277
|
-
// const session = sessions.get(transport!.sessionId!);
|
|
278
|
-
// await handleSessionRequest(req, res, session!);
|
|
279
220
|
}
|
|
280
221
|
// No session ID and no initialize request
|
|
281
|
-
// Should never happen since we are gonna always have a session ID
|
|
282
222
|
utils_2.logger.warn('❌ Session ID not found', {
|
|
283
223
|
sessionId,
|
|
284
224
|
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,
|
|
@@ -292,9 +232,6 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
|
|
|
292
232
|
},
|
|
293
233
|
id: null,
|
|
294
234
|
});
|
|
295
|
-
return;
|
|
296
|
-
// await handleSessionRequest(req, res, sessions.get(sessionId));
|
|
297
|
-
// await sharedTransport.handleRequest(req, res, req.body);
|
|
298
235
|
}
|
|
299
236
|
catch (error) {
|
|
300
237
|
utils_2.logger.error('❌ Error in POST handler', {
|
|
@@ -312,11 +249,21 @@ app.post('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
|
|
|
312
249
|
}
|
|
313
250
|
});
|
|
314
251
|
// GET/DELETE : server-to-client (SSE) and session termination
|
|
252
|
+
app.get('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
|
|
253
|
+
verifier: (0, auth_1.createOAuthTokenVerifier)(),
|
|
254
|
+
requiredScopes: constants_1.REQUIRED_SCOPES,
|
|
255
|
+
resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
|
|
256
|
+
}), _1.addMcpSessionId, handleSessionRequestGetDelete);
|
|
257
|
+
app.delete('/', _1.authErrorLogger, (0, bearerAuth_js_1.requireBearerAuth)({
|
|
258
|
+
verifier: (0, auth_1.createOAuthTokenVerifier)(),
|
|
259
|
+
requiredScopes: constants_1.REQUIRED_SCOPES,
|
|
260
|
+
resourceMetadataUrl: 'mcp', // FIXME: Add dynamic resource!
|
|
261
|
+
}), _1.addMcpSessionId, handleSessionRequestGetDelete);
|
|
315
262
|
// GET/DELETE : server-to-client (SSE) and session termination
|
|
316
263
|
async function handleSessionRequestGetDelete(req, res) {
|
|
317
264
|
try {
|
|
318
265
|
const sessionId = (0, utils_1.getOrCreateSessionId)(req);
|
|
319
|
-
const session =
|
|
266
|
+
const session = session_1.sessionService.getSession(sessionId);
|
|
320
267
|
if (!session) {
|
|
321
268
|
utils_2.logger.warn('Invalid or missing session ID', { sessionId });
|
|
322
269
|
res.status(404).json({
|
|
@@ -329,276 +276,94 @@ async function handleSessionRequestGetDelete(req, res) {
|
|
|
329
276
|
});
|
|
330
277
|
return;
|
|
331
278
|
}
|
|
279
|
+
// Check if session is expired
|
|
280
|
+
if (session_1.sessionService.isSessionExpired(sessionId)) {
|
|
281
|
+
utils_2.logger.warn('Session expired, destroying', { sessionId });
|
|
282
|
+
session_1.sessionService.destroySession(sessionId);
|
|
283
|
+
res.status(404).json({
|
|
284
|
+
jsonrpc: '2.0',
|
|
285
|
+
error: {
|
|
286
|
+
code: 404,
|
|
287
|
+
message: 'Session expired. Please reinitialize.',
|
|
288
|
+
},
|
|
289
|
+
id: null,
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// Record interaction for metrics
|
|
294
|
+
session_1.sessionService.recordInteraction(sessionId);
|
|
332
295
|
await session.transport.handleRequest(req, res, req.body);
|
|
333
296
|
if (req.method === 'DELETE') {
|
|
334
|
-
destroySession(sessionId);
|
|
297
|
+
session_1.sessionService.destroySession(sessionId);
|
|
335
298
|
}
|
|
336
299
|
}
|
|
337
300
|
catch (err) {
|
|
301
|
+
const sessionId = (0, utils_1.getOrCreateSessionId)(req);
|
|
302
|
+
// Record error in session metrics
|
|
303
|
+
if (sessionId) {
|
|
304
|
+
session_1.sessionService.recordError(sessionId);
|
|
305
|
+
}
|
|
338
306
|
utils_2.logger.error('❌ Error in handleSessionRequestGetDelete', {
|
|
339
307
|
error: {
|
|
340
308
|
message: err instanceof Error ? err.message : String(err),
|
|
341
309
|
stack: err instanceof Error ? err.stack : undefined,
|
|
342
310
|
},
|
|
311
|
+
method: req.method,
|
|
312
|
+
url: req.url,
|
|
313
|
+
sessionId,
|
|
343
314
|
});
|
|
315
|
+
if (!res.headersSent) {
|
|
316
|
+
res.status(500).json({
|
|
317
|
+
jsonrpc: '2.0',
|
|
318
|
+
error: {
|
|
319
|
+
code: -32603,
|
|
320
|
+
message: 'Internal server error',
|
|
321
|
+
},
|
|
322
|
+
id: null,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
344
325
|
}
|
|
345
326
|
}
|
|
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
327
|
// Graceful shutdown
|
|
564
328
|
function shutdown() {
|
|
565
329
|
utils_2.logger.info('Shutting down server...');
|
|
566
330
|
// Clear the heartbeat interval
|
|
567
331
|
clearInterval(heartbeatInterval);
|
|
568
|
-
// Clean up all sessions
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
332
|
+
// Clean up all sessions using the enhanced service
|
|
333
|
+
const totalSessions = session_1.sessionService.getSessionCount();
|
|
334
|
+
utils_2.logger.info('Cleaning up sessions before shutdown', { totalSessions });
|
|
335
|
+
session_1.sessionService.clearAllSessions();
|
|
572
336
|
process.exit(0);
|
|
573
337
|
}
|
|
574
338
|
process.on('SIGINT', shutdown);
|
|
575
339
|
process.on('SIGTERM', shutdown);
|
|
576
340
|
process.once('SIGUSR2', () => {
|
|
577
341
|
utils_2.logger.info('SIGUSR2 received, shutting down...');
|
|
578
|
-
|
|
579
|
-
// After cleanup, re-emit the signal to let nodemon restart the process
|
|
342
|
+
shutdown();
|
|
580
343
|
process.kill(process.pid, 'SIGUSR2');
|
|
581
344
|
});
|
|
582
345
|
// Start server
|
|
583
346
|
const PORT = config_1.env.PORT || 3005;
|
|
584
347
|
const serverInstance = app.listen(PORT, async () => {
|
|
585
|
-
// Connect server once at startup
|
|
586
|
-
// await server.connect(sharedTransport);
|
|
587
348
|
// Initialize Carbon Voice API health status
|
|
588
|
-
const
|
|
349
|
+
const status = await (0, cv_api_1.getCarbonVoiceApiStatus)();
|
|
589
350
|
carbonVoiceApiHealth = {
|
|
590
|
-
|
|
351
|
+
...status,
|
|
591
352
|
lastChecked: new Date().toISOString(),
|
|
592
353
|
};
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
totalSessions: sessions.size,
|
|
354
|
+
// Initialize and start session cleanup service
|
|
355
|
+
const sessionConfig = session_config_1.SessionConfig.fromEnv();
|
|
356
|
+
const sessionLogger = new session_1.SessionLogger();
|
|
357
|
+
const sessionCleanupService = new session_1.SessionCleanupService(session_1.sessionService, sessionConfig, sessionLogger);
|
|
358
|
+
sessionCleanupService.start();
|
|
359
|
+
utils_2.logger.info('🚀 HTTP MCP Server started', {
|
|
360
|
+
port: PORT,
|
|
601
361
|
carbonVoiceApiHealth,
|
|
362
|
+
sessionConfig: {
|
|
363
|
+
ttlMs: sessionConfig.ttlMs,
|
|
364
|
+
maxSessions: sessionConfig.maxSessions,
|
|
365
|
+
cleanupIntervalMs: sessionConfig.cleanupIntervalMs,
|
|
366
|
+
},
|
|
602
367
|
});
|
|
603
368
|
});
|
|
604
369
|
serverInstance.on('error', (error) => {
|
|
@@ -611,7 +376,7 @@ process.on('uncaughtException', (error) => {
|
|
|
611
376
|
stack: error.stack,
|
|
612
377
|
name: error.name,
|
|
613
378
|
processId: process.pid,
|
|
614
|
-
totalSessions:
|
|
379
|
+
totalSessions: session_1.sessionService.getAllSessionIds().length,
|
|
615
380
|
});
|
|
616
381
|
});
|
|
617
382
|
process.on('unhandledRejection', (reason, promise) => {
|
|
@@ -620,42 +385,27 @@ process.on('unhandledRejection', (reason, promise) => {
|
|
|
620
385
|
stack: reason instanceof Error ? reason.stack : undefined,
|
|
621
386
|
promise: promise.toString(),
|
|
622
387
|
processId: process.pid,
|
|
623
|
-
totalSessions:
|
|
388
|
+
totalSessions: session_1.sessionService.getAllSessionIds().length,
|
|
624
389
|
});
|
|
625
390
|
});
|
|
626
391
|
// Log every 30 seconds to show the server is alive
|
|
627
392
|
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
|
-
}
|
|
393
|
+
const status = await (0, cv_api_1.getCarbonVoiceApiStatus)();
|
|
394
|
+
// Update Carbon Voice API health cache
|
|
395
|
+
carbonVoiceApiHealth = {
|
|
396
|
+
isHealthy: status.isHealthy,
|
|
397
|
+
apiUrl: status.apiUrl,
|
|
398
|
+
lastChecked: new Date().toISOString(),
|
|
399
|
+
...(status.error && { error: status.error }),
|
|
400
|
+
};
|
|
401
|
+
const icon = carbonVoiceApiHealth.isHealthy ? '🟢' : '🔴';
|
|
402
|
+
const logArgs = {
|
|
403
|
+
totalSessions: session_1.sessionService.getAllSessionIds().length,
|
|
404
|
+
isCarbonVoiceApiWorking: carbonVoiceApiHealth.isHealthy,
|
|
405
|
+
uptime: (0, utils_2.formatProcessUptime)(),
|
|
406
|
+
memoryUsage: process.memoryUsage(),
|
|
407
|
+
carbonVoiceApiHealth,
|
|
408
|
+
};
|
|
409
|
+
utils_2.logger.info(`💓 Server heartbeat ${icon}`, logArgs);
|
|
660
410
|
}, 30000);
|
|
661
411
|
//# sourceMappingURL=streamable.js.map
|