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.
- package/README.md +9 -1
- package/data/nodes.db +0 -0
- package/dist/http-server-single-session.d.ts +21 -1
- package/dist/http-server-single-session.d.ts.map +1 -1
- package/dist/http-server-single-session.js +515 -44
- package/dist/http-server-single-session.js.map +1 -1
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +5 -2
- package/dist/http-server.js.map +1 -1
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +382 -17
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools-n8n-friendly.d.ts +6 -0
- package/dist/mcp/tools-n8n-friendly.d.ts.map +1 -0
- package/dist/mcp/tools-n8n-friendly.js +131 -0
- package/dist/mcp/tools-n8n-friendly.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +187 -11
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/workflow-examples.d.ts +76 -0
- package/dist/mcp/workflow-examples.d.ts.map +1 -0
- package/dist/mcp/workflow-examples.js +111 -0
- package/dist/mcp/workflow-examples.js.map +1 -0
- package/dist/scripts/test-protocol-negotiation.d.ts +3 -0
- package/dist/scripts/test-protocol-negotiation.d.ts.map +1 -0
- package/dist/scripts/test-protocol-negotiation.js +154 -0
- package/dist/scripts/test-protocol-negotiation.js.map +1 -0
- package/dist/services/enhanced-config-validator.d.ts +4 -0
- package/dist/services/enhanced-config-validator.d.ts.map +1 -1
- package/dist/services/enhanced-config-validator.js +86 -1
- package/dist/services/enhanced-config-validator.js.map +1 -1
- package/dist/services/n8n-validation.d.ts +2 -2
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/fixed-collection-validator.d.ts +35 -0
- package/dist/utils/fixed-collection-validator.d.ts.map +1 -0
- package/dist/utils/fixed-collection-validator.js +358 -0
- package/dist/utils/fixed-collection-validator.js.map +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +6 -3
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/protocol-version.d.ts +19 -0
- package/dist/utils/protocol-version.d.ts.map +1 -0
- package/dist/utils/protocol-version.js +95 -0
- package/dist/utils/protocol-version.js.map +1 -0
- 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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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:',
|
|
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:
|
|
93
|
-
data:
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
354
|
+
sessionId,
|
|
355
|
+
initialized: false,
|
|
356
|
+
isSSE: true
|
|
127
357
|
};
|
|
128
|
-
logger_1.logger.info('Created new
|
|
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: '
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
:
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
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('
|
|
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 {
|
|
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
|
}
|