Implement remote MCP server deployment capabilities
- Add HTTP/JSON-RPC server for remote MCP access - Configure domain and authentication via environment variables - Create comprehensive remote deployment documentation - Support both local (stdio) and remote (HTTP) deployment modes - Add PM2 and Nginx configuration examples - Update README with remote server instructions The server can now be deployed on a VM (e.g., Hetzner) and accessed from Claude Desktop over HTTPS using the configured domain. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
42
.env.example
42
.env.example
@@ -1,22 +1,32 @@
|
|||||||
# n8n Configuration
|
# n8n Documentation MCP Remote Server Configuration
|
||||||
N8N_BASIC_AUTH_USER=admin
|
|
||||||
N8N_BASIC_AUTH_PASSWORD=your-secure-password-here
|
|
||||||
N8N_HOST=localhost
|
|
||||||
N8N_API_KEY=your-api-key-here
|
|
||||||
|
|
||||||
# MCP Configuration
|
# Remote Server Configuration
|
||||||
MCP_LOG_LEVEL=info
|
MCP_PORT=3000
|
||||||
NODE_ENV=production
|
MCP_HOST=0.0.0.0
|
||||||
|
MCP_DOMAIN=n8ndocumentation.aiservices.pl
|
||||||
|
|
||||||
|
# Authentication (REQUIRED for production)
|
||||||
|
# Generate a secure token: openssl rand -hex 32
|
||||||
|
MCP_AUTH_TOKEN=your-secure-auth-token-here
|
||||||
|
|
||||||
|
# CORS - Enable for browser-based access
|
||||||
|
MCP_CORS=true
|
||||||
|
|
||||||
|
# TLS Configuration (optional but recommended for production)
|
||||||
|
# MCP_TLS_CERT=/path/to/cert.pem
|
||||||
|
# MCP_TLS_KEY=/path/to/key.pem
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
NODE_DB_PATH=/app/data/nodes.db
|
NODE_DB_PATH=/app/data/nodes-v2.db
|
||||||
|
|
||||||
# Optional: External n8n instance
|
# Node.js Environment
|
||||||
# N8N_API_URL=http://your-n8n-instance:5678
|
NODE_ENV=production
|
||||||
|
|
||||||
# MCP Server Configuration (if using HTTP transport)
|
# Logging
|
||||||
# MCP_SERVER_PORT=3000
|
MCP_LOG_LEVEL=info
|
||||||
# MCP_SERVER_HOST=localhost
|
|
||||||
|
|
||||||
# Authentication
|
# Legacy n8n Configuration (not used in v2)
|
||||||
# MCP_AUTH_TOKEN=your-secure-token
|
# N8N_BASIC_AUTH_USER=admin
|
||||||
|
# N8N_BASIC_AUTH_PASSWORD=your-secure-password-here
|
||||||
|
# N8N_HOST=localhost
|
||||||
|
# N8N_API_KEY=your-api-key-here
|
||||||
54
README.md
54
README.md
@@ -44,6 +44,15 @@ npm run build
|
|||||||
npm run db:rebuild:v2
|
npm run db:rebuild:v2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Deployment Options
|
||||||
|
|
||||||
|
This MCP server can be deployed in two ways:
|
||||||
|
|
||||||
|
1. **Local Installation** - Run on your machine and connect Claude Desktop locally
|
||||||
|
2. **Remote Deployment** - Deploy to a VM/server and connect Claude Desktop over HTTPS
|
||||||
|
|
||||||
|
For remote deployment instructions, see [docs/REMOTE_DEPLOYMENT.md](docs/REMOTE_DEPLOYMENT.md).
|
||||||
|
|
||||||
## Installing in Claude Desktop
|
## Installing in Claude Desktop
|
||||||
|
|
||||||
### 1. Build the project first
|
### 1. Build the project first
|
||||||
@@ -90,6 +99,30 @@ In Claude, you should see "n8n-nodes" in the MCP connections. Try asking:
|
|||||||
- "Search for webhook nodes in n8n"
|
- "Search for webhook nodes in n8n"
|
||||||
- "Show me the source code for the HTTP Request node"
|
- "Show me the source code for the HTTP Request node"
|
||||||
|
|
||||||
|
### Remote Server Configuration
|
||||||
|
|
||||||
|
If you're connecting to a remote server instead of local installation:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"n8n-nodes-remote": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"@modelcontextprotocol/client-http",
|
||||||
|
"https://n8ndocumentation.aiservices.pl/mcp"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MCP_AUTH_TOKEN": "your-auth-token-from-server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `n8ndocumentation.aiservices.pl` with your actual domain and use the auth token configured on your server.
|
||||||
|
|
||||||
## Available MCP Tools
|
## Available MCP Tools
|
||||||
|
|
||||||
### `list_nodes`
|
### `list_nodes`
|
||||||
@@ -193,9 +226,12 @@ The SQLite database is stored at: `data/nodes-v2.db`
|
|||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run in development mode
|
# Run in development mode (local stdio)
|
||||||
npm run dev:v2
|
npm run dev:v2
|
||||||
|
|
||||||
|
# Run HTTP server for remote access
|
||||||
|
npm run dev:http
|
||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
npm run test:v2
|
npm run test:v2
|
||||||
|
|
||||||
@@ -203,6 +239,22 @@ npm run test:v2
|
|||||||
npm run typecheck
|
npm run typecheck
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Running Remote Server
|
||||||
|
|
||||||
|
To run the server for remote access:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy and configure environment
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your domain and auth token
|
||||||
|
|
||||||
|
# Run in production mode
|
||||||
|
npm run start:http
|
||||||
|
|
||||||
|
# Or with PM2 for production
|
||||||
|
pm2 start dist/index-http.js --name n8n-mcp
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Claude Desktop doesn't show the MCP server
|
### Claude Desktop doesn't show the MCP server
|
||||||
|
|||||||
432
docs/REMOTE_DEPLOYMENT.md
Normal file
432
docs/REMOTE_DEPLOYMENT.md
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
# Remote Deployment Guide
|
||||||
|
|
||||||
|
This guide explains how to deploy the n8n Documentation MCP Server to a remote VM (such as Hetzner) and connect to it from Claude Desktop.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The n8n Documentation MCP Server can be deployed as a remote HTTP service, allowing Claude Desktop to access n8n node documentation over the internet. This is useful for:
|
||||||
|
|
||||||
|
- Centralized documentation serving for teams
|
||||||
|
- Accessing documentation without local n8n installation
|
||||||
|
- Cloud-based AI development workflows
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Claude Desktop → Internet → MCP Server (HTTPS) → SQLite Database
|
||||||
|
↓
|
||||||
|
n8n Documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- A VM with Ubuntu 20.04+ or similar Linux distribution
|
||||||
|
- Node.js 18+ installed
|
||||||
|
- A domain name (e.g., `n8ndocumentation.aiservices.pl`)
|
||||||
|
- SSL certificate (Let's Encrypt recommended)
|
||||||
|
- Basic knowledge of Linux server administration
|
||||||
|
|
||||||
|
## Deployment Steps
|
||||||
|
|
||||||
|
### 1. Server Setup
|
||||||
|
|
||||||
|
SSH into your VM and prepare the environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update system packages
|
||||||
|
sudo apt update && sudo apt upgrade -y
|
||||||
|
|
||||||
|
# Install Node.js 18+ (if not already installed)
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||||
|
sudo apt install -y nodejs
|
||||||
|
|
||||||
|
# Install git and build essentials
|
||||||
|
sudo apt install -y git build-essential
|
||||||
|
|
||||||
|
# Install PM2 for process management
|
||||||
|
sudo npm install -g pm2
|
||||||
|
|
||||||
|
# Create application directory
|
||||||
|
sudo mkdir -p /opt/n8n-mcp
|
||||||
|
sudo chown $USER:$USER /opt/n8n-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Clone and Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt
|
||||||
|
git clone https://github.com/yourusername/n8n-mcp.git
|
||||||
|
cd n8n-mcp
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Initialize database
|
||||||
|
npm run db:rebuild:v2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure Environment
|
||||||
|
|
||||||
|
Create the production environment file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure with your domain and security settings:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Remote Server Configuration
|
||||||
|
MCP_PORT=3000
|
||||||
|
MCP_HOST=0.0.0.0
|
||||||
|
MCP_DOMAIN=n8ndocumentation.aiservices.pl
|
||||||
|
|
||||||
|
# Authentication - REQUIRED for production
|
||||||
|
# Generate secure token: openssl rand -hex 32
|
||||||
|
MCP_AUTH_TOKEN=your-generated-secure-token-here
|
||||||
|
|
||||||
|
# Enable CORS for browser access
|
||||||
|
MCP_CORS=true
|
||||||
|
|
||||||
|
# Database path
|
||||||
|
NODE_DB_PATH=/opt/n8n-mcp/data/nodes-v2.db
|
||||||
|
|
||||||
|
# Production environment
|
||||||
|
NODE_ENV=production
|
||||||
|
MCP_LOG_LEVEL=info
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Setup SSL with Nginx
|
||||||
|
|
||||||
|
Install and configure Nginx as a reverse proxy with SSL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Nginx and Certbot
|
||||||
|
sudo apt install -y nginx certbot python3-certbot-nginx
|
||||||
|
|
||||||
|
# Create Nginx configuration
|
||||||
|
sudo nano /etc/nginx/sites-available/n8n-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the following configuration:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name n8ndocumentation.aiservices.pl;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name n8ndocumentation.aiservices.pl;
|
||||||
|
|
||||||
|
# SSL will be configured by Certbot
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header X-Frame-Options DENY;
|
||||||
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
|
|
||||||
|
# Proxy settings
|
||||||
|
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_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
|
# Increase timeouts for MCP operations
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable the site and obtain SSL certificate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable the site
|
||||||
|
sudo ln -s /etc/nginx/sites-available/n8n-mcp /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
|
||||||
|
# Obtain SSL certificate
|
||||||
|
sudo certbot --nginx -d n8ndocumentation.aiservices.pl
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Start with PM2
|
||||||
|
|
||||||
|
Create PM2 ecosystem file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano /opt/n8n-mcp/ecosystem.config.js
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
apps: [{
|
||||||
|
name: 'n8n-mcp',
|
||||||
|
script: './dist/index-http.js',
|
||||||
|
cwd: '/opt/n8n-mcp',
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: '1G',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production'
|
||||||
|
},
|
||||||
|
error_file: '/opt/n8n-mcp/logs/error.log',
|
||||||
|
out_file: '/opt/n8n-mcp/logs/out.log',
|
||||||
|
log_file: '/opt/n8n-mcp/logs/combined.log',
|
||||||
|
time: true
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create logs directory
|
||||||
|
mkdir -p /opt/n8n-mcp/logs
|
||||||
|
|
||||||
|
# Start with PM2
|
||||||
|
pm2 start ecosystem.config.js
|
||||||
|
|
||||||
|
# Save PM2 configuration
|
||||||
|
pm2 save
|
||||||
|
|
||||||
|
# Setup PM2 to start on boot
|
||||||
|
pm2 startup
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Configure Firewall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Allow SSH, HTTP, and HTTPS
|
||||||
|
sudo ufw allow 22/tcp
|
||||||
|
sudo ufw allow 80/tcp
|
||||||
|
sudo ufw allow 443/tcp
|
||||||
|
sudo ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Claude Desktop Configuration
|
||||||
|
|
||||||
|
### 1. Get your auth token
|
||||||
|
|
||||||
|
From your server, get the configured auth token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep MCP_AUTH_TOKEN /opt/n8n-mcp/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Claude Desktop
|
||||||
|
|
||||||
|
Edit your Claude Desktop configuration:
|
||||||
|
|
||||||
|
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
|
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
**Linux**: `~/.config/Claude/claude_desktop_config.json`
|
||||||
|
|
||||||
|
Add the remote MCP server:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"n8n-nodes-remote": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"@modelcontextprotocol/client-http",
|
||||||
|
"https://n8ndocumentation.aiservices.pl/mcp"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MCP_AUTH_TOKEN": "your-auth-token-here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Restart Claude Desktop
|
||||||
|
|
||||||
|
Quit and restart Claude Desktop to load the new configuration.
|
||||||
|
|
||||||
|
## Server Management
|
||||||
|
|
||||||
|
### Viewing Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View real-time logs
|
||||||
|
pm2 logs n8n-mcp
|
||||||
|
|
||||||
|
# View error logs
|
||||||
|
tail -f /opt/n8n-mcp/logs/error.log
|
||||||
|
|
||||||
|
# View Nginx logs
|
||||||
|
sudo tail -f /var/log/nginx/access.log
|
||||||
|
sudo tail -f /var/log/nginx/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuilding Database
|
||||||
|
|
||||||
|
To update the node documentation database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/n8n-mcp
|
||||||
|
|
||||||
|
# Stop the server
|
||||||
|
pm2 stop n8n-mcp
|
||||||
|
|
||||||
|
# Rebuild database
|
||||||
|
npm run db:rebuild:v2
|
||||||
|
|
||||||
|
# Restart server
|
||||||
|
pm2 restart n8n-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating the Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/n8n-mcp
|
||||||
|
|
||||||
|
# Pull latest changes
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
pm2 restart n8n-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Authentication Token**: Always use a strong, randomly generated token
|
||||||
|
```bash
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **HTTPS**: Always use HTTPS in production. The setup above includes automatic SSL with Let's Encrypt.
|
||||||
|
|
||||||
|
3. **Firewall**: Only open necessary ports (22, 80, 443)
|
||||||
|
|
||||||
|
4. **Updates**: Keep the system and Node.js updated regularly
|
||||||
|
|
||||||
|
5. **Monitoring**: Set up monitoring for the service:
|
||||||
|
```bash
|
||||||
|
# PM2 monitoring
|
||||||
|
pm2 install pm2-logrotate
|
||||||
|
pm2 set pm2-logrotate:max_size 10M
|
||||||
|
pm2 set pm2-logrotate:retain 7
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
Once deployed, your server provides:
|
||||||
|
|
||||||
|
- `GET https://n8ndocumentation.aiservices.pl/` - Server information
|
||||||
|
- `GET https://n8ndocumentation.aiservices.pl/health` - Health check
|
||||||
|
- `GET https://n8ndocumentation.aiservices.pl/stats` - Database statistics
|
||||||
|
- `POST https://n8ndocumentation.aiservices.pl/mcp` - MCP protocol endpoint
|
||||||
|
- `POST https://n8ndocumentation.aiservices.pl/rebuild` - Rebuild database (requires auth)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Connection Issues
|
||||||
|
|
||||||
|
1. Check if the server is running:
|
||||||
|
```bash
|
||||||
|
pm2 status
|
||||||
|
curl https://n8ndocumentation.aiservices.pl/health
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Verify Nginx is working:
|
||||||
|
```bash
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl status nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Check firewall:
|
||||||
|
```bash
|
||||||
|
sudo ufw status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication Failures
|
||||||
|
|
||||||
|
1. Verify the token matches in both `.env` and Claude config
|
||||||
|
2. Check server logs for auth errors:
|
||||||
|
```bash
|
||||||
|
pm2 logs n8n-mcp --lines 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Issues
|
||||||
|
|
||||||
|
1. Check database exists:
|
||||||
|
```bash
|
||||||
|
ls -la /opt/n8n-mcp/data/nodes-v2.db
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Rebuild if necessary:
|
||||||
|
```bash
|
||||||
|
cd /opt/n8n-mcp
|
||||||
|
npm run db:rebuild:v2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring and Maintenance
|
||||||
|
|
||||||
|
### Health Monitoring
|
||||||
|
|
||||||
|
Set up external monitoring (e.g., UptimeRobot) to check:
|
||||||
|
- `https://n8ndocumentation.aiservices.pl/health`
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
|
||||||
|
Regular backups of the database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create backup script
|
||||||
|
cat > /opt/n8n-mcp/backup.sh << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
BACKUP_DIR="/opt/n8n-mcp/backups"
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
cp /opt/n8n-mcp/data/nodes-v2.db "$BACKUP_DIR/nodes-v2-$(date +%Y%m%d-%H%M%S).db"
|
||||||
|
# Keep only last 7 backups
|
||||||
|
find $BACKUP_DIR -name "nodes-v2-*.db" -mtime +7 -delete
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x /opt/n8n-mcp/backup.sh
|
||||||
|
|
||||||
|
# Add to crontab (daily at 2 AM)
|
||||||
|
(crontab -l 2>/dev/null; echo "0 2 * * * /opt/n8n-mcp/backup.sh") | crontab -
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cost Optimization
|
||||||
|
|
||||||
|
For a small Hetzner VM (CX11 - 1 vCPU, 2GB RAM):
|
||||||
|
- Monthly cost: ~€4-5
|
||||||
|
- Sufficient for serving documentation to multiple Claude instances
|
||||||
|
- Can handle hundreds of concurrent connections
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues specific to remote deployment:
|
||||||
|
1. Check server logs first
|
||||||
|
2. Verify network connectivity
|
||||||
|
3. Ensure all dependencies are installed
|
||||||
|
4. Check GitHub issues for similar problems
|
||||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -21,6 +21,7 @@
|
|||||||
"@types/express": "^5.0.3",
|
"@types/express": "^5.0.3",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.15.30",
|
"@types/node": "^22.15.30",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
"ts-jest": "^29.3.4",
|
"ts-jest": "^29.3.4",
|
||||||
@@ -4319,6 +4320,16 @@
|
|||||||
"@types/webidl-conversions": "*"
|
"@types/webidl-conversions": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/yargs": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "17.0.33",
|
"version": "17.0.33",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
|
||||||
|
|||||||
@@ -7,8 +7,10 @@
|
|||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "nodemon --exec ts-node src/index.ts",
|
"dev": "nodemon --exec ts-node src/index.ts",
|
||||||
"dev:v2": "nodemon --exec ts-node src/index-v2.ts",
|
"dev:v2": "nodemon --exec ts-node src/index-v2.ts",
|
||||||
|
"dev:http": "nodemon --exec ts-node src/index-http.ts",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"start:v2": "node dist/index-v2.js",
|
"start:v2": "node dist/index-v2.js",
|
||||||
|
"start:http": "node dist/index-http.js",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"lint": "tsc --noEmit",
|
"lint": "tsc --noEmit",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
"@types/express": "^5.0.3",
|
"@types/express": "^5.0.3",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.15.30",
|
"@types/node": "^22.15.30",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
"ts-jest": "^29.3.4",
|
"ts-jest": "^29.3.4",
|
||||||
|
|||||||
93
src/index-http.ts
Normal file
93
src/index-http.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import { N8NDocumentationRemoteServer } from './mcp/remote-server';
|
||||||
|
import { logger } from './utils/logger';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
// Get configuration from environment
|
||||||
|
const config = {
|
||||||
|
port: parseInt(process.env.MCP_PORT || '3000', 10),
|
||||||
|
host: process.env.MCP_HOST || '0.0.0.0',
|
||||||
|
domain: process.env.MCP_DOMAIN || 'localhost',
|
||||||
|
authToken: process.env.MCP_AUTH_TOKEN,
|
||||||
|
cors: process.env.MCP_CORS === 'true',
|
||||||
|
tlsCert: process.env.MCP_TLS_CERT,
|
||||||
|
tlsKey: process.env.MCP_TLS_KEY,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate required configuration
|
||||||
|
if (!config.domain || config.domain === 'localhost') {
|
||||||
|
logger.warn('MCP_DOMAIN not set or set to localhost. Using default: localhost');
|
||||||
|
logger.warn('For production, set MCP_DOMAIN to your actual domain (e.g., n8ndocumentation.aiservices.pl)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.authToken) {
|
||||||
|
logger.warn('MCP_AUTH_TOKEN not set. Server will run without authentication.');
|
||||||
|
logger.warn('For production, set MCP_AUTH_TOKEN to a secure value.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set database path if not already set
|
||||||
|
if (!process.env.NODE_DB_PATH) {
|
||||||
|
process.env.NODE_DB_PATH = path.join(__dirname, '../data/nodes-v2.db');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Starting n8n Documentation MCP Remote Server');
|
||||||
|
logger.info('Configuration:', {
|
||||||
|
port: config.port,
|
||||||
|
host: config.host,
|
||||||
|
domain: config.domain,
|
||||||
|
cors: config.cors,
|
||||||
|
authEnabled: !!config.authToken,
|
||||||
|
tlsEnabled: !!(config.tlsCert && config.tlsKey),
|
||||||
|
databasePath: process.env.NODE_DB_PATH,
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = new N8NDocumentationRemoteServer(config);
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
await server.start();
|
||||||
|
|
||||||
|
// Handle graceful shutdown
|
||||||
|
const shutdown = async () => {
|
||||||
|
logger.info('Received shutdown signal');
|
||||||
|
await server.stop();
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', shutdown);
|
||||||
|
process.on('SIGTERM', shutdown);
|
||||||
|
|
||||||
|
logger.info('Server is ready to accept connections');
|
||||||
|
logger.info(`Claude Desktop configuration:`);
|
||||||
|
logger.info(JSON.stringify({
|
||||||
|
"mcpServers": {
|
||||||
|
"n8n-nodes-remote": {
|
||||||
|
"command": "curl",
|
||||||
|
"args": [
|
||||||
|
"-X", "POST",
|
||||||
|
"-H", "Content-Type: application/json",
|
||||||
|
"-H", `Authorization: Bearer ${config.authToken || 'YOUR_AUTH_TOKEN'}`,
|
||||||
|
"-d", "@-",
|
||||||
|
`https://${config.domain}/mcp`
|
||||||
|
],
|
||||||
|
"env": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the server
|
||||||
|
main().catch((error) => {
|
||||||
|
logger.error('Unhandled error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
592
src/mcp/http-server.ts
Normal file
592
src/mcp/http-server.ts
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { createServer } from 'http';
|
||||||
|
import { WebSocketServer, WebSocket } from 'ws';
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
// WebSocketServerTransport is not available in the SDK, we'll implement a custom solution
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ErrorCode,
|
||||||
|
ListResourcesRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
McpError,
|
||||||
|
ReadResourceRequestSchema,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { NodeDocumentationService } from '../services/node-documentation-service';
|
||||||
|
import { nodeDocumentationTools } from './tools-v2';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
import { authenticateRequest } from '../utils/auth-middleware';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
interface HttpServerConfig {
|
||||||
|
port: number;
|
||||||
|
host: string;
|
||||||
|
domain: string;
|
||||||
|
authToken?: string;
|
||||||
|
cors?: boolean;
|
||||||
|
tlsCert?: string;
|
||||||
|
tlsKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP/WebSocket MCP Server for remote access
|
||||||
|
*/
|
||||||
|
export class N8NDocumentationHttpServer {
|
||||||
|
private app: express.Application;
|
||||||
|
private server: any;
|
||||||
|
private wss!: WebSocketServer;
|
||||||
|
private nodeService: NodeDocumentationService;
|
||||||
|
private config: HttpServerConfig;
|
||||||
|
private activeSessions: Map<string, any> = new Map();
|
||||||
|
|
||||||
|
constructor(config: HttpServerConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.app = express();
|
||||||
|
this.nodeService = new NodeDocumentationService();
|
||||||
|
|
||||||
|
this.setupMiddleware();
|
||||||
|
this.setupRoutes();
|
||||||
|
this.setupWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupMiddleware(): void {
|
||||||
|
// JSON parsing
|
||||||
|
this.app.use(express.json());
|
||||||
|
|
||||||
|
// CORS if enabled
|
||||||
|
if (this.config.cors) {
|
||||||
|
this.app.use((req, res, next): void => {
|
||||||
|
res.header('Access-Control-Allow-Origin', '*');
|
||||||
|
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
|
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.sendStatus(200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request logging
|
||||||
|
this.app.use((req, res, next): void => {
|
||||||
|
logger.info(`${req.method} ${req.path}`, {
|
||||||
|
ip: req.ip,
|
||||||
|
userAgent: req.get('user-agent')
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupRoutes(): void {
|
||||||
|
// Health check endpoint
|
||||||
|
this.app.get('/health', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
service: 'n8n-documentation-mcp',
|
||||||
|
version: '2.0.0',
|
||||||
|
uptime: process.uptime()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// MCP info endpoint
|
||||||
|
this.app.get('/mcp', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
name: 'n8n-node-documentation',
|
||||||
|
version: '2.0.0',
|
||||||
|
description: 'MCP server providing n8n node documentation and source code',
|
||||||
|
transport: 'websocket',
|
||||||
|
endpoint: `wss://${this.config.domain}/mcp/websocket`,
|
||||||
|
authentication: 'bearer-token',
|
||||||
|
tools: nodeDocumentationTools.map(t => ({
|
||||||
|
name: t.name,
|
||||||
|
description: t.description
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Database stats endpoint (public)
|
||||||
|
this.app.get('/stats', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const stats = this.nodeService.getStatistics();
|
||||||
|
res.json(stats);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get statistics:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to retrieve statistics' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rebuild endpoint (requires auth)
|
||||||
|
this.app.post('/rebuild', authenticateRequest(this.config.authToken), async (req, res) => {
|
||||||
|
try {
|
||||||
|
logger.info('Database rebuild requested');
|
||||||
|
const stats = await this.nodeService.rebuildDatabase();
|
||||||
|
res.json({
|
||||||
|
message: 'Database rebuild complete',
|
||||||
|
stats
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Rebuild failed:', error);
|
||||||
|
res.status(500).json({ error: 'Rebuild failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupWebSocket(): void {
|
||||||
|
// Create HTTP server
|
||||||
|
this.server = createServer(this.app);
|
||||||
|
|
||||||
|
// Create WebSocket server
|
||||||
|
this.wss = new WebSocketServer({
|
||||||
|
server: this.server,
|
||||||
|
path: '/mcp/websocket'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.wss.on('connection', async (ws: WebSocket, req: any) => {
|
||||||
|
const sessionId = crypto.randomUUID();
|
||||||
|
logger.info(`WebSocket connection established: ${sessionId}`);
|
||||||
|
|
||||||
|
// Authenticate WebSocket connection
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (this.config.authToken && authHeader !== `Bearer ${this.config.authToken}`) {
|
||||||
|
logger.warn(`Unauthorized WebSocket connection attempt: ${sessionId}`);
|
||||||
|
ws.close(1008, 'Unauthorized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create MCP server instance for this connection
|
||||||
|
const mcpServer = new Server(
|
||||||
|
{
|
||||||
|
name: 'n8n-node-documentation',
|
||||||
|
version: '2.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
resources: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Setup MCP handlers
|
||||||
|
this.setupMcpHandlers(mcpServer);
|
||||||
|
|
||||||
|
// WebSocket transport not available in SDK - implement JSON-RPC over WebSocket
|
||||||
|
// For now, we'll handle messages directly
|
||||||
|
ws.on('message', async (data: Buffer) => {
|
||||||
|
try {
|
||||||
|
const request = JSON.parse(data.toString());
|
||||||
|
// Process request through MCP server handlers
|
||||||
|
// This would need custom implementation
|
||||||
|
logger.warn('WebSocket MCP not fully implemented yet');
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: request.id,
|
||||||
|
error: {
|
||||||
|
code: -32601,
|
||||||
|
message: 'WebSocket transport not implemented'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('WebSocket message error:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activeSessions.set(sessionId, { mcpServer, ws });
|
||||||
|
logger.info(`MCP session established: ${sessionId}`);
|
||||||
|
|
||||||
|
// Handle disconnect
|
||||||
|
ws.on('close', () => {
|
||||||
|
logger.info(`WebSocket connection closed: ${sessionId}`);
|
||||||
|
this.activeSessions.delete(sessionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to establish MCP session: ${sessionId}`, error);
|
||||||
|
ws.close(1011, 'Server error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupMcpHandlers(server: Server): void {
|
||||||
|
// List available tools
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||||
|
tools: nodeDocumentationTools,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// List available resources
|
||||||
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
uri: 'nodes://list',
|
||||||
|
name: 'Available n8n Nodes',
|
||||||
|
description: 'List of all available n8n nodes',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'nodes://statistics',
|
||||||
|
name: 'Database Statistics',
|
||||||
|
description: 'Statistics about the node documentation database',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Read resources
|
||||||
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
const { uri } = request.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (uri === 'nodes://list') {
|
||||||
|
const nodes = await this.nodeService.listNodes();
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(nodes.map(n => ({
|
||||||
|
nodeType: n.nodeType,
|
||||||
|
name: n.name,
|
||||||
|
displayName: n.displayName,
|
||||||
|
category: n.category,
|
||||||
|
description: n.description,
|
||||||
|
hasDocumentation: !!n.documentation,
|
||||||
|
hasExample: !!n.exampleWorkflow,
|
||||||
|
})), null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri === 'nodes://statistics') {
|
||||||
|
const stats = this.nodeService.getStatistics();
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(stats, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Resource read error:', error);
|
||||||
|
throw error instanceof McpError ? error : new McpError(
|
||||||
|
ErrorCode.InternalError,
|
||||||
|
`Failed to read resource: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle tool calls
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (name) {
|
||||||
|
case 'list_nodes':
|
||||||
|
return await this.handleListNodes(args);
|
||||||
|
|
||||||
|
case 'get_node_info':
|
||||||
|
return await this.handleGetNodeInfo(args);
|
||||||
|
|
||||||
|
case 'search_nodes':
|
||||||
|
return await this.handleSearchNodes(args);
|
||||||
|
|
||||||
|
case 'get_node_example':
|
||||||
|
return await this.handleGetNodeExample(args);
|
||||||
|
|
||||||
|
case 'get_node_source_code':
|
||||||
|
return await this.handleGetNodeSourceCode(args);
|
||||||
|
|
||||||
|
case 'get_node_documentation':
|
||||||
|
return await this.handleGetNodeDocumentation(args);
|
||||||
|
|
||||||
|
case 'rebuild_database':
|
||||||
|
return await this.handleRebuildDatabase(args);
|
||||||
|
|
||||||
|
case 'get_database_statistics':
|
||||||
|
return await this.handleGetStatistics();
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Tool execution error (${name}):`, error);
|
||||||
|
throw error instanceof McpError ? error : new McpError(
|
||||||
|
ErrorCode.InternalError,
|
||||||
|
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool handlers (copied from server-v2.ts)
|
||||||
|
private async handleListNodes(args: any): Promise<any> {
|
||||||
|
const nodes = await this.nodeService.listNodes();
|
||||||
|
|
||||||
|
let filtered = nodes;
|
||||||
|
|
||||||
|
if (args.category) {
|
||||||
|
filtered = filtered.filter(n => n.category === args.category);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.packageName) {
|
||||||
|
filtered = filtered.filter(n => n.packageName === args.packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.isTrigger !== undefined) {
|
||||||
|
filtered = filtered.filter(n => n.isTrigger === args.isTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(filtered.map(n => ({
|
||||||
|
nodeType: n.nodeType,
|
||||||
|
name: n.name,
|
||||||
|
displayName: n.displayName,
|
||||||
|
category: n.category,
|
||||||
|
description: n.description,
|
||||||
|
packageName: n.packageName,
|
||||||
|
hasDocumentation: !!n.documentation,
|
||||||
|
hasExample: !!n.exampleWorkflow,
|
||||||
|
isTrigger: n.isTrigger,
|
||||||
|
isWebhook: n.isWebhook,
|
||||||
|
})), null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleGetNodeInfo(args: any): Promise<any> {
|
||||||
|
if (!args.nodeType) {
|
||||||
|
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||||
|
|
||||||
|
if (!nodeInfo) {
|
||||||
|
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({
|
||||||
|
nodeType: nodeInfo.nodeType,
|
||||||
|
name: nodeInfo.name,
|
||||||
|
displayName: nodeInfo.displayName,
|
||||||
|
description: nodeInfo.description,
|
||||||
|
category: nodeInfo.category,
|
||||||
|
packageName: nodeInfo.packageName,
|
||||||
|
sourceCode: nodeInfo.sourceCode,
|
||||||
|
credentialCode: nodeInfo.credentialCode,
|
||||||
|
documentation: nodeInfo.documentation,
|
||||||
|
documentationUrl: nodeInfo.documentationUrl,
|
||||||
|
exampleWorkflow: nodeInfo.exampleWorkflow,
|
||||||
|
exampleParameters: nodeInfo.exampleParameters,
|
||||||
|
propertiesSchema: nodeInfo.propertiesSchema,
|
||||||
|
isTrigger: nodeInfo.isTrigger,
|
||||||
|
isWebhook: nodeInfo.isWebhook,
|
||||||
|
}, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleSearchNodes(args: any): Promise<any> {
|
||||||
|
if (!args.query) {
|
||||||
|
throw new McpError(ErrorCode.InvalidParams, 'query is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await this.nodeService.searchNodes({
|
||||||
|
query: args.query,
|
||||||
|
category: args.category,
|
||||||
|
limit: args.limit || 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
let filtered = results;
|
||||||
|
if (args.hasDocumentation) {
|
||||||
|
filtered = filtered.filter(n => !!n.documentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(filtered.map(n => ({
|
||||||
|
nodeType: n.nodeType,
|
||||||
|
name: n.name,
|
||||||
|
displayName: n.displayName,
|
||||||
|
category: n.category,
|
||||||
|
description: n.description,
|
||||||
|
hasDocumentation: !!n.documentation,
|
||||||
|
hasExample: !!n.exampleWorkflow,
|
||||||
|
})), null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleGetNodeExample(args: any): Promise<any> {
|
||||||
|
if (!args.nodeType) {
|
||||||
|
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||||
|
|
||||||
|
if (!nodeInfo) {
|
||||||
|
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeInfo.exampleWorkflow) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `No example available for node: ${args.nodeType}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(nodeInfo.exampleWorkflow, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleGetNodeSourceCode(args: any): Promise<any> {
|
||||||
|
if (!args.nodeType) {
|
||||||
|
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||||
|
|
||||||
|
if (!nodeInfo) {
|
||||||
|
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: any = {
|
||||||
|
nodeType: nodeInfo.nodeType,
|
||||||
|
sourceCode: nodeInfo.sourceCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (args.includeCredentials && nodeInfo.credentialCode) {
|
||||||
|
response.credentialCode = nodeInfo.credentialCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(response, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleGetNodeDocumentation(args: any): Promise<any> {
|
||||||
|
if (!args.nodeType) {
|
||||||
|
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||||
|
|
||||||
|
if (!nodeInfo) {
|
||||||
|
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeInfo.documentation) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `No documentation available for node: ${args.nodeType}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = args.format === 'plain'
|
||||||
|
? nodeInfo.documentation.replace(/[#*`]/g, '')
|
||||||
|
: nodeInfo.documentation;
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: content,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleRebuildDatabase(args: any): Promise<any> {
|
||||||
|
logger.info('Database rebuild requested via MCP');
|
||||||
|
|
||||||
|
const stats = await this.nodeService.rebuildDatabase();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({
|
||||||
|
message: 'Database rebuild complete',
|
||||||
|
stats,
|
||||||
|
}, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleGetStatistics(): Promise<any> {
|
||||||
|
const stats = this.nodeService.getStatistics();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(stats, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.server.listen(this.config.port, this.config.host, () => {
|
||||||
|
logger.info(`n8n Documentation MCP HTTP server started`);
|
||||||
|
logger.info(`HTTP endpoint: http://${this.config.host}:${this.config.port}`);
|
||||||
|
logger.info(`WebSocket endpoint: ws://${this.config.host}:${this.config.port}/mcp/websocket`);
|
||||||
|
logger.info(`Domain: ${this.config.domain}`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
logger.info('Stopping n8n Documentation MCP HTTP server...');
|
||||||
|
|
||||||
|
// Close all WebSocket connections
|
||||||
|
this.wss.clients.forEach((ws: WebSocket) => ws.close());
|
||||||
|
|
||||||
|
// Close HTTP server
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.server.close(() => {
|
||||||
|
this.nodeService.close();
|
||||||
|
logger.info('Server stopped');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
608
src/mcp/remote-server.ts
Normal file
608
src/mcp/remote-server.ts
Normal file
@@ -0,0 +1,608 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { createServer as createHttpServer } from 'http';
|
||||||
|
import { createServer as createHttpsServer } from 'https';
|
||||||
|
import {
|
||||||
|
ErrorCode,
|
||||||
|
McpError,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { NodeDocumentationService } from '../services/node-documentation-service';
|
||||||
|
import { nodeDocumentationTools } from './tools-v2';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
import { authenticateRequest } from '../utils/auth-middleware';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
interface RemoteServerConfig {
|
||||||
|
port: number;
|
||||||
|
host: string;
|
||||||
|
domain: string;
|
||||||
|
authToken?: string;
|
||||||
|
cors?: boolean;
|
||||||
|
tlsCert?: string;
|
||||||
|
tlsKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote MCP Server using Streamable HTTP transport
|
||||||
|
* Based on MCP's modern approach for remote servers
|
||||||
|
*/
|
||||||
|
export class N8NDocumentationRemoteServer {
|
||||||
|
private app: express.Application;
|
||||||
|
private server: any;
|
||||||
|
private nodeService: NodeDocumentationService;
|
||||||
|
private config: RemoteServerConfig;
|
||||||
|
|
||||||
|
constructor(config: RemoteServerConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.app = express();
|
||||||
|
this.nodeService = new NodeDocumentationService();
|
||||||
|
|
||||||
|
this.setupMiddleware();
|
||||||
|
this.setupRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupMiddleware(): void {
|
||||||
|
// Parse JSON bodies with larger limit for MCP messages
|
||||||
|
this.app.use(express.json({ limit: '10mb' }));
|
||||||
|
|
||||||
|
// CORS if enabled
|
||||||
|
if (this.config.cors) {
|
||||||
|
this.app.use((req, res, next): void => {
|
||||||
|
res.header('Access-Control-Allow-Origin', '*');
|
||||||
|
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
|
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Request-ID');
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.sendStatus(200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request logging
|
||||||
|
this.app.use((req, res, next): void => {
|
||||||
|
logger.info(`${req.method} ${req.path}`, {
|
||||||
|
ip: req.ip,
|
||||||
|
userAgent: req.get('user-agent'),
|
||||||
|
requestId: req.get('X-Request-ID')
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupRoutes(): void {
|
||||||
|
// Health check endpoint
|
||||||
|
this.app.get('/health', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
service: 'n8n-documentation-mcp',
|
||||||
|
version: '2.0.0',
|
||||||
|
uptime: process.uptime(),
|
||||||
|
domain: this.config.domain
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// MCP info endpoint - provides server capabilities
|
||||||
|
this.app.get('/', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
name: 'n8n-node-documentation',
|
||||||
|
version: '2.0.0',
|
||||||
|
description: 'MCP server providing n8n node documentation and source code',
|
||||||
|
transport: 'http',
|
||||||
|
endpoint: `https://${this.config.domain}/mcp`,
|
||||||
|
authentication: this.config.authToken ? 'bearer-token' : 'none',
|
||||||
|
capabilities: {
|
||||||
|
tools: nodeDocumentationTools.map(t => ({
|
||||||
|
name: t.name,
|
||||||
|
description: t.description
|
||||||
|
})),
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
uri: 'nodes://list',
|
||||||
|
name: 'Available n8n Nodes',
|
||||||
|
description: 'List of all available n8n nodes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'nodes://statistics',
|
||||||
|
name: 'Database Statistics',
|
||||||
|
description: 'Statistics about the node documentation database',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Database stats endpoint (public)
|
||||||
|
this.app.get('/stats', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const stats = this.nodeService.getStatistics();
|
||||||
|
res.json(stats);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get statistics:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to retrieve statistics' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rebuild endpoint (requires auth)
|
||||||
|
this.app.post('/rebuild', authenticateRequest(this.config.authToken), async (req, res) => {
|
||||||
|
try {
|
||||||
|
logger.info('Database rebuild requested');
|
||||||
|
const stats = await this.nodeService.rebuildDatabase();
|
||||||
|
res.json({
|
||||||
|
message: 'Database rebuild complete',
|
||||||
|
stats
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Rebuild failed:', error);
|
||||||
|
res.status(500).json({ error: 'Rebuild failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main MCP endpoint - handles all MCP protocol messages
|
||||||
|
this.app.post('/mcp', authenticateRequest(this.config.authToken), async (req, res) => {
|
||||||
|
const requestId = req.get('X-Request-ID') || 'unknown';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Process the JSON-RPC request directly
|
||||||
|
const response = await this.handleJsonRpcRequest(req.body);
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`MCP request failed (${requestId}):`, error);
|
||||||
|
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: req.body?.id || null,
|
||||||
|
error: {
|
||||||
|
code: -32603,
|
||||||
|
message: 'Internal error',
|
||||||
|
data: error instanceof Error ? error.message : String(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleJsonRpcRequest(request: any): Promise<any> {
|
||||||
|
const { jsonrpc, method, params, id } = request;
|
||||||
|
|
||||||
|
if (jsonrpc !== '2.0') {
|
||||||
|
return {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: id || null,
|
||||||
|
error: {
|
||||||
|
code: -32600,
|
||||||
|
message: 'Invalid Request',
|
||||||
|
data: 'JSON-RPC version must be "2.0"'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case 'tools/list':
|
||||||
|
result = await this.handleListTools();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'resources/list':
|
||||||
|
result = await this.handleListResources();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'resources/read':
|
||||||
|
result = await this.handleReadResource(params);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'tools/call':
|
||||||
|
result = await this.handleToolCall(params);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: id || null,
|
||||||
|
error: {
|
||||||
|
code: -32601,
|
||||||
|
message: 'Method not found',
|
||||||
|
data: `Unknown method: ${method}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: id || null,
|
||||||
|
result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error handling method ${method}:`, error);
|
||||||
|
|
||||||
|
const errorCode = error instanceof McpError ? error.code : -32603;
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Internal error';
|
||||||
|
|
||||||
|
return {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: id || null,
|
||||||
|
error: {
|
||||||
|
code: errorCode,
|
||||||
|
message: errorMessage,
|
||||||
|
data: error instanceof McpError ? error.data : undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleListTools(): Promise<any> {
|
||||||
|
return {
|
||||||
|
tools: nodeDocumentationTools,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleListResources(): Promise<any> {
|
||||||
|
return {
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
uri: 'nodes://list',
|
||||||
|
name: 'Available n8n Nodes',
|
||||||
|
description: 'List of all available n8n nodes',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'nodes://statistics',
|
||||||
|
name: 'Database Statistics',
|
||||||
|
description: 'Statistics about the node documentation database',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleReadResource(params: any): Promise<any> {
|
||||||
|
const { uri } = params;
|
||||||
|
|
||||||
|
if (uri === 'nodes://list') {
|
||||||
|
const nodes = await this.nodeService.listNodes();
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(nodes.map(n => ({
|
||||||
|
nodeType: n.nodeType,
|
||||||
|
name: n.name,
|
||||||
|
displayName: n.displayName,
|
||||||
|
category: n.category,
|
||||||
|
description: n.description,
|
||||||
|
hasDocumentation: !!n.documentation,
|
||||||
|
hasExample: !!n.exampleWorkflow,
|
||||||
|
})), null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri === 'nodes://statistics') {
|
||||||
|
const stats = this.nodeService.getStatistics();
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(stats, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleToolCall(params: any): Promise<any> {
|
||||||
|
const { name, arguments: args } = params;
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case 'list_nodes':
|
||||||
|
return await this.handleListNodes(args);
|
||||||
|
|
||||||
|
case 'get_node_info':
|
||||||
|
return await this.handleGetNodeInfo(args);
|
||||||
|
|
||||||
|
case 'search_nodes':
|
||||||
|
return await this.handleSearchNodes(args);
|
||||||
|
|
||||||
|
case 'get_node_example':
|
||||||
|
return await this.handleGetNodeExample(args);
|
||||||
|
|
||||||
|
case 'get_node_source_code':
|
||||||
|
return await this.handleGetNodeSourceCode(args);
|
||||||
|
|
||||||
|
case 'get_node_documentation':
|
||||||
|
return await this.handleGetNodeDocumentation(args);
|
||||||
|
|
||||||
|
case 'rebuild_database':
|
||||||
|
return await this.handleRebuildDatabase(args);
|
||||||
|
|
||||||
|
case 'get_database_statistics':
|
||||||
|
return await this.handleGetStatistics();
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool handlers
|
||||||
|
private async handleListNodes(args: any): Promise<any> {
|
||||||
|
const nodes = await this.nodeService.listNodes();
|
||||||
|
|
||||||
|
let filtered = nodes;
|
||||||
|
|
||||||
|
if (args.category) {
|
||||||
|
filtered = filtered.filter(n => n.category === args.category);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.packageName) {
|
||||||
|
filtered = filtered.filter(n => n.packageName === args.packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.isTrigger !== undefined) {
|
||||||
|
filtered = filtered.filter(n => n.isTrigger === args.isTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(filtered.map(n => ({
|
||||||
|
nodeType: n.nodeType,
|
||||||
|
name: n.name,
|
||||||
|
displayName: n.displayName,
|
||||||
|
category: n.category,
|
||||||
|
description: n.description,
|
||||||
|
packageName: n.packageName,
|
||||||
|
hasDocumentation: !!n.documentation,
|
||||||
|
hasExample: !!n.exampleWorkflow,
|
||||||
|
isTrigger: n.isTrigger,
|
||||||
|
isWebhook: n.isWebhook,
|
||||||
|
})), null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleGetNodeInfo(args: any): Promise<any> {
|
||||||
|
if (!args.nodeType) {
|
||||||
|
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||||
|
|
||||||
|
if (!nodeInfo) {
|
||||||
|
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({
|
||||||
|
nodeType: nodeInfo.nodeType,
|
||||||
|
name: nodeInfo.name,
|
||||||
|
displayName: nodeInfo.displayName,
|
||||||
|
description: nodeInfo.description,
|
||||||
|
category: nodeInfo.category,
|
||||||
|
packageName: nodeInfo.packageName,
|
||||||
|
sourceCode: nodeInfo.sourceCode,
|
||||||
|
credentialCode: nodeInfo.credentialCode,
|
||||||
|
documentation: nodeInfo.documentation,
|
||||||
|
documentationUrl: nodeInfo.documentationUrl,
|
||||||
|
exampleWorkflow: nodeInfo.exampleWorkflow,
|
||||||
|
exampleParameters: nodeInfo.exampleParameters,
|
||||||
|
propertiesSchema: nodeInfo.propertiesSchema,
|
||||||
|
isTrigger: nodeInfo.isTrigger,
|
||||||
|
isWebhook: nodeInfo.isWebhook,
|
||||||
|
}, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleSearchNodes(args: any): Promise<any> {
|
||||||
|
if (!args.query) {
|
||||||
|
throw new McpError(ErrorCode.InvalidParams, 'query is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await this.nodeService.searchNodes({
|
||||||
|
query: args.query,
|
||||||
|
category: args.category,
|
||||||
|
limit: args.limit || 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
let filtered = results;
|
||||||
|
if (args.hasDocumentation) {
|
||||||
|
filtered = filtered.filter(n => !!n.documentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(filtered.map(n => ({
|
||||||
|
nodeType: n.nodeType,
|
||||||
|
name: n.name,
|
||||||
|
displayName: n.displayName,
|
||||||
|
category: n.category,
|
||||||
|
description: n.description,
|
||||||
|
hasDocumentation: !!n.documentation,
|
||||||
|
hasExample: !!n.exampleWorkflow,
|
||||||
|
})), null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleGetNodeExample(args: any): Promise<any> {
|
||||||
|
if (!args.nodeType) {
|
||||||
|
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||||
|
|
||||||
|
if (!nodeInfo) {
|
||||||
|
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeInfo.exampleWorkflow) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `No example available for node: ${args.nodeType}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(nodeInfo.exampleWorkflow, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleGetNodeSourceCode(args: any): Promise<any> {
|
||||||
|
if (!args.nodeType) {
|
||||||
|
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||||
|
|
||||||
|
if (!nodeInfo) {
|
||||||
|
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: any = {
|
||||||
|
nodeType: nodeInfo.nodeType,
|
||||||
|
sourceCode: nodeInfo.sourceCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (args.includeCredentials && nodeInfo.credentialCode) {
|
||||||
|
response.credentialCode = nodeInfo.credentialCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(response, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleGetNodeDocumentation(args: any): Promise<any> {
|
||||||
|
if (!args.nodeType) {
|
||||||
|
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
|
||||||
|
|
||||||
|
if (!nodeInfo) {
|
||||||
|
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeInfo.documentation) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `No documentation available for node: ${args.nodeType}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = args.format === 'plain'
|
||||||
|
? nodeInfo.documentation.replace(/[#*`]/g, '')
|
||||||
|
: nodeInfo.documentation;
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: content,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleRebuildDatabase(args: any): Promise<any> {
|
||||||
|
logger.info('Database rebuild requested via MCP');
|
||||||
|
|
||||||
|
const stats = await this.nodeService.rebuildDatabase();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({
|
||||||
|
message: 'Database rebuild complete',
|
||||||
|
stats,
|
||||||
|
}, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleGetStatistics(): Promise<any> {
|
||||||
|
const stats = this.nodeService.getStatistics();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(stats, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
// Create server (HTTP or HTTPS)
|
||||||
|
if (this.config.tlsCert && this.config.tlsKey) {
|
||||||
|
const tlsOptions = {
|
||||||
|
cert: fs.readFileSync(this.config.tlsCert),
|
||||||
|
key: fs.readFileSync(this.config.tlsKey),
|
||||||
|
};
|
||||||
|
this.server = createHttpsServer(tlsOptions, this.app);
|
||||||
|
} else {
|
||||||
|
this.server = createHttpServer(this.app);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.server.listen(this.config.port, this.config.host, () => {
|
||||||
|
const protocol = this.config.tlsCert ? 'https' : 'http';
|
||||||
|
logger.info(`n8n Documentation MCP Remote server started`);
|
||||||
|
logger.info(`Endpoint: ${protocol}://${this.config.host}:${this.config.port}`);
|
||||||
|
logger.info(`Domain: ${this.config.domain}`);
|
||||||
|
logger.info(`MCP endpoint: ${protocol}://${this.config.domain}/mcp`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
logger.info('Stopping n8n Documentation MCP Remote server...');
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.server.close(() => {
|
||||||
|
this.nodeService.close();
|
||||||
|
logger.info('Server stopped');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/utils/auth-middleware.ts
Normal file
49
src/utils/auth-middleware.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { logger } from './logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Express middleware for authenticating requests with Bearer tokens
|
||||||
|
*/
|
||||||
|
export function authenticateRequest(authToken?: string) {
|
||||||
|
return (req: Request, res: Response, next: NextFunction): void => {
|
||||||
|
if (!authToken) {
|
||||||
|
// No auth required
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const authHeader = req.headers['authorization'];
|
||||||
|
|
||||||
|
if (!authHeader) {
|
||||||
|
logger.warn('Missing authorization header', {
|
||||||
|
ip: req.ip,
|
||||||
|
path: req.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(401).json({
|
||||||
|
error: 'Unauthorized',
|
||||||
|
message: 'Missing authorization header',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support both "Bearer TOKEN" and just "TOKEN" formats
|
||||||
|
const providedToken = authHeader.startsWith('Bearer ')
|
||||||
|
? authHeader.substring(7)
|
||||||
|
: authHeader;
|
||||||
|
|
||||||
|
if (providedToken !== authToken) {
|
||||||
|
logger.warn('Invalid authentication token', {
|
||||||
|
ip: req.ip,
|
||||||
|
path: req.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(401).json({
|
||||||
|
error: 'Unauthorized',
|
||||||
|
message: 'Invalid authentication token',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user