n8n-mcp
Version:
Integration between n8n workflow automation and Model Context Protocol (MCP)
652 lines • 26.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startSSEServer = startSSEServer;
const express_1 = __importDefault(require("express"));
const server_1 = require("./mcp/server");
const sse_session_manager_1 = require("./utils/sse-session-manager");
const logger_1 = require("./utils/logger");
const version_1 = require("./utils/version");
const tools_1 = require("./mcp/tools");
const tools_n8n_manager_1 = require("./mcp/tools-n8n-manager");
const n8n_api_1 = require("./config/n8n-api");
const http_server_1 = require("./http-server");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
const dotenv_1 = __importDefault(require("dotenv"));
dotenv_1.default.config();
let expressServer;
let authToken = null;
let sessionManager;
let mcpServer;
function validateEnvironment() {
authToken = (0, http_server_1.loadAuthToken)();
if (!authToken || authToken.trim() === '') {
logger_1.logger.error('No authentication token found or token is empty');
console.error('ERROR: AUTH_TOKEN is required for SSE mode and cannot be empty');
console.error('Set AUTH_TOKEN environment variable or AUTH_TOKEN_FILE pointing to a file containing the token');
console.error('Generate AUTH_TOKEN with: openssl rand -base64 32');
process.exit(1);
}
authToken = authToken.trim();
if (authToken.length < 32) {
logger_1.logger.warn('AUTH_TOKEN should be at least 32 characters for security');
console.warn('WARNING: AUTH_TOKEN should be at least 32 characters for security');
}
}
async function shutdown() {
logger_1.logger.info('Shutting down SSE server...');
console.log('Shutting down SSE server...');
if (sessionManager) {
sessionManager.shutdown();
}
if (expressServer) {
expressServer.close(() => {
logger_1.logger.info('SSE server closed');
console.log('SSE server closed');
process.exit(0);
});
setTimeout(() => {
logger_1.logger.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
}
else {
process.exit(0);
}
}
async function startSSEServer() {
validateEnvironment();
const app = (0, express_1.default)();
sessionManager = new sse_session_manager_1.SSESessionManager();
mcpServer = new server_1.N8NDocumentationMCPServer();
const trustProxy = process.env.TRUST_PROXY ? Number(process.env.TRUST_PROXY) : 0;
if (trustProxy > 0) {
app.set('trust proxy', trustProxy);
logger_1.logger.info(`Trust proxy enabled with ${trustProxy} hop(s)`);
}
app.use(express_1.default.json());
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});
app.use((req, res, next) => {
const allowedOrigin = process.env.CORS_ORIGIN || '*';
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, X-Client-ID, X-Auth-Token, X-API-Key');
res.setHeader('Access-Control-Max-Age', '86400');
res.setHeader('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
logger_1.logger.info('OPTIONS preflight request', {
path: req.path,
origin: req.headers.origin,
headers: req.headers
});
res.sendStatus(204);
return;
}
next();
});
app.use((req, res, next) => {
const logData = {
method: req.method,
path: req.path,
url: req.url,
ip: req.ip,
userAgent: req.get('user-agent'),
contentLength: req.get('content-length'),
headers: process.env.LOG_LEVEL === 'debug' ? req.headers : undefined,
query: req.query,
isSSERequest: req.headers.accept?.includes('text/event-stream') || false
};
logger_1.logger.info(`${req.method} ${req.path}`, logData);
if (req.path.includes('/sse') || req.path === '/mcp' || req.headers.accept?.includes('text/event-stream')) {
logger_1.logger.info('SSE connection attempt detected', {
path: req.path,
acceptHeader: req.headers.accept,
authHeader: req.headers.authorization ? 'present' : 'missing'
});
}
next();
});
const authenticateRequest = (req, res, next) => {
let token = null;
let authMethod = null;
logger_1.logger.debug('Authentication attempt', {
path: req.path,
headers: Object.keys(req.headers),
hasAuthHeader: !!req.headers.authorization,
hasQuery: !!req.query.token
});
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.slice(7).trim();
authMethod = 'Bearer';
logger_1.logger.debug('Using Bearer authentication');
}
if (!token) {
const customHeaderName = process.env.AUTH_HEADER_NAME || 'x-auth-token';
const customHeaderValue = req.headers[customHeaderName.toLowerCase()];
if (customHeaderValue && typeof customHeaderValue === 'string') {
token = customHeaderValue.trim();
authMethod = `Custom header (${customHeaderName})`;
logger_1.logger.debug(`Using custom header authentication: ${customHeaderName}`);
}
}
if (!token && req.query.token) {
token = req.query.token;
authMethod = 'Query parameter';
logger_1.logger.debug('Using query parameter authentication');
}
if (!token && req.headers['x-api-key']) {
token = req.headers['x-api-key'];
authMethod = 'API key header';
logger_1.logger.debug('Using API key header authentication');
}
if (!token) {
logger_1.logger.warn('Authentication failed: No token provided', {
ip: req.ip,
path: req.path,
headers: Object.keys(req.headers),
availableAuthMethods: ['Bearer', 'x-auth-token', 'query.token', 'x-api-key']
});
res.status(401).json({
error: 'Unauthorized',
message: 'No authentication token provided',
hint: 'Use Bearer token, x-auth-token header, query parameter ?token=, or x-api-key header'
});
return;
}
if (token !== authToken) {
logger_1.logger.warn('Authentication failed: Invalid token', {
ip: req.ip,
path: req.path,
authMethod: authMethod,
tokenReceived: true
});
res.status(401).json({
error: 'Unauthorized',
message: 'Invalid authentication token'
});
return;
}
logger_1.logger.debug('Authentication successful', {
path: req.path,
authMethod: authMethod
});
next();
};
app.get('/health', (req, res) => {
res.json({
status: 'ok',
mode: 'sse',
version: version_1.PROJECT_VERSION,
uptime: Math.floor(process.uptime()),
activeSessions: sessionManager.getActiveClientCount(),
memory: {
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
unit: 'MB'
},
timestamp: new Date().toISOString()
});
});
const handleSSE = (req, res) => {
const path = req.params.path || 'default';
logger_1.logger.info('SSE endpoint handler invoked', {
endpoint: req.path,
method: req.method,
acceptHeader: req.headers.accept,
userAgent: req.headers['user-agent'],
path: path
});
let clientId;
try {
clientId = sessionManager.registerClient(res);
}
catch (error) {
logger_1.logger.error('Failed to register SSE client:', error);
res.status(503).json({
error: 'Service Unavailable',
message: error instanceof Error ? error.message : 'Failed to establish SSE connection'
});
return;
}
logger_1.logger.info(`New SSE connection established: ${clientId} (path: ${path})`, {
totalClients: sessionManager.getActiveClientCount(),
headers: {
accept: req.headers.accept,
'content-type': req.headers['content-type'],
'user-agent': req.headers['user-agent']
}
});
const getStringParam = (value) => {
if (typeof value === 'string')
return value;
if (Array.isArray(value) && value.length > 0)
return String(value[0]);
return undefined;
};
const workflowContext = {
workflowId: req.headers['x-workflow-id'] || getStringParam(req.query.workflowId),
executionId: req.headers['x-execution-id'] || getStringParam(req.query.executionId),
nodeId: req.headers['x-node-id'] || getStringParam(req.query.nodeId),
nodeName: req.headers['x-node-name'] || getStringParam(req.query.nodeName),
runId: req.headers['x-run-id'] || getStringParam(req.query.runId),
};
const cleanContext = {};
for (const [key, value] of Object.entries(workflowContext)) {
if (value !== undefined) {
cleanContext[key] = value;
}
}
if (Object.keys(cleanContext).length > 0) {
sessionManager.updateWorkflowContext(clientId, cleanContext);
logger_1.logger.info(`Workflow context for client ${clientId}:`, cleanContext);
}
logger_1.logger.debug('Sending endpoint event', { clientId });
sessionManager.sendToClient(clientId, {
event: 'endpoint',
data: `/messages?session_id=${clientId}`
});
const pingInterval = setInterval(() => {
if (!sessionManager.hasClient(clientId)) {
clearInterval(pingInterval);
return;
}
sessionManager.sendPing(clientId);
}, 30000);
req.on('close', () => {
clearInterval(pingInterval);
logger_1.logger.info(`SSE connection closed: ${clientId}`, {
path: path,
remainingClients: sessionManager.getActiveClientCount() - 1
});
});
};
app.get('/sse', authenticateRequest, handleSSE);
app.get('/mcp', authenticateRequest, handleSSE);
app.get('/mcp/:path/sse', authenticateRequest, handleSSE);
const handleMessage = async (req, res) => {
const sessionId = req.query.session_id;
logger_1.logger.info('Message endpoint called', {
path: req.path,
sessionId,
headers: Object.keys(req.headers),
body: req.body
});
const clientId = sessionId;
if (!clientId || !sessionManager.hasClient(clientId)) {
res.status(400).json({
error: 'Invalid session',
message: 'Client ID not found or session expired'
});
return;
}
try {
const jsonRpcRequest = req.body;
const workflowContext = sessionManager.getWorkflowContext(clientId);
logger_1.logger.debug('Received MCP message:', {
clientId,
method: jsonRpcRequest.method,
id: jsonRpcRequest.id,
workflowContext
});
let response;
switch (jsonRpcRequest.method) {
case 'initialize':
response = {
jsonrpc: '2.0',
result: {
protocolVersion: types_js_1.DEFAULT_NEGOTIATED_PROTOCOL_VERSION,
capabilities: {
tools: {},
resources: {},
prompts: {}
},
serverInfo: {
name: 'n8n-documentation-mcp',
version: version_1.PROJECT_VERSION
}
},
id: jsonRpcRequest.id
};
break;
case 'tools/list':
const tools = [...tools_1.n8nDocumentationToolsFinal];
if ((0, n8n_api_1.isN8nApiConfigured)()) {
tools.push(...tools_n8n_manager_1.n8nManagementTools);
}
response = {
jsonrpc: '2.0',
result: {
tools
},
id: jsonRpcRequest.id
};
break;
case 'tools/call':
const toolName = jsonRpcRequest.params?.name;
const toolArgs = jsonRpcRequest.params?.arguments || {};
logger_1.logger.debug('Tool call details:', {
toolName,
toolArgs,
toolArgsType: typeof toolArgs,
toolArgsKeys: Object.keys(toolArgs),
rawParams: jsonRpcRequest.params
});
try {
const result = await mcpServer.executeTool(toolName, toolArgs);
response = {
jsonrpc: '2.0',
result: {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
},
id: jsonRpcRequest.id
};
}
catch (error) {
logger_1.logger.error(`Error executing tool ${toolName}:`, error);
response = {
jsonrpc: '2.0',
error: {
code: -32603,
message: `Error executing tool ${toolName}: ${error instanceof Error ? error.message : String(error)}`
},
id: jsonRpcRequest.id
};
}
break;
case 'resources/list':
response = {
jsonrpc: '2.0',
result: {
resources: []
},
id: jsonRpcRequest.id
};
break;
case 'resources/read':
response = {
jsonrpc: '2.0',
error: {
code: -32601,
message: 'Resource reading not implemented'
},
id: jsonRpcRequest.id
};
break;
case 'prompts/list':
response = {
jsonrpc: '2.0',
result: {
prompts: []
},
id: jsonRpcRequest.id
};
break;
case 'prompts/get':
response = {
jsonrpc: '2.0',
error: {
code: -32601,
message: 'Prompt retrieval not implemented'
},
id: jsonRpcRequest.id
};
break;
default:
response = {
jsonrpc: '2.0',
error: {
code: -32601,
message: `Method not found: ${jsonRpcRequest.method}`
},
id: jsonRpcRequest.id
};
}
sessionManager.sendMCPMessage(clientId, response);
res.json({
status: 'ok',
messageId: jsonRpcRequest.id
});
}
catch (error) {
logger_1.logger.error('Error processing MCP message:', error);
res.status(500).json({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
};
const handleLegacyMCP = async (req, res) => {
try {
const jsonRpcRequest = req.body;
logger_1.logger.debug('Received legacy MCP request:', { method: jsonRpcRequest.method });
let response;
switch (jsonRpcRequest.method) {
case 'initialize':
response = {
jsonrpc: '2.0',
result: {
protocolVersion: types_js_1.DEFAULT_NEGOTIATED_PROTOCOL_VERSION,
capabilities: {
tools: {},
resources: {},
prompts: {}
},
serverInfo: {
name: 'n8n-documentation-mcp',
version: version_1.PROJECT_VERSION
}
},
id: jsonRpcRequest.id
};
break;
case 'tools/list':
const tools = [...tools_1.n8nDocumentationToolsFinal];
if ((0, n8n_api_1.isN8nApiConfigured)()) {
tools.push(...tools_n8n_manager_1.n8nManagementTools);
}
response = {
jsonrpc: '2.0',
result: {
tools
},
id: jsonRpcRequest.id
};
break;
case 'tools/call':
const toolName = jsonRpcRequest.params?.name;
const toolArgs = jsonRpcRequest.params?.arguments || {};
logger_1.logger.debug('Tool call details:', {
toolName,
toolArgs,
toolArgsType: typeof toolArgs,
toolArgsKeys: Object.keys(toolArgs),
rawParams: jsonRpcRequest.params
});
try {
const result = await mcpServer.executeTool(toolName, toolArgs);
response = {
jsonrpc: '2.0',
result: {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
},
id: jsonRpcRequest.id
};
}
catch (error) {
logger_1.logger.error(`Error executing tool ${toolName}:`, error);
response = {
jsonrpc: '2.0',
error: {
code: -32603,
message: `Error executing tool ${toolName}: ${error instanceof Error ? error.message : String(error)}`
},
id: jsonRpcRequest.id
};
}
break;
case 'resources/list':
response = {
jsonrpc: '2.0',
result: {
resources: []
},
id: jsonRpcRequest.id
};
break;
case 'resources/read':
response = {
jsonrpc: '2.0',
error: {
code: -32601,
message: 'Resource reading not implemented'
},
id: jsonRpcRequest.id
};
break;
case 'prompts/list':
response = {
jsonrpc: '2.0',
result: {
prompts: []
},
id: jsonRpcRequest.id
};
break;
case 'prompts/get':
response = {
jsonrpc: '2.0',
error: {
code: -32601,
message: 'Prompt retrieval not implemented'
},
id: jsonRpcRequest.id
};
break;
default:
response = {
jsonrpc: '2.0',
error: {
code: -32601,
message: `Method not found: ${jsonRpcRequest.method}`
},
id: jsonRpcRequest.id
};
}
res.json(response);
}
catch (error) {
logger_1.logger.error('Legacy MCP request error:', error);
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
data: error instanceof Error ? error.message : undefined
},
id: null
});
}
};
app.post('/messages', authenticateRequest, handleMessage);
app.post('/mcp/message', authenticateRequest, handleMessage);
app.post('/mcp/:path/message', authenticateRequest, handleMessage);
app.post('/mcp', authenticateRequest, handleLegacyMCP);
app.post('/mcp/:path', authenticateRequest, handleLegacyMCP);
app.post('/sse', authenticateRequest, handleLegacyMCP);
app.use((req, res, next) => {
const isKnownRoute = ['/health', '/sse', '/mcp', '/mcp/message'].some(route => req.path === route || req.path.startsWith(route + '/'));
if (!isKnownRoute) {
logger_1.logger.warn('Unmatched request', {
method: req.method,
path: req.path,
url: req.url,
headers: Object.keys(req.headers),
hasAuth: !!req.headers.authorization,
ip: req.ip
});
}
next();
});
app.use((req, res) => {
res.status(404).json({
error: 'Not found',
message: `Cannot ${req.method} ${req.path}`
});
});
app.use((err, req, res, next) => {
logger_1.logger.error('Express error handler:', err);
if (!res.headersSent) {
res.status(500).json({
error: 'Internal server error',
message: process.env.NODE_ENV === 'development' ? err.message : 'An error occurred'
});
}
});
const port = parseInt(process.env.PORT || '3000');
const host = process.env.HOST || '0.0.0.0';
expressServer = app.listen(port, host, () => {
logger_1.logger.info(`n8n MCP SSE Server started`, { port, host });
console.log(`n8n MCP SSE Server running on ${host}:${port}`);
console.log(`Health check: http://localhost:${port}/health`);
console.log(`SSE endpoints:`);
console.log(` - http://localhost:${port}/sse`);
console.log(` - http://localhost:${port}/mcp`);
console.log(` - http://localhost:${port}/mcp/{path}/sse`);
console.log(`Message endpoints:`);
console.log(` - http://localhost:${port}/messages?session_id={session_id}`);
console.log(` - http://localhost:${port}/mcp/message (legacy)`);
console.log('\nPress Ctrl+C to stop the server');
});
expressServer.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
logger_1.logger.error(`Port ${port} is already in use`);
console.error(`ERROR: Port ${port} is already in use`);
process.exit(1);
}
else {
logger_1.logger.error('Server error:', error);
console.error('Server error:', error);
process.exit(1);
}
});
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
process.on('uncaughtException', (error) => {
logger_1.logger.error('Uncaught exception:', error);
console.error('Uncaught exception:', error);
shutdown();
});
process.on('unhandledRejection', (reason, promise) => {
logger_1.logger.error('Unhandled rejection:', reason);
console.error('Unhandled rejection at:', promise, 'reason:', reason);
shutdown();
});
}
if (require.main === module) {
startSSEServer().catch(error => {
logger_1.logger.error('Failed to start SSE server:', error);
console.error('Failed to start SSE server:', error);
process.exit(1);
});
}
//# sourceMappingURL=sse-server.js.map