n8n-mcp 2.8.1 → 2.9.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.
Files changed (47) hide show
  1. package/README.md +9 -1
  2. package/data/nodes.db +0 -0
  3. package/dist/http-server-single-session.d.ts +21 -1
  4. package/dist/http-server-single-session.d.ts.map +1 -1
  5. package/dist/http-server-single-session.js +515 -44
  6. package/dist/http-server-single-session.js.map +1 -1
  7. package/dist/http-server.d.ts.map +1 -1
  8. package/dist/http-server.js +5 -2
  9. package/dist/http-server.js.map +1 -1
  10. package/dist/mcp/server.d.ts +4 -0
  11. package/dist/mcp/server.d.ts.map +1 -1
  12. package/dist/mcp/server.js +382 -17
  13. package/dist/mcp/server.js.map +1 -1
  14. package/dist/mcp/tools-n8n-friendly.d.ts +6 -0
  15. package/dist/mcp/tools-n8n-friendly.d.ts.map +1 -0
  16. package/dist/mcp/tools-n8n-friendly.js +131 -0
  17. package/dist/mcp/tools-n8n-friendly.js.map +1 -0
  18. package/dist/mcp/tools.d.ts.map +1 -1
  19. package/dist/mcp/tools.js +187 -11
  20. package/dist/mcp/tools.js.map +1 -1
  21. package/dist/mcp/workflow-examples.d.ts +76 -0
  22. package/dist/mcp/workflow-examples.d.ts.map +1 -0
  23. package/dist/mcp/workflow-examples.js +111 -0
  24. package/dist/mcp/workflow-examples.js.map +1 -0
  25. package/dist/scripts/test-protocol-negotiation.d.ts +3 -0
  26. package/dist/scripts/test-protocol-negotiation.d.ts.map +1 -0
  27. package/dist/scripts/test-protocol-negotiation.js +154 -0
  28. package/dist/scripts/test-protocol-negotiation.js.map +1 -0
  29. package/dist/services/enhanced-config-validator.d.ts +4 -0
  30. package/dist/services/enhanced-config-validator.d.ts.map +1 -1
  31. package/dist/services/enhanced-config-validator.js +86 -1
  32. package/dist/services/enhanced-config-validator.js.map +1 -1
  33. package/dist/services/n8n-validation.d.ts +2 -2
  34. package/dist/types/index.d.ts +6 -0
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/dist/utils/fixed-collection-validator.d.ts +35 -0
  37. package/dist/utils/fixed-collection-validator.d.ts.map +1 -0
  38. package/dist/utils/fixed-collection-validator.js +358 -0
  39. package/dist/utils/fixed-collection-validator.js.map +1 -0
  40. package/dist/utils/logger.d.ts.map +1 -1
  41. package/dist/utils/logger.js +6 -3
  42. package/dist/utils/logger.js.map +1 -1
  43. package/dist/utils/protocol-version.d.ts +19 -0
  44. package/dist/utils/protocol-version.d.ts.map +1 -0
  45. package/dist/utils/protocol-version.js +95 -0
  46. package/dist/utils/protocol-version.js.map +1 -0
  47. package/package.json +1 -1
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.SingleSessionHTTPServer = void 0;
8
8
  const express_1 = __importDefault(require("express"));
9
9
  const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
10
+ const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
10
11
  const server_1 = require("./mcp/server");
11
12
  const console_manager_1 = require("./utils/console-manager");
12
13
  const logger_1 = require("./utils/logger");
@@ -14,14 +15,126 @@ const fs_1 = require("fs");
14
15
  const dotenv_1 = __importDefault(require("dotenv"));
15
16
  const url_detector_1 = require("./utils/url-detector");
16
17
  const version_1 = require("./utils/version");
18
+ const uuid_1 = require("uuid");
19
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
20
+ const protocol_version_1 = require("./utils/protocol-version");
17
21
  dotenv_1.default.config();
