n8n-mcp 2.19.4 → 2.19.6
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/README.md +1 -1
- package/data/nodes.db +0 -0
- package/dist/http-server-single-session.d.ts +1 -28
- package/dist/http-server-single-session.d.ts.map +1 -1
- package/dist/http-server-single-session.js +33 -707
- package/dist/http-server-single-session.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +9 -19
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp-engine.d.ts +0 -17
- package/dist/mcp-engine.d.ts.map +1 -1
- package/dist/mcp-engine.js +3 -18
- package/dist/mcp-engine.js.map +1 -1
- package/package.json +10 -10
- package/dist/types/session-restoration.d.ts +0 -25
- package/dist/types/session-restoration.d.ts.map +0 -1
- package/dist/types/session-restoration.js +0 -3
- package/dist/types/session-restoration.js.map +0 -1
|
@@ -35,7 +35,7 @@ function extractMultiTenantHeaders(req) {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
class SingleSessionHTTPServer {
|
|
38
|
-
constructor(
|
|
38
|
+
constructor() {
|
|
39
39
|
this.transports = {};
|
|
40
40
|
this.servers = {};
|
|
41
41
|
this.sessionMetadata = {};
|
|
@@ -46,17 +46,7 @@ class SingleSessionHTTPServer {
|
|
|
46
46
|
this.sessionTimeout = 30 * 60 * 1000;
|
|
47
47
|
this.authToken = null;
|
|
48
48
|
this.cleanupTimer = null;
|
|
49
|
-
this.cleanupInProgress = new Set();
|
|
50
|
-
this.isShuttingDown = false;
|
|
51
49
|
this.validateEnvironment();
|
|
52
|
-
this.onSessionNotFound = options.onSessionNotFound;
|
|
53
|
-
this.sessionRestorationTimeout = options.sessionRestorationTimeout || 5000;
|
|
54
|
-
this.sessionEvents = options.sessionEvents;
|
|
55
|
-
this.sessionRestorationRetries = options.sessionRestorationRetries ?? 0;
|
|
56
|
-
this.sessionRestorationRetryDelay = options.sessionRestorationRetryDelay || 100;
|
|
57
|
-
if (options.sessionTimeout) {
|
|
58
|
-
this.sessionTimeout = options.sessionTimeout;
|
|
59
|
-
}
|
|
60
50
|
this.startSessionCleanup();
|
|
61
51
|
}
|
|
62
52
|
startSessionCleanup() {
|
|
@@ -74,7 +64,7 @@ class SingleSessionHTTPServer {
|
|
|
74
64
|
sessionTimeout: this.sessionTimeout / 1000 / 60
|
|
75
65
|
});
|
|
76
66
|
}
|
|
77
|
-
|
|
67
|
+
cleanupExpiredSessions() {
|
|
78
68
|
const now = Date.now();
|
|
79
69
|
const expiredSessions = [];
|
|
80
70
|
for (const sessionId in this.sessionMetadata) {
|
|
@@ -89,100 +79,29 @@ class SingleSessionHTTPServer {
|
|
|
89
79
|
logger_1.logger.debug('Cleaned orphaned session context', { sessionId });
|
|
90
80
|
}
|
|
91
81
|
}
|
|
92
|
-
for (const sessionId in this.transports) {
|
|
93
|
-
if (!this.sessionMetadata[sessionId]) {
|
|
94
|
-
logger_1.logger.warn('Orphaned transport detected, cleaning up', { sessionId });
|
|
95
|
-
try {
|
|
96
|
-
await this.removeSession(sessionId, 'orphaned_transport');
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
logger_1.logger.error('Error cleaning orphaned transport', {
|
|
100
|
-
sessionId,
|
|
101
|
-
error: err instanceof Error ? err.message : String(err)
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
for (const sessionId in this.servers) {
|
|
107
|
-
if (!this.sessionMetadata[sessionId]) {
|
|
108
|
-
logger_1.logger.warn('Orphaned server detected, cleaning up', { sessionId });
|
|
109
|
-
delete this.servers[sessionId];
|
|
110
|
-
logger_1.logger.debug('Cleaned orphaned server', { sessionId });
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
let successCount = 0;
|
|
114
|
-
let failureCount = 0;
|
|
115
82
|
for (const sessionId of expiredSessions) {
|
|
116
|
-
|
|
117
|
-
await this.emitEvent('onSessionExpired', sessionId);
|
|
118
|
-
await this.removeSession(sessionId, 'expired');
|
|
119
|
-
successCount++;
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
failureCount++;
|
|
123
|
-
logger_1.logger.error('Failed to cleanup expired session (isolated)', {
|
|
124
|
-
sessionId,
|
|
125
|
-
error: error instanceof Error ? error.message : String(error),
|
|
126
|
-
stack: error instanceof Error ? error.stack : undefined
|
|
127
|
-
});
|
|
128
|
-
}
|
|
83
|
+
this.removeSession(sessionId, 'expired');
|
|
129
84
|
}
|
|
130
85
|
if (expiredSessions.length > 0) {
|
|
131
|
-
logger_1.logger.info('
|
|
132
|
-
|
|
133
|
-
successful: successCount,
|
|
134
|
-
failed: failureCount,
|
|
86
|
+
logger_1.logger.info('Cleaned up expired sessions', {
|
|
87
|
+
removed: expiredSessions.length,
|
|
135
88
|
remaining: this.getActiveSessionCount()
|
|
136
89
|
});
|
|
137
90
|
}
|
|
138
91
|
}
|
|
139
|
-
async safeCloseTransport(sessionId) {
|
|
140
|
-
const transport = this.transports[sessionId];
|
|
141
|
-
if (!transport)
|
|
142
|
-
return;
|
|
143
|
-
try {
|
|
144
|
-
transport.onclose = undefined;
|
|
145
|
-
transport.onerror = undefined;
|
|
146
|
-
const closePromise = transport.close();
|
|
147
|
-
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Transport close timeout')), 3000));
|
|
148
|
-
await Promise.race([closePromise, timeoutPromise]);
|
|
149
|
-
logger_1.logger.debug('Transport closed safely', { sessionId });
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
logger_1.logger.warn('Transport close error (non-fatal)', {
|
|
153
|
-
sessionId,
|
|
154
|
-
error: error instanceof Error ? error.message : String(error)
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
92
|
async removeSession(sessionId, reason) {
|
|
159
|
-
if (this.cleanupInProgress.has(sessionId)) {
|
|
160
|
-
logger_1.logger.debug('Cleanup already in progress, skipping duplicate', {
|
|
161
|
-
sessionId,
|
|
162
|
-
reason
|
|
163
|
-
});
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
this.cleanupInProgress.add(sessionId);
|
|
167
93
|
try {
|
|
168
94
|
if (this.transports[sessionId]) {
|
|
169
|
-
await this.
|
|
95
|
+
await this.transports[sessionId].close();
|
|
170
96
|
delete this.transports[sessionId];
|
|
171
97
|
}
|
|
172
98
|
delete this.servers[sessionId];
|
|
173
99
|
delete this.sessionMetadata[sessionId];
|
|
174
100
|
delete this.sessionContexts[sessionId];
|
|
175
|
-
logger_1.logger.info('Session removed
|
|
101
|
+
logger_1.logger.info('Session removed', { sessionId, reason });
|
|
176
102
|
}
|
|
177
103
|
catch (error) {
|
|
178
|
-
logger_1.logger.warn('Error
|
|
179
|
-
sessionId,
|
|
180
|
-
reason,
|
|
181
|
-
error: error instanceof Error ? error.message : String(error)
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
finally {
|
|
185
|
-
this.cleanupInProgress.delete(sessionId);
|
|
104
|
+
logger_1.logger.warn('Error removing session', { sessionId, reason, error });
|
|
186
105
|
}
|
|
187
106
|
}
|
|
188
107
|
getActiveSessionCount() {
|
|
@@ -192,16 +111,7 @@ class SingleSessionHTTPServer {
|
|
|
192
111
|
return this.getActiveSessionCount() < MAX_SESSIONS;
|
|
193
112
|
}
|
|
194
113
|
isValidSessionId(sessionId) {
|
|
195
|
-
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(sessionId)) {
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
if (sessionId.length > 100) {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
return true;
|
|
114
|
+
return Boolean(sessionId && sessionId.length > 0);
|
|
205
115
|
}
|
|
206
116
|
sanitizeErrorForClient(error) {
|
|
207
117
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
@@ -228,12 +138,6 @@ class SingleSessionHTTPServer {
|
|
|
228
138
|
updateSessionAccess(sessionId) {
|
|
229
139
|
if (this.sessionMetadata[sessionId]) {
|
|
230
140
|
this.sessionMetadata[sessionId].lastAccess = new Date();
|
|
231
|
-
this.emitEvent('onSessionAccessed', sessionId).catch(err => {
|
|
232
|
-
logger_1.logger.error('Failed to emit onSessionAccessed event (non-blocking)', {
|
|
233
|
-
sessionId,
|
|
234
|
-
error: err instanceof Error ? err.message : String(err)
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
141
|
}
|
|
238
142
|
}
|
|
239
143
|
async switchSessionContext(sessionId, newContext) {
|
|
@@ -265,274 +169,6 @@ class SingleSessionHTTPServer {
|
|
|
265
169
|
}
|
|
266
170
|
}
|
|
267
171
|
}
|
|
268
|
-
timeout(ms) {
|
|
269
|
-
return new Promise((_, reject) => {
|
|
270
|
-
setTimeout(() => {
|
|
271
|
-
const error = new Error(`Operation timed out after ${ms}ms`);
|
|
272
|
-
error.name = 'TimeoutError';
|
|
273
|
-
reject(error);
|
|
274
|
-
}, ms);
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
async emitEvent(eventName, ...args) {
|
|
278
|
-
const handler = this.sessionEvents?.[eventName];
|
|
279
|
-
if (!handler)
|
|
280
|
-
return;
|
|
281
|
-
try {
|
|
282
|
-
await Promise.resolve(handler(...args));
|
|
283
|
-
}
|
|
284
|
-
catch (error) {
|
|
285
|
-
logger_1.logger.error(`Session event handler failed: ${eventName}`, {
|
|
286
|
-
error: error instanceof Error ? error.message : String(error),
|
|
287
|
-
sessionId: args[0]
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
async initializeMCPServerForSession(sessionId, server, instanceContext) {
|
|
292
|
-
const initStartTime = Date.now();
|
|
293
|
-
const initTimeout = 5000;
|
|
294
|
-
try {
|
|
295
|
-
logger_1.logger.info('Initializing MCP server for restored session', {
|
|
296
|
-
sessionId,
|
|
297
|
-
instanceId: instanceContext?.instanceId
|
|
298
|
-
});
|
|
299
|
-
const initializeRequest = {
|
|
300
|
-
jsonrpc: '2.0',
|
|
301
|
-
id: `init-${sessionId}`,
|
|
302
|
-
method: 'initialize',
|
|
303
|
-
params: {
|
|
304
|
-
protocolVersion: protocol_version_1.STANDARD_PROTOCOL_VERSION,
|
|
305
|
-
capabilities: {
|
|
306
|
-
tools: {}
|
|
307
|
-
},
|
|
308
|
-
clientInfo: {
|
|
309
|
-
name: 'n8n-mcp-restored-session',
|
|
310
|
-
version: version_1.PROJECT_VERSION
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
const initPromise = server.server.request(initializeRequest, types_js_1.InitializeRequestSchema);
|
|
315
|
-
const timeoutPromise = this.timeout(initTimeout);
|
|
316
|
-
const response = await Promise.race([initPromise, timeoutPromise]);
|
|
317
|
-
const duration = Date.now() - initStartTime;
|
|
318
|
-
logger_1.logger.info('MCP server initialized successfully for restored session', {
|
|
319
|
-
sessionId,
|
|
320
|
-
duration: `${duration}ms`,
|
|
321
|
-
protocolVersion: response.protocolVersion
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
const duration = Date.now() - initStartTime;
|
|
326
|
-
if (error instanceof Error && error.name === 'TimeoutError') {
|
|
327
|
-
logger_1.logger.error('MCP server initialization timeout for restored session', {
|
|
328
|
-
sessionId,
|
|
329
|
-
timeout: initTimeout,
|
|
330
|
-
duration: `${duration}ms`
|
|
331
|
-
});
|
|
332
|
-
throw new Error(`MCP server initialization timeout after ${initTimeout}ms`);
|
|
333
|
-
}
|
|
334
|
-
logger_1.logger.error('MCP server initialization failed for restored session', {
|
|
335
|
-
sessionId,
|
|
336
|
-
error: error instanceof Error ? error.message : String(error),
|
|
337
|
-
duration: `${duration}ms`
|
|
338
|
-
});
|
|
339
|
-
throw new Error(`MCP server initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
async restoreSessionWithRetry(sessionId) {
|
|
343
|
-
if (!this.onSessionNotFound) {
|
|
344
|
-
throw new Error('onSessionNotFound hook not configured');
|
|
345
|
-
}
|
|
346
|
-
const maxRetries = this.sessionRestorationRetries;
|
|
347
|
-
const retryDelay = this.sessionRestorationRetryDelay;
|
|
348
|
-
const overallTimeout = this.sessionRestorationTimeout;
|
|
349
|
-
const startTime = Date.now();
|
|
350
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
351
|
-
try {
|
|
352
|
-
const remainingTime = overallTimeout - (Date.now() - startTime);
|
|
353
|
-
if (remainingTime <= 0) {
|
|
354
|
-
const error = new Error(`Session restoration timed out after ${overallTimeout}ms`);
|
|
355
|
-
error.name = 'TimeoutError';
|
|
356
|
-
throw error;
|
|
357
|
-
}
|
|
358
|
-
if (attempt > 0) {
|
|
359
|
-
logger_1.logger.debug('Retrying session restoration', {
|
|
360
|
-
sessionId,
|
|
361
|
-
attempt: attempt,
|
|
362
|
-
maxRetries: maxRetries,
|
|
363
|
-
remainingTime: remainingTime + 'ms'
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
const context = await Promise.race([
|
|
367
|
-
this.onSessionNotFound(sessionId),
|
|
368
|
-
this.timeout(remainingTime)
|
|
369
|
-
]);
|
|
370
|
-
if (attempt > 0) {
|
|
371
|
-
logger_1.logger.info('Session restoration succeeded after retry', {
|
|
372
|
-
sessionId,
|
|
373
|
-
attempts: attempt + 1
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
return context;
|
|
377
|
-
}
|
|
378
|
-
catch (error) {
|
|
379
|
-
if (error instanceof Error && error.name === 'TimeoutError') {
|
|
380
|
-
logger_1.logger.error('Session restoration timeout (no retry)', {
|
|
381
|
-
sessionId,
|
|
382
|
-
timeout: overallTimeout
|
|
383
|
-
});
|
|
384
|
-
throw error;
|
|
385
|
-
}
|
|
386
|
-
if (attempt === maxRetries) {
|
|
387
|
-
logger_1.logger.error('Session restoration failed after all retries', {
|
|
388
|
-
sessionId,
|
|
389
|
-
attempts: attempt + 1,
|
|
390
|
-
error: error instanceof Error ? error.message : String(error)
|
|
391
|
-
});
|
|
392
|
-
throw error;
|
|
393
|
-
}
|
|
394
|
-
logger_1.logger.warn('Session restoration failed, will retry', {
|
|
395
|
-
sessionId,
|
|
396
|
-
attempt: attempt + 1,
|
|
397
|
-
maxRetries: maxRetries,
|
|
398
|
-
error: error instanceof Error ? error.message : String(error),
|
|
399
|
-
nextRetryIn: retryDelay + 'ms'
|
|
400
|
-
});
|
|
401
|
-
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
throw new Error('Unexpected state in restoreSessionWithRetry');
|
|
405
|
-
}
|
|
406
|
-
createSession(instanceContext, sessionId, waitForConnection = false) {
|
|
407
|
-
const id = sessionId || this.generateSessionId(instanceContext);
|
|
408
|
-
if (this.transports[id]) {
|
|
409
|
-
logger_1.logger.debug('Session already exists, skipping creation (idempotent)', {
|
|
410
|
-
sessionId: id
|
|
411
|
-
});
|
|
412
|
-
return waitForConnection ? Promise.resolve(id) : id;
|
|
413
|
-
}
|
|
414
|
-
if (sessionId && !this.isValidSessionId(sessionId)) {
|
|
415
|
-
logger_1.logger.error('Invalid session ID format during creation', { sessionId });
|
|
416
|
-
throw new Error('Invalid session ID format');
|
|
417
|
-
}
|
|
418
|
-
if (!this.sessionMetadata[id]) {
|
|
419
|
-
this.sessionMetadata[id] = {
|
|
420
|
-
lastAccess: new Date(),
|
|
421
|
-
createdAt: new Date()
|
|
422
|
-
};
|
|
423
|
-
this.sessionContexts[id] = instanceContext;
|
|
424
|
-
}
|
|
425
|
-
const server = new server_1.N8NDocumentationMCPServer(instanceContext);
|
|
426
|
-
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
427
|
-
sessionIdGenerator: () => id,
|
|
428
|
-
onsessioninitialized: (initializedSessionId) => {
|
|
429
|
-
logger_1.logger.info('Session initialized during explicit creation', {
|
|
430
|
-
sessionId: initializedSessionId
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
this.transports[id] = transport;
|
|
435
|
-
this.servers[id] = server;
|
|
436
|
-
transport.onclose = () => {
|
|
437
|
-
if (transport.sessionId) {
|
|
438
|
-
if (this.isShuttingDown) {
|
|
439
|
-
logger_1.logger.debug('Ignoring transport close event during shutdown', {
|
|
440
|
-
sessionId: transport.sessionId
|
|
441
|
-
});
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
logger_1.logger.info('Transport closed during createSession, cleaning up', {
|
|
445
|
-
sessionId: transport.sessionId
|
|
446
|
-
});
|
|
447
|
-
this.removeSession(transport.sessionId, 'transport_closed').catch(err => {
|
|
448
|
-
logger_1.logger.error('Error during transport close cleanup', {
|
|
449
|
-
sessionId: transport.sessionId,
|
|
450
|
-
error: err instanceof Error ? err.message : String(err)
|
|
451
|
-
});
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
transport.onerror = (error) => {
|
|
456
|
-
if (transport.sessionId) {
|
|
457
|
-
if (this.isShuttingDown) {
|
|
458
|
-
logger_1.logger.debug('Ignoring transport error event during shutdown', {
|
|
459
|
-
sessionId: transport.sessionId
|
|
460
|
-
});
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
logger_1.logger.error('Transport error during createSession', {
|
|
464
|
-
sessionId: transport.sessionId,
|
|
465
|
-
error: error.message
|
|
466
|
-
});
|
|
467
|
-
this.removeSession(transport.sessionId, 'transport_error').catch(err => {
|
|
468
|
-
logger_1.logger.error('Error during transport error cleanup', { error: err });
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
const initializeSession = async () => {
|
|
473
|
-
try {
|
|
474
|
-
await server.initialized;
|
|
475
|
-
await server.connect(transport);
|
|
476
|
-
if (waitForConnection) {
|
|
477
|
-
logger_1.logger.info('Session created and connected successfully', {
|
|
478
|
-
sessionId: id,
|
|
479
|
-
hasInstanceContext: !!instanceContext,
|
|
480
|
-
instanceId: instanceContext?.instanceId
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
else {
|
|
484
|
-
logger_1.logger.info('Session created successfully (connecting server to transport)', {
|
|
485
|
-
sessionId: id,
|
|
486
|
-
hasInstanceContext: !!instanceContext,
|
|
487
|
-
instanceId: instanceContext?.instanceId
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
catch (err) {
|
|
492
|
-
logger_1.logger.error('Failed to connect server to transport in createSession', {
|
|
493
|
-
sessionId: id,
|
|
494
|
-
error: err instanceof Error ? err.message : String(err),
|
|
495
|
-
waitForConnection
|
|
496
|
-
});
|
|
497
|
-
await this.removeSession(id, 'connection_failed').catch(cleanupErr => {
|
|
498
|
-
logger_1.logger.error('Error during connection failure cleanup', { error: cleanupErr });
|
|
499
|
-
});
|
|
500
|
-
throw err;
|
|
501
|
-
}
|
|
502
|
-
this.emitEvent('onSessionCreated', id, instanceContext).catch(eventErr => {
|
|
503
|
-
logger_1.logger.error('Failed to emit onSessionCreated event (non-blocking)', {
|
|
504
|
-
sessionId: id,
|
|
505
|
-
error: eventErr instanceof Error ? eventErr.message : String(eventErr)
|
|
506
|
-
});
|
|
507
|
-
});
|
|
508
|
-
return id;
|
|
509
|
-
};
|
|
510
|
-
if (waitForConnection) {
|
|
511
|
-
return initializeSession();
|
|
512
|
-
}
|
|
513
|
-
initializeSession().catch(error => {
|
|
514
|
-
logger_1.logger.error('Async session creation failed in manual restore flow', {
|
|
515
|
-
sessionId: id,
|
|
516
|
-
error: error instanceof Error ? error.message : String(error)
|
|
517
|
-
});
|
|
518
|
-
});
|
|
519
|
-
return id;
|
|
520
|
-
}
|
|
521
|
-
generateSessionId(instanceContext) {
|
|
522
|
-
const isMultiTenantEnabled = process.env.ENABLE_MULTI_TENANT === 'true';
|
|
523
|
-
const sessionStrategy = process.env.MULTI_TENANT_SESSION_STRATEGY || 'instance';
|
|
524
|
-
if (isMultiTenantEnabled && sessionStrategy === 'instance' && instanceContext?.instanceId) {
|
|
525
|
-
const configHash = (0, crypto_1.createHash)('sha256')
|
|
526
|
-
.update(JSON.stringify({
|
|
527
|
-
url: instanceContext.n8nApiUrl,
|
|
528
|
-
instanceId: instanceContext.instanceId
|
|
529
|
-
}))
|
|
530
|
-
.digest('hex')
|
|
531
|
-
.substring(0, 8);
|
|
532
|
-
return `instance-${instanceContext.instanceId}-${configHash}-${(0, uuid_1.v4)()}`;
|
|
533
|
-
}
|
|
534
|
-
return (0, uuid_1.v4)();
|
|
535
|
-
}
|
|
536
172
|
getSessionMetrics() {
|
|
537
173
|
const now = Date.now();
|
|
538
174
|
let expiredCount = 0;
|
|
@@ -675,27 +311,14 @@ class SingleSessionHTTPServer {
|
|
|
675
311
|
transport.onclose = () => {
|
|
676
312
|
const sid = transport.sessionId;
|
|
677
313
|
if (sid) {
|
|
678
|
-
if (this.isShuttingDown) {
|
|
679
|
-
logger_1.logger.debug('Ignoring transport close event during shutdown', { sessionId: sid });
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
314
|
logger_1.logger.info('handleRequest: Transport closed, cleaning up', { sessionId: sid });
|
|
683
|
-
this.removeSession(sid, 'transport_closed')
|
|
684
|
-
logger_1.logger.error('Error during transport close cleanup', {
|
|
685
|
-
sessionId: sid,
|
|
686
|
-
error: err instanceof Error ? err.message : String(err)
|
|
687
|
-
});
|
|
688
|
-
});
|
|
315
|
+
this.removeSession(sid, 'transport_closed');
|
|
689
316
|
}
|
|
690
317
|
};
|
|
691
318
|
transport.onerror = (error) => {
|
|
692
319
|
const sid = transport.sessionId;
|
|
320
|
+
logger_1.logger.error('Transport error', { sessionId: sid, error: error.message });
|
|
693
321
|
if (sid) {
|
|
694
|
-
if (this.isShuttingDown) {
|
|
695
|
-
logger_1.logger.debug('Ignoring transport error event during shutdown', { sessionId: sid });
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
logger_1.logger.error('Transport error', { sessionId: sid, error: error.message });
|
|
699
322
|
this.removeSession(sid, 'transport_error').catch(err => {
|
|
700
323
|
logger_1.logger.error('Error during transport error cleanup', { error: err });
|
|
701
324
|
});
|
|
@@ -703,12 +326,6 @@ class SingleSessionHTTPServer {
|
|
|
703
326
|
};
|
|
704
327
|
logger_1.logger.info('handleRequest: Connecting server to new transport');
|
|
705
328
|
await server.connect(transport);
|
|
706
|
-
this.emitEvent('onSessionCreated', sessionIdToUse, instanceContext).catch(eventErr => {
|
|
707
|
-
logger_1.logger.error('Failed to emit onSessionCreated event (non-blocking)', {
|
|
708
|
-
sessionId: sessionIdToUse,
|
|
709
|
-
error: eventErr instanceof Error ? eventErr.message : String(eventErr)
|
|
710
|
-
});
|
|
711
|
-
});
|
|
712
329
|
}
|
|
713
330
|
else if (sessionId && this.transports[sessionId]) {
|
|
714
331
|
if (!this.isValidSessionId(sessionId)) {
|
|
@@ -733,194 +350,29 @@ class SingleSessionHTTPServer {
|
|
|
733
350
|
this.updateSessionAccess(sessionId);
|
|
734
351
|
}
|
|
735
352
|
else {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
},
|
|
747
|
-
id: req.body?.id || null
|
|
748
|
-
});
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
if (this.onSessionNotFound) {
|
|
752
|
-
logger_1.logger.info('Attempting session restoration', { sessionId });
|
|
753
|
-
try {
|
|
754
|
-
const restoredContext = await this.restoreSessionWithRetry(sessionId);
|
|
755
|
-
if (restoredContext === null || restoredContext === undefined) {
|
|
756
|
-
logger_1.logger.info('Session restoration declined by hook', {
|
|
757
|
-
sessionId,
|
|
758
|
-
returnValue: restoredContext === null ? 'null' : 'undefined'
|
|
759
|
-
});
|
|
760
|
-
res.status(400).json({
|
|
761
|
-
jsonrpc: '2.0',
|
|
762
|
-
error: {
|
|
763
|
-
code: -32000,
|
|
764
|
-
message: 'Session not found or expired'
|
|
765
|
-
},
|
|
766
|
-
id: req.body?.id || null
|
|
767
|
-
});
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
|
-
const validation = (0, instance_context_1.validateInstanceContext)(restoredContext);
|
|
771
|
-
if (!validation.valid) {
|
|
772
|
-
logger_1.logger.error('Invalid context returned from restoration hook', {
|
|
773
|
-
sessionId,
|
|
774
|
-
errors: validation.errors
|
|
775
|
-
});
|
|
776
|
-
res.status(400).json({
|
|
777
|
-
jsonrpc: '2.0',
|
|
778
|
-
error: {
|
|
779
|
-
code: -32000,
|
|
780
|
-
message: 'Invalid session context'
|
|
781
|
-
},
|
|
782
|
-
id: req.body?.id || null
|
|
783
|
-
});
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
logger_1.logger.info('Session restoration successful, creating transport inline', {
|
|
787
|
-
sessionId,
|
|
788
|
-
instanceId: restoredContext.instanceId
|
|
789
|
-
});
|
|
790
|
-
const server = new server_1.N8NDocumentationMCPServer(restoredContext);
|
|
791
|
-
transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
792
|
-
sessionIdGenerator: () => sessionId,
|
|
793
|
-
onsessioninitialized: (initializedSessionId) => {
|
|
794
|
-
logger_1.logger.info('Session initialized after restoration', {
|
|
795
|
-
sessionId: initializedSessionId
|
|
796
|
-
});
|
|
797
|
-
this.transports[initializedSessionId] = transport;
|
|
798
|
-
this.servers[initializedSessionId] = server;
|
|
799
|
-
this.sessionMetadata[initializedSessionId] = {
|
|
800
|
-
lastAccess: new Date(),
|
|
801
|
-
createdAt: new Date()
|
|
802
|
-
};
|
|
803
|
-
this.sessionContexts[initializedSessionId] = restoredContext;
|
|
804
|
-
}
|
|
805
|
-
});
|
|
806
|
-
transport.onclose = () => {
|
|
807
|
-
const sid = transport.sessionId;
|
|
808
|
-
if (sid) {
|
|
809
|
-
if (this.isShuttingDown) {
|
|
810
|
-
logger_1.logger.debug('Ignoring transport close event during shutdown', { sessionId: sid });
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
logger_1.logger.info('Restored transport closed, cleaning up', { sessionId: sid });
|
|
814
|
-
this.removeSession(sid, 'transport_closed').catch(err => {
|
|
815
|
-
logger_1.logger.error('Error during transport close cleanup', {
|
|
816
|
-
sessionId: sid,
|
|
817
|
-
error: err instanceof Error ? err.message : String(err)
|
|
818
|
-
});
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
};
|
|
822
|
-
transport.onerror = (error) => {
|
|
823
|
-
const sid = transport.sessionId;
|
|
824
|
-
if (sid) {
|
|
825
|
-
if (this.isShuttingDown) {
|
|
826
|
-
logger_1.logger.debug('Ignoring transport error event during shutdown', { sessionId: sid });
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
logger_1.logger.error('Restored transport error', { sessionId: sid, error: error.message });
|
|
830
|
-
this.removeSession(sid, 'transport_error').catch(err => {
|
|
831
|
-
logger_1.logger.error('Error during transport error cleanup', { error: err });
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
|
-
logger_1.logger.info('Connecting server to restored session transport');
|
|
836
|
-
await server.connect(transport);
|
|
837
|
-
const isTestMemory = process.env.NODE_ENV === 'test' &&
|
|
838
|
-
process.env.NODE_DB_PATH === ':memory:';
|
|
839
|
-
if (!isTestMemory) {
|
|
840
|
-
try {
|
|
841
|
-
logger_1.logger.info('Initializing MCP server for restored session', { sessionId });
|
|
842
|
-
await this.initializeMCPServerForSession(sessionId, server, restoredContext);
|
|
843
|
-
}
|
|
844
|
-
catch (initError) {
|
|
845
|
-
logger_1.logger.warn('MCP server initialization failed during restoration (non-fatal)', {
|
|
846
|
-
sessionId,
|
|
847
|
-
error: initError instanceof Error ? initError.message : String(initError)
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
else {
|
|
852
|
-
logger_1.logger.debug('Skipping MCP server initialization in test mode with :memory: database', {
|
|
853
|
-
sessionId
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
this.emitEvent('onSessionRestored', sessionId, restoredContext).catch(err => {
|
|
857
|
-
logger_1.logger.error('Failed to emit onSessionRestored event (non-blocking)', {
|
|
858
|
-
sessionId,
|
|
859
|
-
error: err instanceof Error ? err.message : String(err)
|
|
860
|
-
});
|
|
861
|
-
});
|
|
862
|
-
logger_1.logger.info('Restored session transport ready', { sessionId });
|
|
863
|
-
}
|
|
864
|
-
catch (error) {
|
|
865
|
-
if (error instanceof Error && error.name === 'TimeoutError') {
|
|
866
|
-
logger_1.logger.error('Session restoration timeout', {
|
|
867
|
-
sessionId,
|
|
868
|
-
timeout: this.sessionRestorationTimeout
|
|
869
|
-
});
|
|
870
|
-
res.status(408).json({
|
|
871
|
-
jsonrpc: '2.0',
|
|
872
|
-
error: {
|
|
873
|
-
code: -32000,
|
|
874
|
-
message: 'Session restoration timeout'
|
|
875
|
-
},
|
|
876
|
-
id: req.body?.id || null
|
|
877
|
-
});
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
logger_1.logger.error('Session restoration failed', {
|
|
881
|
-
sessionId,
|
|
882
|
-
error: error instanceof Error ? error.message : String(error)
|
|
883
|
-
});
|
|
884
|
-
res.status(500).json({
|
|
885
|
-
jsonrpc: '2.0',
|
|
886
|
-
error: {
|
|
887
|
-
code: -32603,
|
|
888
|
-
message: 'Session restoration failed'
|
|
889
|
-
},
|
|
890
|
-
id: req.body?.id || null
|
|
891
|
-
});
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
else {
|
|
896
|
-
logger_1.logger.warn('Session not found and no restoration hook configured', {
|
|
897
|
-
sessionId
|
|
898
|
-
});
|
|
899
|
-
res.status(400).json({
|
|
900
|
-
jsonrpc: '2.0',
|
|
901
|
-
error: {
|
|
902
|
-
code: -32000,
|
|
903
|
-
message: 'Session not found or expired'
|
|
904
|
-
},
|
|
905
|
-
id: req.body?.id || null
|
|
906
|
-
});
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
353
|
+
const errorDetails = {
|
|
354
|
+
hasSessionId: !!sessionId,
|
|
355
|
+
isInitialize: isInitialize,
|
|
356
|
+
sessionIdValid: sessionId ? this.isValidSessionId(sessionId) : false,
|
|
357
|
+
sessionExists: sessionId ? !!this.transports[sessionId] : false
|
|
358
|
+
};
|
|
359
|
+
logger_1.logger.warn('handleRequest: Invalid request - no session ID and not initialize', errorDetails);
|
|
360
|
+
let errorMessage = 'Bad Request: No valid session ID provided and not an initialize request';
|
|
361
|
+
if (sessionId && !this.isValidSessionId(sessionId)) {
|
|
362
|
+
errorMessage = 'Bad Request: Invalid session ID format';
|
|
909
363
|
}
|
|
910
|
-
else {
|
|
911
|
-
|
|
912
|
-
isInitialize
|
|
913
|
-
});
|
|
914
|
-
res.status(400).json({
|
|
915
|
-
jsonrpc: '2.0',
|
|
916
|
-
error: {
|
|
917
|
-
code: -32000,
|
|
918
|
-
message: 'Bad Request: No valid session ID provided and not an initialize request'
|
|
919
|
-
},
|
|
920
|
-
id: req.body?.id || null
|
|
921
|
-
});
|
|
922
|
-
return;
|
|
364
|
+
else if (sessionId && !this.transports[sessionId]) {
|
|
365
|
+
errorMessage = 'Bad Request: Session not found or expired';
|
|
923
366
|
}
|
|
367
|
+
res.status(400).json({
|
|
368
|
+
jsonrpc: '2.0',
|
|
369
|
+
error: {
|
|
370
|
+
code: -32000,
|
|
371
|
+
message: errorMessage
|
|
372
|
+
},
|
|
373
|
+
id: req.body?.id || null
|
|
374
|
+
});
|
|
375
|
+
return;
|
|
924
376
|
}
|
|
925
377
|
logger_1.logger.info('handleRequest: Handling request with transport', {
|
|
926
378
|
sessionId: isInitialize ? 'new' : sessionId,
|
|
@@ -1503,8 +955,6 @@ class SingleSessionHTTPServer {
|
|
|
1503
955
|
}
|
|
1504
956
|
async shutdown() {
|
|
1505
957
|
logger_1.logger.info('Shutting down Single-Session HTTP server...');
|
|
1506
|
-
this.isShuttingDown = true;
|
|
1507
|
-
logger_1.logger.info('Shutdown flag set - recursive cleanup prevention enabled');
|
|
1508
958
|
if (this.cleanupTimer) {
|
|
1509
959
|
clearInterval(this.cleanupTimer);
|
|
1510
960
|
this.cleanupTimer = null;
|
|
@@ -1512,28 +962,15 @@ class SingleSessionHTTPServer {
|
|
|
1512
962
|
}
|
|
1513
963
|
const sessionIds = Object.keys(this.transports);
|
|
1514
964
|
logger_1.logger.info(`Closing ${sessionIds.length} active sessions`);
|
|
1515
|
-
let successCount = 0;
|
|
1516
|
-
let failureCount = 0;
|
|
1517
965
|
for (const sessionId of sessionIds) {
|
|
1518
966
|
try {
|
|
1519
967
|
logger_1.logger.info(`Closing transport for session ${sessionId}`);
|
|
1520
968
|
await this.removeSession(sessionId, 'server_shutdown');
|
|
1521
|
-
successCount++;
|
|
1522
969
|
}
|
|
1523
970
|
catch (error) {
|
|
1524
|
-
|
|
1525
|
-
logger_1.logger.warn(`Error closing transport for session ${sessionId}:`, {
|
|
1526
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1527
|
-
});
|
|
971
|
+
logger_1.logger.warn(`Error closing transport for session ${sessionId}:`, error);
|
|
1528
972
|
}
|
|
1529
973
|
}
|
|
1530
|
-
if (sessionIds.length > 0) {
|
|
1531
|
-
logger_1.logger.info('Session shutdown completed', {
|
|
1532
|
-
total: sessionIds.length,
|
|
1533
|
-
successful: successCount,
|
|
1534
|
-
failed: failureCount
|
|
1535
|
-
});
|
|
1536
|
-
}
|
|
1537
974
|
if (this.session) {
|
|
1538
975
|
try {
|
|
1539
976
|
await this.session.transport.close();
|
|
@@ -1581,117 +1018,6 @@ class SingleSessionHTTPServer {
|
|
|
1581
1018
|
}
|
|
1582
1019
|
};
|
|
1583
1020
|
}
|
|
1584
|
-
getActiveSessions() {
|
|
1585
|
-
return Object.keys(this.sessionMetadata);
|
|
1586
|
-
}
|
|
1587
|
-
getSessionState(sessionId) {
|
|
1588
|
-
const metadata = this.sessionMetadata[sessionId];
|
|
1589
|
-
if (!metadata) {
|
|
1590
|
-
return null;
|
|
1591
|
-
}
|
|
1592
|
-
const instanceContext = this.sessionContexts[sessionId];
|
|
1593
|
-
const expiresAt = new Date(metadata.lastAccess.getTime() + this.sessionTimeout);
|
|
1594
|
-
return {
|
|
1595
|
-
sessionId,
|
|
1596
|
-
instanceContext: instanceContext || {
|
|
1597
|
-
n8nApiUrl: process.env.N8N_API_URL,
|
|
1598
|
-
n8nApiKey: process.env.N8N_API_KEY,
|
|
1599
|
-
instanceId: process.env.N8N_INSTANCE_ID
|
|
1600
|
-
},
|
|
1601
|
-
createdAt: metadata.createdAt,
|
|
1602
|
-
lastAccess: metadata.lastAccess,
|
|
1603
|
-
expiresAt,
|
|
1604
|
-
metadata: instanceContext?.metadata
|
|
1605
|
-
};
|
|
1606
|
-
}
|
|
1607
|
-
getAllSessionStates() {
|
|
1608
|
-
const sessionIds = this.getActiveSessions();
|
|
1609
|
-
const states = [];
|
|
1610
|
-
for (const sessionId of sessionIds) {
|
|
1611
|
-
const state = this.getSessionState(sessionId);
|
|
1612
|
-
if (state) {
|
|
1613
|
-
states.push(state);
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
return states;
|
|
1617
|
-
}
|
|
1618
|
-
manuallyRestoreSession(sessionId, instanceContext) {
|
|
1619
|
-
try {
|
|
1620
|
-
if (!this.isValidSessionId(sessionId)) {
|
|
1621
|
-
logger_1.logger.error('Invalid session ID format in manual restoration', { sessionId });
|
|
1622
|
-
return false;
|
|
1623
|
-
}
|
|
1624
|
-
const validation = (0, instance_context_1.validateInstanceContext)(instanceContext);
|
|
1625
|
-
if (!validation.valid) {
|
|
1626
|
-
logger_1.logger.error('Invalid instance context in manual restoration', {
|
|
1627
|
-
sessionId,
|
|
1628
|
-
errors: validation.errors
|
|
1629
|
-
});
|
|
1630
|
-
return false;
|
|
1631
|
-
}
|
|
1632
|
-
this.sessionMetadata[sessionId] = {
|
|
1633
|
-
lastAccess: new Date(),
|
|
1634
|
-
createdAt: new Date()
|
|
1635
|
-
};
|
|
1636
|
-
this.sessionContexts[sessionId] = instanceContext;
|
|
1637
|
-
const creationResult = this.createSession(instanceContext, sessionId, false);
|
|
1638
|
-
Promise.resolve(creationResult).catch(error => {
|
|
1639
|
-
logger_1.logger.error('Async session creation failed in manual restoration', {
|
|
1640
|
-
sessionId,
|
|
1641
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1642
|
-
});
|
|
1643
|
-
delete this.sessionMetadata[sessionId];
|
|
1644
|
-
delete this.sessionContexts[sessionId];
|
|
1645
|
-
});
|
|
1646
|
-
logger_1.logger.info('Session manually restored', {
|
|
1647
|
-
sessionId,
|
|
1648
|
-
instanceId: instanceContext.instanceId
|
|
1649
|
-
});
|
|
1650
|
-
return true;
|
|
1651
|
-
}
|
|
1652
|
-
catch (error) {
|
|
1653
|
-
logger_1.logger.error('Failed to manually restore session', {
|
|
1654
|
-
sessionId,
|
|
1655
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1656
|
-
});
|
|
1657
|
-
return false;
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
manuallyDeleteSession(sessionId) {
|
|
1661
|
-
if (!this.sessionMetadata[sessionId]) {
|
|
1662
|
-
logger_1.logger.debug('Session not found for manual deletion', { sessionId });
|
|
1663
|
-
return false;
|
|
1664
|
-
}
|
|
1665
|
-
try {
|
|
1666
|
-
if (this.transports[sessionId]) {
|
|
1667
|
-
this.transports[sessionId].close().catch(error => {
|
|
1668
|
-
logger_1.logger.warn('Error closing transport during manual deletion', {
|
|
1669
|
-
sessionId,
|
|
1670
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1671
|
-
});
|
|
1672
|
-
});
|
|
1673
|
-
}
|
|
1674
|
-
this.emitEvent('onSessionDeleted', sessionId).catch(err => {
|
|
1675
|
-
logger_1.logger.error('Failed to emit onSessionDeleted event (non-blocking)', {
|
|
1676
|
-
sessionId,
|
|
1677
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1678
|
-
});
|
|
1679
|
-
});
|
|
1680
|
-
delete this.transports[sessionId];
|
|
1681
|
-
delete this.servers[sessionId];
|
|
1682
|
-
delete this.sessionMetadata[sessionId];
|
|
1683
|
-
delete this.sessionContexts[sessionId];
|
|
1684
|
-
logger_1.logger.info('Session manually deleted', { sessionId });
|
|
1685
|
-
return true;
|
|
1686
|
-
}
|
|
1687
|
-
catch (error) {
|
|
1688
|
-
logger_1.logger.error('Error during manual session deletion', {
|
|
1689
|
-
sessionId,
|
|
1690
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1691
|
-
});
|
|
1692
|
-
return false;
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
1021
|
}
|
|
1696
1022
|
exports.SingleSessionHTTPServer = SingleSessionHTTPServer;
|
|
1697
1023
|
if (require.main === module) {
|