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.
- package/README.md +29 -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 +530 -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 +391 -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/dist/utils/simple-cache.d.ts +2 -0
- package/dist/utils/simple-cache.d.ts.map +1 -1
- package/dist/utils/simple-cache.js +9 -1
- package/dist/utils/simple-cache.js.map +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
+
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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:',
|
|
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:
|
|
93
|
-
data:
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
363
|
+
sessionId,
|
|
364
|
+
initialized: false,
|
|
365
|
+
isSSE: true
|
|
127
366
|
};
|
|
128
|
-
logger_1.logger.info('Created new
|
|
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: '
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
:
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
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('
|
|
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 {
|
|
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
|
}
|