n8n-mcp 2.8.1 → 2.10.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 (51) hide show
  1. package/README.md +29 -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 +530 -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 +391 -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/dist/utils/simple-cache.d.ts +2 -0
  48. package/dist/utils/simple-cache.d.ts.map +1 -1
  49. package/dist/utils/simple-cache.js +9 -1
  50. package/dist/utils/simple-cache.js.map +1 -1
  51. package/package.json +4 -6
@@ -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,156 @@ 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
+ transport.onerror = (error) => {
249
+ const sid = transport.sessionId;
250
+ logger_1.logger.error('Transport error', { sessionId: sid, error: error.message });
251
+ if (sid) {
252
+ this.removeSession(sid, 'transport_error').catch(err => {
253
+ logger_1.logger.error('Error during transport error cleanup', { error: err });
254
+ });
255
+ }
256
+ };
257
+ logger_1.logger.info('handleRequest: Connecting server to new transport');
258
+ await server.connect(transport);
74
259
  }
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
260
+ else if (sessionId && this.transports[sessionId]) {
261
+ if (!this.isValidSessionId(sessionId)) {
262
+ logger_1.logger.warn('handleRequest: Invalid session ID format', { sessionId });
263
+ res.status(400).json({
264
+ jsonrpc: '2.0',
265
+ error: {
266
+ code: -32602,
267
+ message: 'Invalid session ID format'
268
+ },
269
+ id: req.body?.id || null
270
+ });
271
+ return;
272
+ }
273
+ logger_1.logger.info('handleRequest: Reusing existing transport for session', { sessionId });
274
+ transport = this.transports[sessionId];
275
+ this.updateSessionAccess(sessionId);
276
+ }
277
+ else {
278
+ const errorDetails = {
279
+ hasSessionId: !!sessionId,
280
+ isInitialize: isInitialize,
281
+ sessionIdValid: sessionId ? this.isValidSessionId(sessionId) : false,
282
+ sessionExists: sessionId ? !!this.transports[sessionId] : false
283
+ };
284
+ logger_1.logger.warn('handleRequest: Invalid request - no session ID and not initialize', errorDetails);
285
+ let errorMessage = 'Bad Request: No valid session ID provided and not an initialize request';
286
+ if (sessionId && !this.isValidSessionId(sessionId)) {
287
+ errorMessage = 'Bad Request: Invalid session ID format';
288
+ }
289
+ else if (sessionId && !this.transports[sessionId]) {
290
+ errorMessage = 'Bad Request: Session not found or expired';
291
+ }
292
+ res.status(400).json({
293
+ jsonrpc: '2.0',
294
+ error: {
295
+ code: -32000,
296
+ message: errorMessage
297
+ },
298
+ id: req.body?.id || null
299
+ });
300
+ return;
301
+ }
302
+ logger_1.logger.info('handleRequest: Handling request with transport', {
303
+ sessionId: isInitialize ? 'new' : sessionId,
304
+ isInitialize
83
305
  });
306
+ await transport.handleRequest(req, res, req.body);
307
+ const duration = Date.now() - startTime;
308
+ logger_1.logger.info('MCP request completed', { duration, sessionId: transport.sessionId });
84
309
  }
85
310
  catch (error) {
86
- logger_1.logger.error('MCP request error:', error);
311
+ logger_1.logger.error('handleRequest: MCP request error:', {
312
+ error: error instanceof Error ? error.message : error,
313
+ errorName: error instanceof Error ? error.name : 'Unknown',
314
+ stack: error instanceof Error ? error.stack : undefined,
315
+ activeTransports: Object.keys(this.transports),
316
+ requestDetails: {
317
+ method: req.method,
318
+ url: req.url,
319
+ hasBody: !!req.body,
320
+ sessionId: req.headers['mcp-session-id']
321
+ },
322
+ duration: Date.now() - startTime
323
+ });
87
324
  if (!res.headersSent) {
325
+ const sanitizedError = this.sanitizeErrorForClient(error);
88
326
  res.status(500).json({
89
327
  jsonrpc: '2.0',
90
328
  error: {
91
329
  code: -32603,
92
- message: 'Internal server error',
93
- data: process.env.NODE_ENV === 'development'
94
- ? error.message
95
- : undefined
330
+ message: sanitizedError.message,
331
+ data: {
332
+ code: sanitizedError.code
333
+ }
96
334
  },
97
- id: null
335
+ id: req.body?.id || null
98
336
  });
99
337
  }
100
338
  }
