feat: add HTTP server mode for remote deployment with token auth

This commit is contained in:
czlonkowski
2025-06-13 11:54:42 +02:00
parent 23a21071bc
commit 89e1df03de
11 changed files with 512 additions and 48 deletions

117
src/http-server.ts Normal file
View File

@@ -0,0 +1,117 @@
#!/usr/bin/env node
/**
* Minimal HTTP server for n8n-MCP
* Single-user, stateless design for private deployments
*/
import express from 'express';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { N8NDocumentationMCPServer } from './mcp/server-update';
import { logger } from './utils/logger';
import dotenv from 'dotenv';
dotenv.config();
export async function startHTTPServer() {
const app = express();
app.use(express.json({ limit: '10mb' }));
// Simple token auth
const authToken = process.env.AUTH_TOKEN;
if (!authToken) {
logger.error('AUTH_TOKEN environment variable required');
console.error('ERROR: AUTH_TOKEN environment variable is required for HTTP mode');
console.error('Generate one with: openssl rand -base64 32');
process.exit(1);
}
// Request logging middleware
app.use((req, res, next) => {
logger.info(`${req.method} ${req.path}`, {
ip: req.ip,
userAgent: req.get('user-agent')
});
next();
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
mode: 'http',
version: '2.3.0'
});
});
// Main MCP endpoint - Create a new server and transport for each request (stateless)
app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => {
// Simple auth check
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith('Bearer ')
? authHeader.slice(7)
: authHeader;
if (token !== authToken) {
logger.warn('Authentication failed', { ip: req.ip });
res.status(401).json({ error: 'Unauthorized' });
return;
}
// Create new instances for each request (stateless)
const mcpServer = new N8NDocumentationMCPServer();
try {
// Create a stateless transport
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Stateless mode
});
// Connect server to transport
await mcpServer.connect(transport);
// Handle the request
await transport.handleRequest(req, res, req.body);
// Clean up on close
res.on('close', () => {
logger.debug('Request closed, cleaning up');
transport.close();
});
} catch (error) {
logger.error('MCP request error:', error);
if (!res.headersSent) {
res.status(500).json({
error: 'Internal server error',
message: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined
});
}
}
});
// Error handler
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.error('Request error:', err);
res.status(500).json({
error: 'Internal server error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
const port = parseInt(process.env.PORT || '3000');
const host = process.env.HOST || '0.0.0.0';
app.listen(port, host, () => {
logger.info(`n8n MCP HTTP Server started`, { port, host });
console.log(`n8n MCP HTTP Server running on ${host}:${port}`);
console.log(`Health check: http://localhost:${port}/health`);
console.log(`MCP endpoint: http://localhost:${port}/mcp`);
});
}
// Start if called directly
if (require.main === module) {
startHTTPServer().catch(error => {
logger.error('Failed to start HTTP server:', error);
console.error('Failed to start HTTP server:', error);
process.exit(1);
});
}

View File

@@ -18,13 +18,21 @@ process.on('unhandledRejection', (reason, promise) => {
async function main() {
try {
console.error('Starting n8n Documentation MCP Server...');
const mode = process.env.MCP_MODE || 'stdio';
console.error(`Starting n8n Documentation MCP Server in ${mode} mode...`);
console.error('Current directory:', process.cwd());
console.error('Script directory:', __dirname);
console.error('Node version:', process.version);
const server = new N8NDocumentationMCPServer();
await server.run();
if (mode === 'http') {
// HTTP mode - for remote deployment
const { startHTTPServer } = await import('../http-server');
await startHTTPServer();
} else {
// Stdio mode - for local Claude Desktop
const server = new N8NDocumentationMCPServer();
await server.run();
}
} catch (error) {
console.error('Failed to start MCP server:', error);
logger.error('Failed to start MCP server', error);

View File

@@ -150,7 +150,9 @@ export class N8NDocumentationMCPServer {
}
}
private listNodes(filters: any = {}): any {
private async listNodes(filters: any = {}): Promise<any> {
await this.ensureInitialized();
let query = 'SELECT * FROM nodes WHERE 1=1';
const params: any[] = [];
@@ -199,7 +201,8 @@ export class N8NDocumentationMCPServer {
};
}
private getNodeInfo(nodeType: string): any {
private async getNodeInfo(nodeType: string): Promise<any> {
await this.ensureInitialized();
if (!this.repository) throw new Error('Repository not initialized');
let node = this.repository.getNode(nodeType);
@@ -228,7 +231,8 @@ export class N8NDocumentationMCPServer {
return node;
}
private searchNodes(query: string, limit: number = 20): any {
private async searchNodes(query: string, limit: number = 20): Promise<any> {
await this.ensureInitialized();
if (!this.db) throw new Error('Database not initialized');
// Simple search across multiple fields
const searchQuery = `%${query}%`;
@@ -273,7 +277,8 @@ export class N8NDocumentationMCPServer {
return 'low';
}
private listAITools(): any {
private async listAITools(): Promise<any> {
await this.ensureInitialized();
if (!this.repository) throw new Error('Repository not initialized');
const tools = this.repository.getAITools();
@@ -287,7 +292,8 @@ export class N8NDocumentationMCPServer {
};
}
private getNodeDocumentation(nodeType: string): any {
private async getNodeDocumentation(nodeType: string): Promise<any> {
await this.ensureInitialized();
if (!this.db) throw new Error('Database not initialized');
const node = this.db!.prepare(`
SELECT node_type, display_name, documentation
@@ -307,7 +313,8 @@ export class N8NDocumentationMCPServer {
};
}
private getDatabaseStatistics(): any {
private async getDatabaseStatistics(): Promise<any> {
await this.ensureInitialized();
if (!this.db) throw new Error('Database not initialized');
const stats = this.db!.prepare(`
SELECT
@@ -345,6 +352,15 @@ export class N8NDocumentationMCPServer {
};
}
// Add connect method to accept any transport
async connect(transport: any): Promise<void> {
await this.ensureInitialized();
await this.server.connect(transport);
logger.info('MCP Server connected', {
transportType: transport.constructor.name
});
}
async run(): Promise<void> {
// Ensure database is initialized before starting server
await this.ensureInitialized();