22
+ const DEFAULT_PROTOCOL_VERSION = protocol_version_1.STANDARD_PROTOCOL_VERSION;
23
+ const MAX_SESSIONS = 100;
24
+ const SESSION_CLEANUP_INTERVAL = 5 * 60 * 1000;
18
25
  class SingleSessionHTTPServer {
19
26
  constructor() {
27
+ this.transports = {};
28
+ this.servers = {};
29
+ this.sessionMetadata = {};
20
30
  this.session = null;
21
31
  this.consoleManager = new console_manager_1.ConsoleManager();
22
32
  this.sessionTimeout = 30 * 60 * 1000;
23
33
  this.authToken = null;
34
+ this.cleanupTimer = null;
24
35
  this.validateEnvironment();
36
+ this.startSessionCleanup();
37
+ }
38
+ startSessionCleanup() {
39
+ this.cleanupTimer = setInterval(async () => {
40
+ try {
41
+ await this.cleanupExpiredSessions();
42
+ }
43
+ catch (error) {
44
+ logger_1.logger.error('Error during session cleanup', error);
45
+ }
46
+ }, SESSION_CLEANUP_INTERVAL);
47
+ logger_1.logger.info('Session cleanup started', {
48
+ interval: SESSION_CLEANUP_INTERVAL / 1000 / 60,
49
+ maxSessions: MAX_SESSIONS,
50
+ sessionTimeout: this.sessionTimeout / 1000 / 60
51
+ });
52
+ }
53
+ cleanupExpiredSessions() {
54
+ const now = Date.now();
55
+ const expiredSessions = [];
56
+ for (const sessionId in this.sessionMetadata) {
57
+ const metadata = this.sessionMetadata[sessionId];
58
+ if (now - metadata.lastAccess.getTime() > this.sessionTimeout) {
59
+ expiredSessions.push(sessionId);
60
+ }
61
+ }
62
+ for (const sessionId of expiredSessions) {
63
+ this.removeSession(sessionId, 'expired');
64
+ }
65
+ if (expiredSessions.length > 0) {
66
+ logger_1.logger.info('Cleaned up expired sessions', {
67
+ removed: expiredSessions.length,
68
+ remaining: this.getActiveSessionCount()
69
+ });
70
+ }
71
+ }
72
+ async removeSession(sessionId, reason) {
73
+ try {
74
+ if (this.transports[sessionId]) {
75
+ await this.transports[sessionId].close();
76
+ delete this.transports[sessionId];
77
+ }
78
+ delete this.servers[sessionId];
79
+ delete this.sessionMetadata[sessionId];
80
+ logger_1.logger.info('Session removed', { sessionId, reason });
81
+ }
82
+ catch (error) {
83
+ logger_1.logger.warn('Error removing session', { sessionId, reason, error });
84
+ }
85
+ }
86
+ getActiveSessionCount() {
87
+ return Object.keys(this.transports).length;
88
+ }
89
+ canCreateSession() {
90
+ return this.getActiveSessionCount() < MAX_SESSIONS;
91
+ }
92
+ isValidSessionId(sessionId) {
93
+ const uuidv4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
94
+ return uuidv4Regex.test(sessionId);
95
+ }
96
+ sanitizeErrorForClient(error) {
97
+ const isProduction = process.env.NODE_ENV === 'production';
98
+ if (error instanceof Error) {
99
+ if (isProduction) {
100
+ if (error.message.includes('Unauthorized') || error.message.includes('authentication')) {
101
+ return { message: 'Authentication failed', code: 'AUTH_ERROR' };
102
+ }
103
+ if (error.message.includes('Session') || error.message.includes('session')) {
104
+ return { message: 'Session error', code: 'SESSION_ERROR' };
105
+ }
106
+ if (error.message.includes('Invalid') || error.message.includes('validation')) {
107
+ return { message: 'Validation error', code: 'VALIDATION_ERROR' };
108
+ }
109
+ return { message: 'Internal server error', code: 'INTERNAL_ERROR' };
110
+ }
111
+ return {
112
+ message: error.message.substring(0, 200),
113
+ code: error.name || 'ERROR'
114
+ };
115
+ }
116
+ return { message: 'An error occurred', code: 'UNKNOWN_ERROR' };
117
+ }
118
+ updateSessionAccess(sessionId) {
119
+ if (this.sessionMetadata[sessionId]) {
120
+ this.sessionMetadata[sessionId].lastAccess = new Date();
121
+ }
122
+ }
123
+ getSessionMetrics() {
124
+ const now = Date.now();
125
+ let expiredCount = 0;
126
+ for (const sessionId in this.sessionMetadata) {
127
+ const metadata = this.sessionMetadata[sessionId];
128
+ if (now - metadata.lastAccess.getTime() > this.sessionTimeout) {
129
+ expiredCount++;
130
+ }
131
+ }
132
+ return {
133
+ totalSessions: Object.keys(this.sessionMetadata).length,
134
+ activeSessions: this.getActiveSessionCount(),
135
+ expiredSessions: expiredCount,
136
+ lastCleanup: new Date()
137
+ };
25
138
  }
26
139
  loadAuthToken() {
27
140
  if (process.env.AUTH_TOKEN) {
@@ -54,7 +167,17 @@ class SingleSessionHTTPServer {
54
167
  if (this.authToken.length < 32) {
55
168
  logger_1.logger.warn('AUTH_TOKEN should be at least 32 characters for security');
56
169
  }
57
- if (this.authToken === 'REPLACE_THIS_AUTH_TOKEN_32_CHARS_MIN_abcdefgh') {
170
+ const isDefaultToken = this.authToken === 'REPLACE_THIS_AUTH_TOKEN_32_CHARS_MIN_abcdefgh';
171
+ const isProduction = process.env.NODE_ENV === 'production';
172
+ if (isDefaultToken) {
173
+ if (isProduction) {
174
+ const message = 'CRITICAL SECURITY ERROR: Cannot start in production with default AUTH_TOKEN. Generate secure token: openssl rand -base64 32';
175
+ logger_1.logger.error(message);
176
+ console.error('\n🚨 CRITICAL SECURITY ERROR 🚨');
177
+ console.error(message);
178
+ console.error('Set NODE_ENV to development for testing, or update AUTH_TOKEN for production\n');
179
+ throw new Error(message);
180
+ }
58
181
  logger_1.logger.warn('⚠️ SECURITY WARNING: Using default AUTH_TOKEN - CHANGE IMMEDIATELY!');
59
182
  logger_1.logger.warn('Generate secure token with: openssl rand -base64 32');
60
183
  if (process.env.MCP_MODE === 'http') {
@@ -69,41 +192,147 @@ class SingleSessionHTTPServer {
69
192
  const startTime = Date.now();
70
193
  return this.consoleManager.wrapOperation(async () => {
71
194
  try {
72
- if (!this.session || this.isExpired()) {
73
- await this.resetSession();
195
+ const sessionId = req.headers['mcp-session-id'];
196
+ const isInitialize = req.body ? (0, types_js_1.isInitializeRequest)(req.body) : false;
197
+ logger_1.logger.info('handleRequest: Processing MCP request - SDK PATTERN', {
198
+ requestId: req.get('x-request-id') || 'unknown',
199
+ sessionId: sessionId,
200
+ method: req.method,
201
+ url: req.url,
202
+ bodyType: typeof req.body,
203
+ bodyContent: req.body ? JSON.stringify(req.body, null, 2) : 'undefined',
204
+ existingTransports: Object.keys(this.transports),
205
+ isInitializeRequest: isInitialize
206
+ });
207
+ let transport;
208
+ if (isInitialize) {
209
+ if (!this.canCreateSession()) {
210
+ logger_1.logger.warn('handleRequest: Session limit reached', {
211
+ currentSessions: this.getActiveSessionCount(),
212
+ maxSessions: MAX_SESSIONS
213
+ });
214
+ res.status(429).json({
215
+ jsonrpc: '2.0',
216
+ error: {
217
+ code: -32000,
218
+ message: `Session limit reached (${MAX_SESSIONS}). Please wait for existing sessions to expire.`
219
+ },
220
+ id: req.body?.id || null
221
+ });
222
+ return;
223
+ }
224
+ logger_1.logger.info('handleRequest: Creating new transport for initialize request');
225
+ const sessionIdToUse = sessionId || (0, uuid_1.v4)();
226
+ const server = new server_1.N8NDocumentationMCPServer();
227
+ transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
228
+ sessionIdGenerator: () => sessionIdToUse,
229
+ onsessioninitialized: (initializedSessionId) => {
230
+ logger_1.logger.info('handleRequest: Session initialized, storing transport and server', {
231
+ sessionId: initializedSessionId
232
+ });
233
+ this.transports[initializedSessionId] = transport;
234
+ this.servers[initializedSessionId] = server;
235
+ this.sessionMetadata[initializedSessionId] = {
236
+ lastAccess: new Date(),
237
+ createdAt: new Date()
238
+ };
239
+ }
240
+ });
241
+ transport.onclose = () => {
242
+ const sid = transport.sessionId;
243
+ if (sid) {
244
+ logger_1.logger.info('handleRequest: Transport closed, cleaning up', { sessionId: sid });
245
+ this.removeSession(sid, 'transport_closed');
246
+ }
247
+ };
248
+ logger_1.logger.info('handleRequest: Connecting server to new transport');
249
+ await server.connect(transport);
74
250
  }
75
- this.session.lastAccess = new Date();
76
- logger_1.logger.debug('Calling transport.handleRequest...');
77
- await this.session.transport.handleRequest(req, res);
78
- logger_1.logger.debug('transport.handleRequest completed');
79
- const duration = Date.now() - startTime;
80
- logger_1.logger.info('MCP request completed', {
81
- duration,
82
- sessionId: this.session.sessionId
251
+ else if (sessionId && this.transports[sessionId]) {
252
+ if (!this.isValidSessionId(sessionId)) {
253
+ logger_1.logger.warn('handleRequest: Invalid session ID format', { sessionId });
254
+ res.status(400).json({
255
+ jsonrpc: '2.0',
256
+ error: {
257
+ code: -32602,
258
+ message: 'Invalid session ID format'
259
+ },
260
+ id: req.body?.id || null
261
+ });
262
+ return;
263
+ }
264
+ logger_1.logger.info('handleRequest: Reusing existing transport for session', { sessionId });
265
+ transport = this.transports[sessionId];
266
+ this.updateSessionAccess(sessionId);
267
+ }
268
+ else {
269
+ const errorDetails = {
270
+ hasSessionId: !!sessionId,
271
+ isInitialize: isInitialize,
272
+ sessionIdValid: sessionId ? this.isValidSessionId(sessionId) : false,
273
+ sessionExists: sessionId ? !!this.transports[sessionId] : false
274
+ };
275
+ logger_1.logger.warn('handleRequest: Invalid request - no session ID and not initialize', errorDetails);
276
+ let errorMessage = 'Bad Request: No valid session ID provided and not an initialize request';
277
+ if (sessionId && !this.isValidSessionId(sessionId)) {
278
+ errorMessage = 'Bad Request: Invalid session ID format';
279
+ }
280
+ else if (sessionId && !this.transports[sessionId]) {
281
+ errorMessage = 'Bad Request: Session not found or expired';
282
+ }
283
+ res.status(400).json({
284
+ jsonrpc: '2.0',
285
+ error: {
286
+ code: -32000,
287
+ message: errorMessage
288
+ },
289
+ id: req.body?.id || null
290
+ });
291
+ return;
292
+ }
293
+ logger_1.logger.info('handleRequest: Handling request with transport', {
294
+ sessionId: isInitialize ? 'new' : sessionId,
295
+ isInitialize
83
296
  });
297
+ await transport.handleRequest(req, res, req.body);
298
+ const duration = Date.now() - startTime;
299
+ logger_1.logger.info('MCP request completed', { duration, sessionId: transport.sessionId });
84
300
  }
85
301
  catch (error) {
86
- logger_1.logger.error('MCP request error:', error);
302
+ logger_1.logger.error('handleRequest: MCP request error:', {
303
+ error: error instanceof Error ? error.message : error,
304
+ errorName: error instanceof Error ? error.name : 'Unknown',
305
+ stack: error instanceof Error ? error.stack : undefined,
306
+ activeTransports: Object.keys(this.transports),
307
+ requestDetails: {
308
+ method: req.method,
309
+ url: req.url,
310
+ hasBody: !!req.body,
311
+ sessionId: req.headers['mcp-session-id']
312
+ },
313
+ duration: Date.now() - startTime
314
+ });
87
315
  if (!res.headersSent) {
316
+ const sanitizedError = this.sanitizeErrorForClient(error);
88
317
  res.status(500).json({
89
318
  jsonrpc: '2.0',
90
319
  error: {
91
320
  code: -32603,
92
- message: 'Internal server error',
93
- data: process.env.NODE_ENV === 'development'
94
- ? error.message
95
- : undefined
321
+ message: sanitizedError.message,
322
+ data: {
323
+ code: sanitizedError.code
324
+ }
96
325
  },
97
- id: null
326
+ id: req.body?.id || null
98
327
  });
99
328
  }
100
329
  }
101
330
  });
102
331
  }
103
- async resetSession() {
332
+ async resetSessionSSE(res) {
104
333
  if (this.session) {
105
334
  try {
106
- logger_1.logger.info('Closing previous session', { sessionId: this.session.sessionId });
335
+ logger_1.logger.info('Closing previous session for SSE', { sessionId: this.session.sessionId });
107
336
  await this.session.transport.close();
108
337
  }
109
338
  catch (error) {
@@ -111,24 +340,25 @@ class SingleSessionHTTPServer {
111
340
  }
112
341
  }
113
342
  try {
114
- logger_1.logger.info('Creating new N8NDocumentationMCPServer...');
343
+ logger_1.logger.info('Creating new N8NDocumentationMCPServer for SSE...');
115
344
  const server = new server_1.N8NDocumentationMCPServer();
116
- logger_1.logger.info('Creating StreamableHTTPServerTransport...');
117
- const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
118
- sessionIdGenerator: () => 'single-session',
119
- });
120
- logger_1.logger.info('Connecting server to transport...');
345
+ const sessionId = (0, uuid_1.v4)();
346
+ logger_1.logger.info('Creating SSEServerTransport...');
347
+ const transport = new sse_js_1.SSEServerTransport('/mcp', res);
348
+ logger_1.logger.info('Connecting server to SSE transport...');
121
349
  await server.connect(transport);
122
350
  this.session = {
123
351
  server,
124
352
  transport,
125
353
  lastAccess: new Date(),
126
- sessionId: 'single-session'
354
+ sessionId,
355
+ initialized: false,
356
+ isSSE: true
127
357
  };
128
- logger_1.logger.info('Created new single session successfully', { sessionId: this.session.sessionId });
358
+ logger_1.logger.info('Created new SSE session successfully', { sessionId: this.session.sessionId });
129
359
  }
130
360
  catch (error) {
131
- logger_1.logger.error('Failed to create session:', error);
361
+ logger_1.logger.error('Failed to create SSE session:', error);
132
362
  throw error;
133
363
  }
134
364
  }
@@ -139,6 +369,7 @@ class SingleSessionHTTPServer {
139
369
  }
140
370
  async start() {
141
371
  const app = (0, express_1.default)();
372
+ const jsonParser = express_1.default.json({ limit: '10mb' });
142
373
  const trustProxy = process.env.TRUST_PROXY ? Number(process.env.TRUST_PROXY) : 0;
143
374
  if (trustProxy > 0) {
144
375
  app.set('trust proxy', trustProxy);
@@ -154,8 +385,9 @@ class SingleSessionHTTPServer {
154
385
  app.use((req, res, next) => {
155
386
  const allowedOrigin = process.env.CORS_ORIGIN || '*';
156
387
  res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
157
- res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
158
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept');
388
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, OPTIONS');
389
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, Mcp-Session-Id');
390
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
159
391
  res.setHeader('Access-Control-Max-Age', '86400');
160
392
  if (req.method === 'OPTIONS') {
161
393
  res.sendStatus(204);
@@ -201,15 +433,33 @@ class SingleSessionHTTPServer {
201
433
  });
202
434
  });
203
435
  app.get('/health', (req, res) => {
436
+ const activeTransports = Object.keys(this.transports);
437
+ const activeServers = Object.keys(this.servers);
438
+ const sessionMetrics = this.getSessionMetrics();
439
+ const isProduction = process.env.NODE_ENV === 'production';
440
+ const isDefaultToken = this.authToken === 'REPLACE_THIS_AUTH_TOKEN_32_CHARS_MIN_abcdefgh';
204
441
  res.json({
205
442
  status: 'ok',
206
- mode: 'single-session',
443
+ mode: 'sdk-pattern-transports',
207
444
  version: version_1.PROJECT_VERSION,
445
+ environment: process.env.NODE_ENV || 'development',
208
446
  uptime: Math.floor(process.uptime()),
209
- sessionActive: !!this.session,
210
- sessionAge: this.session
211
- ? Math.floor((Date.now() - this.session.lastAccess.getTime()) / 1000)
212
- : null,
447
+ sessions: {
448
+ active: sessionMetrics.activeSessions,
449
+ total: sessionMetrics.totalSessions,
450
+ expired: sessionMetrics.expiredSessions,
451
+ max: MAX_SESSIONS,
452
+ usage: `${sessionMetrics.activeSessions}/${MAX_SESSIONS}`,
453
+ sessionIds: activeTransports
454
+ },
455
+ security: {
456
+ production: isProduction,
457
+ defaultToken: isDefaultToken,
458
+ tokenLength: this.authToken?.length || 0
459
+ },
460
+ activeTransports: activeTransports.length,
461
+ activeServers: activeServers.length,
462
+ legacySessionActive: !!this.session,
213
463
  memory: {
214
464
  used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
215
465
  total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
@@ -218,7 +468,81 @@ class SingleSessionHTTPServer {
218
468
  timestamp: new Date().toISOString()
219
469
  });
220
470
  });
221
- app.get('/mcp', (req, res) => {
471
+ app.post('/mcp/test', jsonParser, async (req, res) => {
472
+ logger_1.logger.info('TEST ENDPOINT: Manual test request received', {
473
+ method: req.method,
474
+ headers: req.headers,
475
+ body: req.body,
476
+ bodyType: typeof req.body,
477
+ bodyContent: req.body ? JSON.stringify(req.body, null, 2) : 'undefined'
478
+ });
479
+ const negotiationResult = (0, protocol_version_1.negotiateProtocolVersion)(undefined, undefined, req.get('user-agent'), req.headers);
480
+ (0, protocol_version_1.logProtocolNegotiation)(negotiationResult, logger_1.logger, 'TEST_ENDPOINT');
481
+ const testResponse = {
482
+ jsonrpc: '2.0',
483
+ id: req.body?.id || 1,
484
+ result: {
485
+ protocolVersion: negotiationResult.version,
486
+ capabilities: {
487
+ tools: {}
488
+ },
489
+ serverInfo: {
490
+ name: 'n8n-mcp',
491
+ version: version_1.PROJECT_VERSION
492
+ }
493
+ }
494
+ };
495
+ logger_1.logger.info('TEST ENDPOINT: Sending test response', {
496
+ response: testResponse
497
+ });
498
+ res.json(testResponse);
499
+ });
500
+ app.get('/mcp', async (req, res) => {
501
+ const sessionId = req.headers['mcp-session-id'];
502
+ if (sessionId && this.transports[sessionId]) {
503
+ try {
504
+ await this.transports[sessionId].handleRequest(req, res, undefined);
505
+ return;
506
+ }
507
+ catch (error) {
508
+ logger_1.logger.error('StreamableHTTP GET request failed:', error);
509
+ }
510
+ }
511
+ const accept = req.headers.accept;
512
+ if (accept && accept.includes('text/event-stream')) {
513
+ logger_1.logger.info('SSE stream request received - establishing SSE connection');
514
+ try {
515
+ await this.resetSessionSSE(res);
516
+ logger_1.logger.info('SSE connection established successfully');
517
+ }
518
+ catch (error) {
519
+ logger_1.logger.error('Failed to establish SSE connection:', error);
520
+ res.status(500).json({
521
+ jsonrpc: '2.0',
522
+ error: {
523
+ code: -32603,
524
+ message: 'Failed to establish SSE connection'
525
+ },
526
+ id: null
527
+ });
528
+ }
529
+ return;
530
+ }
531
+ if (process.env.N8N_MODE === 'true') {
532
+ const negotiationResult = (0, protocol_version_1.negotiateProtocolVersion)(undefined, undefined, req.get('user-agent'), req.headers);
533
+ (0, protocol_version_1.logProtocolNegotiation)(negotiationResult, logger_1.logger, 'N8N_MODE_GET');
534
+ res.json({
535
+ protocolVersion: negotiationResult.version,
536
+ serverInfo: {
537
+ name: 'n8n-mcp',
538
+ version: version_1.PROJECT_VERSION,
539
+ capabilities: {
540
+ tools: {}
541
+ }
542
+ }
543
+ });
544
+ return;
545
+ }
222
546
  res.json({
223
547
  description: 'n8n Documentation MCP Server',
224
548
  version: version_1.PROJECT_VERSION,
@@ -245,7 +569,92 @@ class SingleSessionHTTPServer {
245
569
  documentation: 'https://github.com/czlonkowski/n8n-mcp'
246
570
  });
247
571
  });
248
- app.post('/mcp', async (req, res) => {
572
+ app.delete('/mcp', async (req, res) => {
573
+ const mcpSessionId = req.headers['mcp-session-id'];
574
+ if (!mcpSessionId) {
575
+ res.status(400).json({
576
+ jsonrpc: '2.0',
577
+ error: {
578
+ code: -32602,
579
+ message: 'Mcp-Session-Id header is required'
580
+ },
581
+ id: null
582
+ });
583
+ return;
584
+ }
585
+ if (!this.isValidSessionId(mcpSessionId)) {
586
+ res.status(400).json({
587
+ jsonrpc: '2.0',
588
+ error: {
589
+ code: -32602,
590
+ message: 'Invalid session ID format'
591
+ },
592
+ id: null
593
+ });
594
+ return;
595
+ }
596
+ if (this.transports[mcpSessionId]) {
597
+ logger_1.logger.info('Terminating session via DELETE request', { sessionId: mcpSessionId });
598
+ try {
599
+ await this.removeSession(mcpSessionId, 'manual_termination');
600
+ res.status(204).send();
601
+ }
602
+ catch (error) {
603
+ logger_1.logger.error('Error terminating session:', error);
604
+ res.status(500).json({
605
+ jsonrpc: '2.0',
606
+ error: {
607
+ code: -32603,
608
+ message: 'Error terminating session'
609
+ },
610
+ id: null
611
+ });
612
+ }
613
+ }
614
+ else {
615
+ res.status(404).json({
616
+ jsonrpc: '2.0',
617
+ error: {
618
+ code: -32001,
619
+ message: 'Session not found'
620
+ },
621
+ id: null
622
+ });
623
+ }
624
+ });
625
+ app.post('/mcp', jsonParser, async (req, res) => {
626
+ logger_1.logger.info('POST /mcp request received - DETAILED DEBUG', {
627
+ headers: req.headers,
628
+ readable: req.readable,
629
+ readableEnded: req.readableEnded,
630
+ complete: req.complete,
631
+ bodyType: typeof req.body,
632
+ bodyContent: req.body ? JSON.stringify(req.body, null, 2) : 'undefined',
633
+ contentLength: req.get('content-length'),
634
+ contentType: req.get('content-type'),
635
+ userAgent: req.get('user-agent'),
636
+ ip: req.ip,
637
+ method: req.method,
638
+ url: req.url,
639
+ originalUrl: req.originalUrl
640
+ });
641
+ const sessionId = req.headers['mcp-session-id'];
642
+ if (typeof req.on === 'function') {
643
+ req.on('close', () => {
644
+ if (!res.headersSent && sessionId) {
645
+ logger_1.logger.info('Connection closed before response sent', { sessionId });
646
+ setImmediate(() => {
647
+ if (this.sessionMetadata[sessionId]) {
648
+ const metadata = this.sessionMetadata[sessionId];
649
+ const timeSinceAccess = Date.now() - metadata.lastAccess.getTime();
650
+ if (timeSinceAccess > 60000) {
651
+ this.removeSession(sessionId, 'connection_closed');
652
+ }
653
+ }
654
+ });
655
+ }
656
+ });
657
+ }
249
658
  const authHeader = req.headers.authorization;
250
659
  if (!authHeader) {
251
660
  logger_1.logger.warn('Authentication failed: Missing Authorization header', {
@@ -268,7 +677,7 @@ class SingleSessionHTTPServer {
268
677
  ip: req.ip,
269
678
  userAgent: req.get('user-agent'),
270
679
  reason: 'invalid_auth_format',
271
- headerPrefix: authHeader.substring(0, 10) + '...'
680
+ headerPrefix: authHeader.substring(0, Math.min(authHeader.length, 10)) + '...'
272
681
  });
273
682
  res.status(401).json({
274
683
  jsonrpc: '2.0',
@@ -297,7 +706,17 @@ class SingleSessionHTTPServer {
297
706
  });
298
707
  return;
299
708
  }
709
+ logger_1.logger.info('Authentication successful - proceeding to handleRequest', {
710
+ hasSession: !!this.session,
711
+ sessionType: this.session?.isSSE ? 'SSE' : 'StreamableHTTP',
712
+ sessionInitialized: this.session?.initialized
713
+ });
300
714
  await this.handleRequest(req, res);
715
+ logger_1.logger.info('POST /mcp request completed - checking response status', {
716
+ responseHeadersSent: res.headersSent,
717
+ responseStatusCode: res.statusCode,
718
+ responseFinished: res.finished
719
+ });
301
720
  });
302
721
  app.use((req, res) => {
303
722
  res.status(404).json({
@@ -322,14 +741,32 @@ class SingleSessionHTTPServer {
322
741
  const port = parseInt(process.env.PORT || '3000');
323
742
  const host = process.env.HOST || '0.0.0.0';
324
743
  this.expressServer = app.listen(port, host, () => {
325
- logger_1.logger.info(`n8n MCP Single-Session HTTP Server started`, { port, host });
744
+ const isProduction = process.env.NODE_ENV === 'production';
745
+ const isDefaultToken = this.authToken === 'REPLACE_THIS_AUTH_TOKEN_32_CHARS_MIN_abcdefgh';
746
+ logger_1.logger.info(`n8n MCP Single-Session HTTP Server started`, {
747
+ port,
748
+ host,
749
+ environment: process.env.NODE_ENV || 'development',
750
+ maxSessions: MAX_SESSIONS,
751
+ sessionTimeout: this.sessionTimeout / 1000 / 60,
752
+ production: isProduction,
753
+ defaultToken: isDefaultToken
754
+ });
326
755
  const baseUrl = (0, url_detector_1.getStartupBaseUrl)(host, port);
327
756
  const endpoints = (0, url_detector_1.formatEndpointUrls)(baseUrl);
328
757
  console.log(`n8n MCP Single-Session HTTP Server running on ${host}:${port}`);
758
+ console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
759
+ console.log(`Session Limits: ${MAX_SESSIONS} max sessions, ${this.sessionTimeout / 1000 / 60}min timeout`);
329
760
  console.log(`Health check: ${endpoints.health}`);
330
761
  console.log(`MCP endpoint: ${endpoints.mcp}`);
762
+ if (isProduction) {
763
+ console.log('🔒 Running in PRODUCTION mode - enhanced security enabled');
764
+ }
765
+ else {
766
+ console.log('🛠️ Running in DEVELOPMENT mode');
767
+ }
331
768
  console.log('\nPress Ctrl+C to stop the server');
332
- if (this.authToken === 'REPLACE_THIS_AUTH_TOKEN_32_CHARS_MIN_abcdefgh') {
769
+ if (isDefaultToken && !isProduction) {
333
770
  setInterval(() => {
334
771
  logger_1.logger.warn('⚠️ Still using default AUTH_TOKEN - security risk!');
335
772
  if (process.env.MCP_MODE === 'http') {
@@ -359,13 +796,29 @@ class SingleSessionHTTPServer {
359
796
  }
360
797
  async shutdown() {
361
798
  logger_1.logger.info('Shutting down Single-Session HTTP server...');
799
+ if (this.cleanupTimer) {
800
+ clearInterval(this.cleanupTimer);
801
+ this.cleanupTimer = null;
802
+ logger_1.logger.info('Session cleanup timer stopped');
803
+ }
804
+ const sessionIds = Object.keys(this.transports);
805
+ logger_1.logger.info(`Closing ${sessionIds.length} active sessions`);
806
+ for (const sessionId of sessionIds) {
807
+ try {
808
+ logger_1.logger.info(`Closing transport for session ${sessionId}`);
809
+ await this.removeSession(sessionId, 'server_shutdown');
810
+ }
811
+ catch (error) {
812
+ logger_1.logger.warn(`Error closing transport for session ${sessionId}:`, error);
813
+ }
814
+ }
362
815
  if (this.session) {
363
816
  try {
364
817
  await this.session.transport.close();
365
- logger_1.logger.info('Session closed');
818
+ logger_1.logger.info('Legacy session closed');
366
819
  }
367
820
  catch (error) {
368
- logger_1.logger.warn('Error closing session:', error);
821
+ logger_1.logger.warn('Error closing legacy session:', error);
369
822
  }
370
823
  this.session = null;
371
824
  }
@@ -377,15 +830,33 @@ class SingleSessionHTTPServer {
377
830
  });
378
831
  });
379
832
  }
833
+ logger_1.logger.info('Single-Session HTTP server shutdown completed');
380
834
  }
381
835
  getSessionInfo() {
836
+ const metrics = this.getSessionMetrics();
382
837
  if (!this.session) {
383
- return { active: false };
838
+ return {
839
+ active: false,
840
+ sessions: {
841
+ total: metrics.totalSessions,
842
+ active: metrics.activeSessions,
843
+ expired: metrics.expiredSessions,
844
+ max: MAX_SESSIONS,
845
+ sessionIds: Object.keys(this.transports)
846
+ }
847
+ };
384
848
  }
385
849
  return {
386
850
  active: true,
387
851
  sessionId: this.session.sessionId,
388
- age: Date.now() - this.session.lastAccess.getTime()
852
+ age: Date.now() - this.session.lastAccess.getTime(),
853
+ sessions: {
854
+ total: metrics.totalSessions,
855
+ active: metrics.activeSessions,
856
+ expired: metrics.expiredSessions,
857
+ max: MAX_SESSIONS,
858
+ sessionIds: Object.keys(this.transports)
859
+ }
389
860
  };
390
861
  }
391
862
  }