feat: add HTTP server mode for remote deployment with token auth
This commit is contained in:
117
src/http-server.ts
Normal file
117
src/http-server.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user