101
339
  });
102
340
  }
103
- async resetSession() {
341
+ async resetSessionSSE(res) {
104
342
  if (this.session) {
105
343
  try {
106
- logger_1.logger.info('Closing previous session', { sessionId: this.session.sessionId });
344
+ logger_1.logger.info('Closing previous session for SSE', { sessionId: this.session.sessionId });
107
345
  await this.session.transport.close();
108
346
  }
109
347
  catch (error) {
@@ -111,24 +349,25 @@ class SingleSessionHTTPServer {
111
349
  }
112
350
  }
113
351
  try {
114
- logger_1.logger.info('Creating new N8NDocumentationMCPServer...');
352
+ logger_1.logger.info('Creating new N8NDocumentationMCPServer for SSE...');
115
353
  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...');
354
+ const sessionId = (0, uuid_1.v4)();
355
+ logger_1.logger.info('Creating SSEServerTransport...');
356
+ const transport = new sse_js_1.SSEServerTransport('/mcp', res);
357
+ logger_1.logger.info('Connecting server to SSE transport...');
121
358
  await server.connect(transport);
122
359
  this.session = {
123
360
  server,
124
361
  transport,
125
362
  lastAccess: new Date(),
126
- sessionId: 'single-session'
363
+ sessionId,
364
+ initialized: false,
365
+ isSSE: true
127
366
  };
128
- logger_1.logger.info('Created new single session successfully', { sessionId: this.session.sessionId });
367
+ logger_1.logger.info('Created new SSE session successfully', { sessionId: this.session.sessionId });
129
368
  }
130
369
  catch (error) {
131
- logger_1.logger.error('Failed to create session:', error);
370
+ logger_1.logger.error('Failed to create SSE session:', error);
132
371
  throw error;
133
372
  }
134
373
  }
@@ -139,6 +378,7 @@ class SingleSessionHTTPServer {
139
378
  }
140
379
  async start() {
141
380
  const app = (0, express_1.default)();
381
+ const jsonParser = express_1.default.json({ limit: '10mb' });
142
382
  const trustProxy = process.env.TRUST_PROXY ? Number(process.env.TRUST_PROXY) : 0;
143
383
  if (trustProxy > 0) {
144
384
  app.set('trust proxy', trustProxy);
@@ -154,8 +394,9 @@ class SingleSessionHTTPServer {
154
394
  app.use((req, res, next) => {
155
395
  const allowedOrigin = process.env.CORS_ORIGIN || '*';
156
396
  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');
397
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, OPTIONS');
398
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, Mcp-Session-Id');
399
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
159
400
  res.setHeader('Access-Control-Max-Age', '86400');
160
401
  if (req.method === 'OPTIONS') {
161
402
  res.sendStatus(204);
@@ -201,15 +442,33 @@ class SingleSessionHTTPServer {
201
442
  });
202
443
  });
203
444
  app.get('/health', (req, res) => {
445
+ const activeTransports = Object.keys(this.transports);
446
+ const activeServers = Object.keys(this.servers);
447
+ const sessionMetrics = this.getSessionMetrics();
448
+ const isProduction = process.env.NODE_ENV === 'production';
449
+ const isDefaultToken = this.authToken === 'REPLACE_THIS_AUTH_TOKEN_32_CHARS_MIN_abcdefgh';
204
450
  res.json({
205
451
  status: 'ok',
206
- mode: 'single-session',
452
+ mode: 'sdk-pattern-transports',
207
453
  version: version_1.PROJECT_VERSION,
454
+ environment: process.env.NODE_ENV || 'development',
208
455
  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,
456
+ sessions: {
457
+ active: sessionMetrics.activeSessions,
458
+ total: sessionMetrics.totalSessions,
459
+ expired: sessionMetrics.expiredSessions,
460
+ max: MAX_SESSIONS,
461
+ usage: `${sessionMetrics.activeSessions}/${MAX_SESSIONS}`,
462
+ sessionIds: activeTransports
463
+ },
464
+ security: {
465
+ production: isProduction,
466
+ defaultToken: isDefaultToken,
467
+ tokenLength: this.authToken?.length || 0
468
+ },
469
+ activeTransports: activeTransports.length,
470
+ activeServers: activeServers.length,
471
+ legacySessionActive: !!this.session,
213
472
  memory: {
214
473
  used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
215
474
  total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
@@ -218,7 +477,81 @@ class SingleSessionHTTPServer {
218
477
  timestamp: new Date().toISOString()
219
478
  });
220
479
  });
221
- app.get('/mcp', (req, res) => {
480
+ app.post('/mcp/test', jsonParser, async (req, res) => {
481
+ logger_1.logger.info('TEST ENDPOINT: Manual test request received', {
482
+ method: req.method,
483
+ headers: req.headers,
484
+ body: req.body,
485
+ bodyType: typeof req.body,
486
+ bodyContent: req.body ? JSON.stringify(req.body, null, 2) : 'undefined'
487
+ });
488
+ const negotiationResult = (0, protocol_version_1.negotiateProtocolVersion)(undefined, undefined, req.get('user-agent'), req.headers);
489
+ (0, protocol_version_1.logProtocolNegotiation)(negotiationResult, logger_1.logger, 'TEST_ENDPOINT');
490
+ const testResponse = {
491
+ jsonrpc: '2.0',
492
+ id: req.body?.id || 1,
493
+ result: {
494
+ protocolVersion: negotiationResult.version,
495
+ capabilities: {
496
+ tools: {}
497
+ },
498
+ serverInfo: {
499
+ name: 'n8n-mcp',
500
+ version: version_1.PROJECT_VERSION
501
+ }
502
+ }
503
+ };
504
+ logger_1.logger.info('TEST ENDPOINT: Sending test response', {
505
+ response: testResponse
506
+ });
507
+ res.json(testResponse);
508
+ });
509
+ app.get('/mcp', async (req, res) => {
510
+ const sessionId = req.headers['mcp-session-id'];
511
+ if (sessionId && this.transports[sessionId]) {
512
+ try {
513
+ await this.transports[sessionId].handleRequest(req, res, undefined);
514
+ return;
515
+ }
516
+ catch (error) {
517
+ logger_1.logger.error('StreamableHTTP GET request failed:', error);
518
+ }
519
+ }
520
+ const accept = req.headers.accept;
521
+ if (accept && accept.includes('text/event-stream')) {
522
+ logger_1.logger.info('SSE stream request received - establishing SSE connection');
523
+ try {
524
+ await this.resetSessionSSE(res);
525
+ logger_1.logger.info('SSE connection established successfully');
526
+ }
527
+ catch (error) {
528
+ logger_1.logger.error('Failed to establish SSE connection:', error);
529
+ res.status(500).json({
530
+ jsonrpc: '2.0',
531
+ error: {
532
+ code: -32603,
533
+ message: 'Failed to establish SSE connection'
534
+ },
535
+ id: null
536
+ });
537
+ }
538
+ return;
539
+ }
540
+ if (process.env.N8N_MODE === 'true') {
541
+ const negotiationResult = (0, protocol_version_1.negotiateProtocolVersion)(undefined, undefined, req.get('user-agent'), req.headers);
542
+ (0, protocol_version_1.logProtocolNegotiation)(negotiationResult, logger_1.logger, 'N8N_MODE_GET');
543
+ res.json({
544
+ protocolVersion: negotiationResult.version,
545
+ serverInfo: {
546
+ name: 'n8n-mcp',
547
+ version: version_1.PROJECT_VERSION,
548
+ capabilities: {
549
+ tools: {}
550
+ }
551
+ }
552
+ });
553
+ return;
554
+ }
222
555
  res.json({
223
556
  description: 'n8n Documentation MCP Server',
224
557
  version: version_1.PROJECT_VERSION,
@@ -245,7 +578,98 @@ class SingleSessionHTTPServer {
245
578
  documentation: 'https://github.com/czlonkowski/n8n-mcp'
246
579
  });
247
580
  });
248
- app.post('/mcp', async (req, res) => {
581
+ app.delete('/mcp', async (req, res) => {
582
+ const mcpSessionId = req.headers['mcp-session-id'];
583
+ if (!mcpSessionId) {
584
+ res.status(400).json({
585
+ jsonrpc: '2.0',
586
+ error: {
587
+ code: -32602,
588
+ message: 'Mcp-Session-Id header is required'
589
+ },
590
+ id: null
591
+ });
592
+ return;
593
+ }
594
+ if (!this.isValidSessionId(mcpSessionId)) {
595
+ res.status(400).json({
596
+ jsonrpc: '2.0',
597
+ error: {
598
+ code: -32602,
599
+ message: 'Invalid session ID format'
600
+ },
601
+ id: null
602
+ });
603
+ return;
604
+ }
605
+ if (this.transports[mcpSessionId]) {
606
+ logger_1.logger.info('Terminating session via DELETE request', { sessionId: mcpSessionId });
607
+ try {
608
+ await this.removeSession(mcpSessionId, 'manual_termination');
609
+ res.status(204).send();
610
+ }
611
+ catch (error) {
612
+ logger_1.logger.error('Error terminating session:', error);
613
+ res.status(500).json({
614
+ jsonrpc: '2.0',
615
+ error: {
616
+ code: -32603,
617
+ message: 'Error terminating session'
618
+ },
619
+ id: null
620
+ });
621
+ }
622
+ }
623
+ else {
624
+ res.status(404).json({
625
+ jsonrpc: '2.0',
626
+ error: {
627
+ code: -32001,
628
+ message: 'Session not found'
629
+ },
630
+ id: null
631
+ });
632
+ }
633
+ });
634
+ app.post('/mcp', jsonParser, async (req, res) => {
635
+ logger_1.logger.info('POST /mcp request received - DETAILED DEBUG', {
636
+ headers: req.headers,
637
+ readable: req.readable,
638
+ readableEnded: req.readableEnded,
639
+ complete: req.complete,
640
+ bodyType: typeof req.body,
641
+ bodyContent: req.body ? JSON.stringify(req.body, null, 2) : 'undefined',
642
+ contentLength: req.get('content-length'),
643
+ contentType: req.get('content-type'),
644
+ userAgent: req.get('user-agent'),
645
+ ip: req.ip,
646
+ method: req.method,
647
+ url: req.url,
648
+ originalUrl: req.originalUrl
649
+ });
650
+ const sessionId = req.headers['mcp-session-id'];
651
+ if (typeof req.on === 'function') {
652
+ const closeHandler = () => {
653
+ if (!res.headersSent && sessionId) {
654
+ logger_1.logger.info('Connection closed before response sent', { sessionId });
655
+ setImmediate(() => {
656
+ if (this.sessionMetadata[sessionId]) {
657
+ const metadata = this.sessionMetadata[sessionId];
658
+ const timeSinceAccess = Date.now() - metadata.lastAccess.getTime();
659
+ if (timeSinceAccess > 60000) {
660
+ this.removeSession(sessionId, 'connection_closed').catch(err => {
661
+ logger_1.logger.error('Error during connection close cleanup', { error: err });
662
+ });
663
+ }
664
+ }
665
+ });
666
+ }
667
+ };
668
+ req.on('close', closeHandler);
669
+ res.on('finish', () => {
670
+ req.removeListener('close', closeHandler);
671
+ });
672
+ }
249
673
  const authHeader = req.headers.authorization;
250
674
  if (!authHeader) {
251
675
  logger_1.logger.warn('Authentication failed: Missing Authorization header', {
@@ -268,7 +692,7 @@ class SingleSessionHTTPServer {
268
692
  ip: req.ip,
269
693
  userAgent: req.get('user-agent'),
270
694
  reason: 'invalid_auth_format',
271
- headerPrefix: authHeader.substring(0, 10) + '...'
695
+ headerPrefix: authHeader.substring(0, Math.min(authHeader.length, 10)) + '...'
272
696
  });
273
697
  res.status(401).json({
274
698
  jsonrpc: '2.0',
@@ -297,7 +721,17 @@ class SingleSessionHTTPServer {
297
721
  });
298
722
  return;
299
723
  }
724
+ logger_1.logger.info('Authentication successful - proceeding to handleRequest', {
725
+ hasSession: !!this.session,
726
+ sessionType: this.session?.isSSE ? 'SSE' : 'StreamableHTTP',
727
+ sessionInitialized: this.session?.initialized
728
+ });
300
729
  await this.handleRequest(req, res);
730
+ logger_1.logger.info('POST /mcp request completed - checking response status', {
731
+ responseHeadersSent: res.headersSent,
732
+ responseStatusCode: res.statusCode,
733
+ responseFinished: res.finished
734
+ });
301
735
  });
302
736
  app.use((req, res) => {
303
737
  res.status(404).json({
@@ -322,14 +756,32 @@ class SingleSessionHTTPServer {
322
756
  const port = parseInt(process.env.PORT || '3000');
323
757
  const host = process.env.HOST || '0.0.0.0';
324
758
  this.expressServer = app.listen(port, host, () => {
325
- logger_1.logger.info(`n8n MCP Single-Session HTTP Server started`, { port, host });
759
+ const isProduction = process.env.NODE_ENV === 'production';
760
+ const isDefaultToken = this.authToken === 'REPLACE_THIS_AUTH_TOKEN_32_CHARS_MIN_abcdefgh';
761
+ logger_1.logger.info(`n8n MCP Single-Session HTTP Server started`, {
762
+ port,
763
+ host,
764
+ environment: process.env.NODE_ENV || 'development',
765
+ maxSessions: MAX_SESSIONS,
766
+ sessionTimeout: this.sessionTimeout / 1000 / 60,
767
+ production: isProduction,
768
+ defaultToken: isDefaultToken
769
+ });
326
770
  const baseUrl = (0, url_detector_1.getStartupBaseUrl)(host, port);
327
771
  const endpoints = (0, url_detector_1.formatEndpointUrls)(baseUrl);
328
772
  console.log(`n8n MCP Single-Session HTTP Server running on ${host}:${port}`);
773
+ console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
774
+ console.log(`Session Limits: ${MAX_SESSIONS} max sessions, ${this.sessionTimeout / 1000 / 60}min timeout`);
329
775
  console.log(`Health check: ${endpoints.health}`);
330
776
  console.log(`MCP endpoint: ${endpoints.mcp}`);
777
+ if (isProduction) {
778
+ console.log('🔒 Running in PRODUCTION mode - enhanced security enabled');
779
+ }
780
+ else {
781
+ console.log('🛠️ Running in DEVELOPMENT mode');
782
+ }
331
783
  console.log('\nPress Ctrl+C to stop the server');
332
- if (this.authToken === 'REPLACE_THIS_AUTH_TOKEN_32_CHARS_MIN_abcdefgh') {
784
+ if (isDefaultToken && !isProduction) {
333
785
  setInterval(() => {
334
786
  logger_1.logger.warn('⚠️ Still using default AUTH_TOKEN - security risk!');
335
787
  if (process.env.MCP_MODE === 'http') {
@@ -359,13 +811,29 @@ class SingleSessionHTTPServer {
359
811
  }
360
812
  async shutdown() {
361
813
  logger_1.logger.info('Shutting down Single-Session HTTP server...');
814
+ if (this.cleanupTimer) {
815
+ clearInterval(this.cleanupTimer);
816
+ this.cleanupTimer = null;
817
+ logger_1.logger.info('Session cleanup timer stopped');
818
+ }
819
+ const sessionIds = Object.keys(this.transports);
820
+ logger_1.logger.info(`Closing ${sessionIds.length} active sessions`);
821
+ for (const sessionId of sessionIds) {
822
+ try {
823
+ logger_1.logger.info(`Closing transport for session ${sessionId}`);
824
+ await this.removeSession(sessionId, 'server_shutdown');
825
+ }
826
+ catch (error) {
827
+ logger_1.logger.warn(`Error closing transport for session ${sessionId}:`, error);
828
+ }
829
+ }
362
830
  if (this.session) {
363
831
  try {
364
832
  await this.session.transport.close();
365
- logger_1.logger.info('Session closed');
833
+ logger_1.logger.info('Legacy session closed');
366
834
  }
367
835
  catch (error) {
368
- logger_1.logger.warn('Error closing session:', error);
836
+ logger_1.logger.warn('Error closing legacy session:', error);
369
837
  }
370
838
  this.session = null;
371
839
  }
@@ -377,15 +845,33 @@ class SingleSessionHTTPServer {
377
845
  });
378
846
  });
379
847
  }
848
+ logger_1.logger.info('Single-Session HTTP server shutdown completed');
380
849
  }
381
850
  getSessionInfo() {
851
+ const metrics = this.getSessionMetrics();
382
852
  if (!this.session) {
383
- return { active: false };
853
+ return {
854
+ active: false,
855
+ sessions: {
856
+ total: metrics.totalSessions,
857
+ active: metrics.activeSessions,
858
+ expired: metrics.expiredSessions,
859
+ max: MAX_SESSIONS,
860
+ sessionIds: Object.keys(this.transports)
861
+ }
862
+ };
384
863
  }
385
864
  return {
386
865
  active: true,
387
866
  sessionId: this.session.sessionId,
388
- age: Date.now() - this.session.lastAccess.getTime()
867
+ age: Date.now() - this.session.lastAccess.getTime(),
868
+ sessions: {
869
+ total: metrics.totalSessions,
870
+ active: metrics.activeSessions,
871
+ expired: metrics.expiredSessions,
872
+ max: MAX_SESSIONS,
873
+ sessionIds: Object.keys(this.transports)
874
+ }
389
875
  };
390
876
  }
391
877
  }