From 89e1df03de4f14a62ee9fe7722de3cf085e49635 Mon Sep 17 00:00:00 2001 From: czlonkowski <56956555+czlonkowski@users.noreply.github.com> Date: Fri, 13 Jun 2025 11:54:42 +0200 Subject: [PATCH] feat: add HTTP server mode for remote deployment with token auth --- .env.example | 25 ++--- CHANGELOG.md | 10 ++ CLAUDE.md | 48 ++++++++- README.md | 48 ++++++--- docs/HTTP_DEPLOYMENT.md | 214 +++++++++++++++++++++++++++++++++++++++ package-lock.json | 10 -- package.json | 3 + scripts/deploy-http.sh | 41 ++++++++ src/http-server.ts | 117 +++++++++++++++++++++ src/mcp/index.ts | 16 ++- src/mcp/server-update.ts | 28 +++-- 11 files changed, 512 insertions(+), 48 deletions(-) create mode 100644 docs/HTTP_DEPLOYMENT.md create mode 100755 scripts/deploy-http.sh create mode 100644 src/http-server.ts diff --git a/.env.example b/.env.example index 1a220b2..857a5cb 100644 --- a/.env.example +++ b/.env.example @@ -29,22 +29,17 @@ MCP_SERVER_HOST=localhost # MCP_AUTH_TOKEN=optional-for-local-development # ========================= -# HTTP MODE CONFIGURATION +# SIMPLE HTTP MODE # ========================= -# Used when running: npm run start:http or npm run dev:http +# Used for private single-user deployments -# HTTP Server Configuration -MCP_PORT=3000 -MCP_HOST=0.0.0.0 -MCP_DOMAIN=localhost +# Server mode: stdio (local) or http (remote) +MCP_MODE=stdio -# Authentication (REQUIRED for production HTTP mode) -# Generate a secure token: openssl rand -hex 32 -# MCP_AUTH_TOKEN=your-secure-auth-token-here +# HTTP Server Configuration (only used when MCP_MODE=http) +PORT=3000 +HOST=0.0.0.0 -# CORS - Enable for browser-based access -MCP_CORS=false - -# TLS Configuration (optional for HTTPS) -# MCP_TLS_CERT=/path/to/cert.pem -# MCP_TLS_KEY=/path/to/key.pem \ No newline at end of file +# Authentication token for HTTP mode (REQUIRED) +# Generate with: openssl rand -base64 32 +AUTH_TOKEN=your-secure-token-here \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4650f56..a1ea7c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. ## [2.3.0] - 2024-12-06 ### Added +- **HTTP Remote Deployment**: Single-user HTTP server for remote access + - Stateless architecture for simple deployments + - Bearer token authentication + - Compatible with mcp-remote adapter for Claude Desktop + - New HTTP mode scripts and deployment helper - **Universal Node.js Compatibility**: Automatic database adapter fallback system - Primary adapter: `better-sqlite3` for optimal performance - Fallback adapter: `sql.js` (pure JavaScript) for version mismatches @@ -13,23 +18,28 @@ All notable changes to this project will be documented in this file. - Database adapter abstraction layer (`src/database/database-adapter.ts`) - Version detection and logging for troubleshooting - sql.js dependency for pure JavaScript SQLite implementation +- HTTP server implementation (`src/http-server.ts`) +- Deployment documentation and scripts ### Changed - Updated all database operations to use the adapter interface - Removed Node.js v20.17.0 requirement - now works with ANY version - Simplified Claude Desktop setup - no wrapper scripts needed - Enhanced error messages for database initialization +- Made all MCP tool handlers async for proper initialization ### Fixed - NODE_MODULE_VERSION mismatch errors with Claude Desktop - Native module compilation issues in restricted environments - Compatibility issues when running with different Node.js versions +- Database initialization race conditions in HTTP mode ### Technical Details - Better-sqlite3: ~10-50x faster (when compatible) - sql.js: ~2-5x slower but universally compatible - Both adapters maintain identical API and functionality - Automatic persistence for sql.js with 100ms debounced saves +- HTTP server uses StreamableHTTPServerTransport for MCP compatibility ## [2.2.0] - 2024-12-06 diff --git a/CLAUDE.md b/CLAUDE.md index 044b101..0307b9f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,13 +72,19 @@ npm run rebuild # Rebuild node database npm run validate # Validate critical nodes npm run test-nodes # Test critical node properties/operations +# HTTP Server Commands: +npm run start:http # Start server in HTTP mode +npm run http # Build and start HTTP server +npm run dev:http # HTTP server with auto-reload + # Legacy Commands (deprecated): npm run db:rebuild # Old rebuild command npm run db:init # Initialize empty database npm run docs:rebuild # Rebuild documentation from TypeScript source # Production -npm start # Run built application +npm start # Run built application (stdio mode) +npm run start:http # Run in HTTP mode for remote access ``` ## High-Level Architecture @@ -234,6 +240,46 @@ This project uses the Sustainable Use License. Key points: - ❌ Cannot host as a service without permission - ❌ Cannot include in commercial products without permission +## HTTP Remote Deployment (v2.3.0) + +### ✅ HTTP Server Implementation Complete + +The project now includes a simplified HTTP server mode for remote deployments: +- **Single-user design**: Stateless architecture for private deployments +- **Simple token auth**: Bearer token authentication +- **MCP-compatible**: Works with mcp-remote adapter for Claude Desktop +- **Easy deployment**: Minimal configuration required + +### Quick Start +```bash +# Server setup +export MCP_MODE=http +export AUTH_TOKEN=$(openssl rand -base64 32) +npm run start:http + +# Client setup (Claude Desktop config) +{ + "mcpServers": { + "n8n-documentation": { + "command": "mcp-remote", + "args": [ + "https://your-server.com/mcp", + "--header", + "Authorization: Bearer your-auth-token" + ] + } + } +} +``` + +### Available Scripts +- `npm run start:http` - Start in HTTP mode +- `npm run http` - Build and start HTTP server +- `npm run dev:http` - Development mode with auto-reload +- `./scripts/deploy-http.sh` - Deployment helper script + +For detailed deployment instructions, see [HTTP Deployment Guide](./docs/HTTP_DEPLOYMENT.md). + ## Recent Problem Solutions ### SQLite Version Mismatch (Solved in v2.3) diff --git a/README.md b/README.md index 6040a7f..ec9640a 100644 --- a/README.md +++ b/README.md @@ -184,21 +184,45 @@ Current implementation achieves: - ✅ 35 AI-capable tools detected - ✅ All critical nodes validated -## Future Development +## Remote Deployment -### HTTP Remote Deployment (Planned) +### HTTP Server Mode -We are planning to add HTTP transport support to enable remote deployment of the MCP server. This will allow: -- Centralized hosting on cloud servers -- Multiple users connecting to a single instance -- No local installation required -- Enterprise-ready authentication +n8n-MCP now supports HTTP mode for remote deployments. This allows you to: +- Host the MCP server on a cloud VPS +- Connect from Claude Desktop using mcp-remote +- Single-user design for private use +- Simple token-based authentication -For detailed planning documents, see: -- [HTTP Remote Deployment Plan](./HTTP_REMOTE_DEPLOYMENT_PLAN.md) -- [HTTP Implementation Guide](./HTTP_IMPLEMENTATION_GUIDE.md) -- [HTTP Implementation Roadmap](./HTTP_IMPLEMENTATION_ROADMAP.md) -- [HTTP Remote Summary](./HTTP_REMOTE_SUMMARY.md) +### Quick Start for HTTP Mode + +1. On your server: +```bash +# Set environment variables +export MCP_MODE=http +export AUTH_TOKEN=$(openssl rand -base64 32) + +# Start the server +npm run start:http +``` + +2. On your client, configure Claude Desktop with mcp-remote: +```json +{ + "mcpServers": { + "n8n-documentation": { + "command": "mcp-remote", + "args": [ + "https://your-server.com/mcp", + "--header", + "Authorization: Bearer your-auth-token" + ] + } + } +} +``` + +For detailed instructions, see [HTTP Deployment Guide](./docs/HTTP_DEPLOYMENT.md). ## Contributing diff --git a/docs/HTTP_DEPLOYMENT.md b/docs/HTTP_DEPLOYMENT.md new file mode 100644 index 0000000..6bb7200 --- /dev/null +++ b/docs/HTTP_DEPLOYMENT.md @@ -0,0 +1,214 @@ +# HTTP Deployment Guide + +This guide explains how to deploy n8n-MCP as a private HTTP server for remote access. + +## Overview + +The HTTP mode allows you to run n8n-MCP on a remote server and connect to it from Claude Desktop using the mcp-remote adapter. This is designed for single-user private deployments. + +## Requirements + +- Node.js v16+ on the server +- A server with a public IP or domain +- HTTPS proxy (nginx/caddy) for secure connections +- mcp-remote installed on the client + +## Server Setup + +### 1. Clone and Build + +```bash +git clone https://github.com/yourusername/n8n-mcp.git +cd n8n-mcp +npm install +npm run build +npm run rebuild +``` + +### 2. Configure Environment + +```bash +cp .env.example .env +``` + +Edit `.env`: +```env +# HTTP mode configuration +MCP_MODE=http +PORT=3000 +HOST=0.0.0.0 + +# Generate secure token +AUTH_TOKEN=your-secure-token-here + +# Other settings +NODE_DB_PATH=./data/nodes.db +MCP_LOG_LEVEL=info +NODE_ENV=production +``` + +Generate a secure token: +```bash +openssl rand -base64 32 +``` + +### 3. Start the Server + +```bash +# Using the deployment script +./scripts/deploy-http.sh + +# Or manually +MCP_MODE=http npm start +``` + +The server will start on `http://0.0.0.0:3000` + +### 4. Setup HTTPS Proxy (Recommended) + +#### Using nginx: + +```nginx +server { + listen 443 ssl; + server_name your-domain.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} +``` + +#### Using Caddy: + +```caddyfile +your-domain.com { + reverse_proxy localhost:3000 +} +``` + +## Client Setup + +### 1. Install mcp-remote + +```bash +npm install -g mcp-remote +``` + +### 2. Configure Claude Desktop + +Edit Claude Desktop config: + +```json +{ + "mcpServers": { + "n8n-documentation": { + "command": "mcp-remote", + "args": [ + "https://your-domain.com/mcp", + "--header", + "Authorization: Bearer your-secure-token-here" + ] + } + } +} +``` + +### 3. Test Connection + +1. Restart Claude Desktop +2. The MCP tools should be available +3. Test with: "List all n8n nodes" + +## Security Considerations + +1. **Always use HTTPS** in production +2. **Keep AUTH_TOKEN secret** - treat it like a password +3. **Firewall rules** - Only expose necessary ports +4. **Regular updates** - Keep dependencies updated +5. **Monitor logs** - Check for unauthorized access attempts + +## Health Monitoring + +Check server health: +```bash +curl https://your-domain.com/health +``` + +Expected response: +```json +{ + "status": "ok", + "mode": "http", + "version": "2.3.0" +} +``` + +## Troubleshooting + +### Connection Refused +- Check firewall rules +- Verify server is running +- Check nginx/proxy configuration + +### Authentication Failed +- Verify AUTH_TOKEN matches in both server and client +- Check Authorization header format + +### MCP Tools Not Available +- Restart Claude Desktop +- Check mcp-remote installation +- Verify server logs for errors + +## Performance Tips + +1. Use a VPS with good network connectivity +2. Enable gzip compression in your proxy +3. Consider using PM2 for process management: + ```bash + pm2 start npm --name "n8n-mcp" -- run start:http + ``` + +## Example Systemd Service + +Create `/etc/systemd/system/n8n-mcp.service`: + +```ini +[Unit] +Description=n8n MCP HTTP Server +After=network.target + +[Service] +Type=simple +User=your-user +WorkingDirectory=/path/to/n8n-mcp +ExecStart=/usr/bin/npm run start:http +Restart=on-failure +Environment=NODE_ENV=production + +[Install] +WantedBy=multi-user.target +``` + +Enable and start: +```bash +sudo systemctl enable n8n-mcp +sudo systemctl start n8n-mcp +``` + +## Limitations + +- Single-user design (no multi-tenancy) +- Stateless (no session persistence) +- No built-in rate limiting +- Basic token authentication only + +For multi-user deployments, consider implementing a proper API gateway with user management. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0ac40aa..8be51e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", "@n8n/n8n-nodes-langchain": "^1.0.0", - "@types/better-sqlite3": "^7.6.13", "better-sqlite3": "^11.10.0", "dotenv": "^16.5.0", "express": "^5.1.0", @@ -13151,15 +13150,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", diff --git a/package.json b/package.json index 76314e8..f7e75bf 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ "validate": "node dist/scripts/validate.js", "test-nodes": "node dist/scripts/test-nodes.js", "start": "node dist/mcp/index.js", + "start:http": "MCP_MODE=http node dist/mcp/index.js", + "http": "npm run build && npm run start:http", "dev": "npm run build && npm run rebuild && npm run validate", + "dev:http": "MCP_MODE=http nodemon --watch src --ext ts --exec 'npm run build && npm run start:http'", "test": "jest", "lint": "tsc --noEmit", "typecheck": "tsc --noEmit", diff --git a/scripts/deploy-http.sh b/scripts/deploy-http.sh new file mode 100755 index 0000000..1bb7188 --- /dev/null +++ b/scripts/deploy-http.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Simple deployment script for n8n-MCP HTTP server +# For private, single-user deployments only + +set -e + +echo "n8n-MCP HTTP Deployment Script" +echo "==============================" +echo "" + +# Check if .env exists +if [ ! -f .env ]; then + echo "Creating .env file..." + cp .env.example .env + echo "" + echo "⚠️ Please edit .env file and set:" + echo " - AUTH_TOKEN (generate with: openssl rand -base64 32)" + echo " - MCP_MODE=http" + echo " - PORT (default 3000)" + echo "" + exit 1 +fi + +# Check if AUTH_TOKEN is set +if ! grep -q "AUTH_TOKEN=.*[a-zA-Z0-9]" .env; then + echo "ERROR: AUTH_TOKEN not set in .env file" + echo "Generate one with: openssl rand -base64 32" + exit 1 +fi + +# Build and start +echo "Building project..." +npm run build + +echo "" +echo "Starting HTTP server..." +echo "Use Ctrl+C to stop" +echo "" + +# Start with production settings +NODE_ENV=production npm run start:http \ No newline at end of file diff --git a/src/http-server.ts b/src/http-server.ts new file mode 100644 index 0000000..8acfd95 --- /dev/null +++ b/src/http-server.ts @@ -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 => { + // 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); + }); +} \ No newline at end of file diff --git a/src/mcp/index.ts b/src/mcp/index.ts index 0188f48..96abe0e 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -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); diff --git a/src/mcp/server-update.ts b/src/mcp/server-update.ts index b96856a..e39f87a 100644 --- a/src/mcp/server-update.ts +++ b/src/mcp/server-update.ts @@ -150,7 +150,9 @@ export class N8NDocumentationMCPServer { } } - private listNodes(filters: any = {}): any { + private async listNodes(filters: any = {}): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + await this.ensureInitialized(); + await this.server.connect(transport); + logger.info('MCP Server connected', { + transportType: transport.constructor.name + }); + } + async run(): Promise { // Ensure database is initialized before starting server await this.ensureInitialized();