Merge branch 'main' into kimbo128/main - resolve conflicts

This commit is contained in:
czlonkowski
2025-07-17 00:55:50 +02:00
32 changed files with 1710 additions and 28 deletions

View File

@@ -7,6 +7,7 @@
# Database Configuration # Database Configuration
# For local development: ./data/nodes.db # For local development: ./data/nodes.db
# For Docker: /app/data/nodes.db # For Docker: /app/data/nodes.db
# Custom paths supported in v2.7.16+ (must end with .db)
NODE_DB_PATH=./data/nodes.db NODE_DB_PATH=./data/nodes.db
# Logging Level (debug, info, warn, error) # Logging Level (debug, info, warn, error)
@@ -44,6 +45,15 @@ USE_FIXED_HTTP=true
PORT=3000 PORT=3000
HOST=0.0.0.0 HOST=0.0.0.0
# Base URL Configuration (optional)
# Set this when running behind a proxy or when the server is accessed via a different URL
# than what it binds to. If not set, URLs will be auto-detected from proxy headers (if TRUST_PROXY is set)
# or constructed from HOST and PORT.
# Examples:
# BASE_URL=https://n8n-mcp.example.com
# BASE_URL=https://your-domain.com:8443
# PUBLIC_URL=https://n8n-mcp.mydomain.com (alternative to BASE_URL)
# Authentication token for HTTP mode (REQUIRED) # Authentication token for HTTP mode (REQUIRED)
# Generate with: openssl rand -base64 32 # Generate with: openssl rand -base64 32
AUTH_TOKEN=your-secure-token-here AUTH_TOKEN=your-secure-token-here

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# GitHub Funding Configuration
github: [czlonkowski]

View File

@@ -7,9 +7,25 @@ on:
- main - main
tags: tags:
- 'v*' - 'v*'
paths-ignore:
- '**.md'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
- '.github/pull_request_template.md'
- 'LICENSE'
- 'ATTRIBUTION.md'
- 'docs/**'
pull_request: pull_request:
branches: branches:
- main - main
paths-ignore:
- '**.md'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
- '.github/pull_request_template.md'
- 'LICENSE'
- 'ATTRIBUTION.md'
- 'docs/**'
workflow_dispatch: workflow_dispatch:
env: env:
@@ -56,7 +72,7 @@ jobs:
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
type=sha,prefix={{branch}}-,format=short type=sha,format=short
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image - name: Build and push Docker image

Binary file not shown.

View File

@@ -1,39 +1,75 @@
#!/bin/sh #!/bin/sh
set -e set -e
# Helper function for safe logging (prevents stdio mode corruption)
log_message() {
[ "$MCP_MODE" != "stdio" ] && echo "$@"
}
# Environment variable validation # Environment variable validation
if [ "$MCP_MODE" = "http" ] && [ -z "$AUTH_TOKEN" ] && [ -z "$AUTH_TOKEN_FILE" ]; then if [ "$MCP_MODE" = "http" ] && [ -z "$AUTH_TOKEN" ] && [ -z "$AUTH_TOKEN_FILE" ]; then
echo "ERROR: AUTH_TOKEN or AUTH_TOKEN_FILE is required for HTTP mode" log_message "ERROR: AUTH_TOKEN or AUTH_TOKEN_FILE is required for HTTP mode" >&2
exit 1 exit 1
fi fi
# Validate AUTH_TOKEN_FILE if provided # Validate AUTH_TOKEN_FILE if provided
if [ -n "$AUTH_TOKEN_FILE" ] && [ ! -f "$AUTH_TOKEN_FILE" ]; then if [ -n "$AUTH_TOKEN_FILE" ] && [ ! -f "$AUTH_TOKEN_FILE" ]; then
echo "ERROR: AUTH_TOKEN_FILE specified but file not found: $AUTH_TOKEN_FILE" log_message "ERROR: AUTH_TOKEN_FILE specified but file not found: $AUTH_TOKEN_FILE" >&2
exit 1 exit 1
fi fi
# Database path configuration - respect NODE_DB_PATH if set
if [ -n "$NODE_DB_PATH" ]; then
# Basic validation - must end with .db
case "$NODE_DB_PATH" in
*.db) ;;
*) log_message "ERROR: NODE_DB_PATH must end with .db" >&2; exit 1 ;;
esac
# Use the path as-is (Docker paths should be absolute anyway)
DB_PATH="$NODE_DB_PATH"
else
DB_PATH="/app/data/nodes.db"
fi
DB_DIR=$(dirname "$DB_PATH")
# Ensure database directory exists with correct ownership
if [ ! -d "$DB_DIR" ]; then
log_message "Creating database directory: $DB_DIR"
if [ "$(id -u)" = "0" ]; then
# Create as root but immediately fix ownership
mkdir -p "$DB_DIR" && chown nodejs:nodejs "$DB_DIR"
else
mkdir -p "$DB_DIR"
fi
fi
# Database initialization with file locking to prevent race conditions # Database initialization with file locking to prevent race conditions
if [ ! -f "/app/data/nodes.db" ]; then if [ ! -f "$DB_PATH" ]; then
echo "Database not found. Initializing..." log_message "Database not found at $DB_PATH. Initializing..."
# Use a lock file to prevent multiple containers from initializing simultaneously # Use a lock file to prevent multiple containers from initializing simultaneously
( (
flock -x 200 flock -x 200
# Double-check inside the lock # Double-check inside the lock
if [ ! -f "/app/data/nodes.db" ]; then if [ ! -f "$DB_PATH" ]; then
echo "Initializing database..." log_message "Initializing database at $DB_PATH..."
cd /app && node dist/scripts/rebuild.js || { cd /app && NODE_DB_PATH="$DB_PATH" node dist/scripts/rebuild.js || {
echo "ERROR: Database initialization failed" log_message "ERROR: Database initialization failed" >&2
exit 1 exit 1
} }
fi fi
) 200>/app/data/.db.lock ) 200>"$DB_DIR/.db.lock"
fi fi
# Fix permissions if running as root (for development) # Fix permissions if running as root (for development)
if [ "$(id -u)" = "0" ]; then if [ "$(id -u)" = "0" ]; then
echo "Running as root, fixing permissions..." log_message "Running as root, fixing permissions..."
chown -R nodejs:nodejs "$DB_DIR"
# Also ensure /app/data exists for backward compatibility
if [ -d "/app/data" ]; then
chown -R nodejs:nodejs /app/data chown -R nodejs:nodejs /app/data
fi
# Switch to nodejs user (using Alpine's native su) # Switch to nodejs user (using Alpine's native su)
exec su nodejs -c "$*" exec su nodejs -c "$*"
fi fi

View File

@@ -5,6 +5,54 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.7.15] - 2025-07-15
### Fixed
- **HTTP Server URL Handling**: Fixed hardcoded localhost URLs in HTTP server output (Issue #41, #42)
- Added intelligent URL detection that considers BASE_URL, PUBLIC_URL, and proxy headers
- Server now displays correct public URLs when deployed behind reverse proxies
- Added support for X-Forwarded-Proto and X-Forwarded-Host headers when TRUST_PROXY is enabled
- Fixed port display logic to hide standard ports (80/443) in URLs
- Added new GET endpoints (/, /mcp) for better API discovery
### Security
- **Host Header Injection Prevention**: Added hostname validation to prevent malicious proxy headers
- Only accepts valid hostnames (alphanumeric, dots, hyphens, optional port)
- Rejects hostnames with paths, usernames, or special characters
- Falls back to safe defaults when invalid headers are detected
- **URL Scheme Validation**: Restricted URL schemes to http/https only
- Blocks dangerous schemes like javascript:, file://, data:
- Validates all configured URLs (BASE_URL, PUBLIC_URL)
- **Information Disclosure**: Removed sensitive environment data from API responses
- Root endpoint no longer exposes internal configuration
- Only shows essential API information
### Added
- **URL Detection Utility**: New `url-detector.ts` module for intelligent URL detection
- Prioritizes explicit configuration (BASE_URL/PUBLIC_URL)
- Falls back to proxy headers when TRUST_PROXY is enabled
- Uses host/port configuration as final fallback
- Includes comprehensive security validations
- **Test Scripts**: Added test scripts for URL configuration and security validation
- `test-url-configuration.ts`: Tests various URL detection scenarios
- `test-security.ts`: Validates security fixes for malicious headers
### Changed
- **Consistent Versioning**: Fixed version inconsistency between server implementations
- Both http-server.ts and http-server-single-session.ts now use PROJECT_VERSION
- Removed hardcoded version strings
- **HTTP Bridge**: Updated to use HOST/PORT environment variables for default URL construction
- **Documentation**: Updated HTTP deployment guide with URL configuration section
## [2.7.14] - 2025-07-15
### Fixed
- **Partial Update Tool**: Fixed validation/execution discrepancy that caused "settings must NOT have additional properties" error (Issue #45)
- Removed logic in `cleanWorkflowForUpdate` that was incorrectly adding default settings to workflows
- The function now only removes read-only fields without adding any new properties
- This fixes the issue where partial updates would pass validation but fail during execution
- Added comprehensive test coverage in `test-issue-45-fix.ts`
## [2.7.13] - 2025-07-11 ## [2.7.13] - 2025-07-11
### Fixed ### Fixed

94
docs/CLAUDE_CODE_SETUP.md Normal file
View File

@@ -0,0 +1,94 @@
# Claude Code Setup
Connect n8n-MCP to Claude Code CLI for enhanced n8n workflow development from the command line.
## Quick Setup via CLI
### Basic configuration (documentation tools only):
```bash
claude mcp add n8n-mcp \
-e MCP_MODE=stdio \
-e LOG_LEVEL=error \
-e DISABLE_CONSOLE_OUTPUT=true \
-- npx n8n-mcp
```
![Adding n8n-MCP server in Claude Code](./img/cc_command.png)
### Full configuration (with n8n management tools):
```bash
claude mcp add n8n-mcp \
-e MCP_MODE=stdio \
-e LOG_LEVEL=error \
-e DISABLE_CONSOLE_OUTPUT=true \
-e N8N_API_URL=https://your-n8n-instance.com \
-e N8N_API_KEY=your-api-key \
-- npx n8n-mcp
```
Make sure to replace `https://your-n8n-instance.com` with your actual n8n URL and `your-api-key` with your n8n API key.
## Alternative Setup Methods
### Option 1: Import from Claude Desktop
If you already have n8n-MCP configured in Claude Desktop:
```bash
claude mcp add-from-claude-desktop
```
### Option 2: Project Configuration
For team sharing, add to `.mcp.json` in your project root:
```json
{
"servers": {
"n8n-mcp": {
"command": "npx",
"args": ["n8n-mcp"],
"env": {
"MCP_MODE": "stdio",
"LOG_LEVEL": "error",
"DISABLE_CONSOLE_OUTPUT": "true",
"N8N_API_URL": "https://your-n8n-instance.com",
"N8N_API_KEY": "your-api-key"
}
}
}
}
```
Then use with scope flag:
```bash
claude mcp add n8n-mcp --scope project
```
## Managing Your MCP Server
Check server status:
```bash
claude mcp list
claude mcp get n8n-mcp
```
During a conversation, use the `/mcp` command to see server status and available tools.
![n8n-MCP connected and showing 39 tools available](./img/cc_connected.png)
Remove the server:
```bash
claude mcp remove n8n-mcp
```
## Project Instructions
For optimal results, create a `CLAUDE.md` file in your project root with the instructions from the [main README's Claude Project Setup section](../README.md#-claude-project-setup).
## Tips
- If you're running n8n locally, use `http://localhost:5678` as the N8N_API_URL
- The n8n API credentials are optional - without them, you'll have documentation and validation tools only
- With API credentials, you'll get full workflow management capabilities
- Use `--scope local` (default) to keep your API credentials private
- Use `--scope project` to share configuration with your team (put credentials in environment variables)
- Claude Code will automatically start the MCP server when you begin a conversation

73
docs/CURSOR_SETUP.md Normal file
View File

@@ -0,0 +1,73 @@
# Cursor Setup
Connect n8n-MCP to Cursor IDE for enhanced n8n workflow development with AI assistance.
[![n8n-mcp Cursor Setup Tutorial](./img/cursor_tut.png)](https://www.youtube.com/watch?v=hRmVxzLGJWI)
## Video Tutorial
Watch the complete setup process: [n8n-MCP Cursor Setup Tutorial](https://www.youtube.com/watch?v=hRmVxzLGJWI)
## Setup Process
### 1. Create MCP Configuration
1. Create a `.cursor` folder in your project root
2. Create `mcp.json` file inside the `.cursor` folder
3. Copy the configuration from this repository
**Basic configuration (documentation tools only):**
```json
{
"mcpServers": {
"n8n-mcp": {
"command": "npx",
"args": ["n8n-mcp"],
"env": {
"MCP_MODE": "stdio",
"LOG_LEVEL": "error",
"DISABLE_CONSOLE_OUTPUT": "true"
}
}
}
}
```
**Full configuration (with n8n management tools):**
```json
{
"mcpServers": {
"n8n-mcp": {
"command": "npx",
"args": ["n8n-mcp"],
"env": {
"MCP_MODE": "stdio",
"LOG_LEVEL": "error",
"DISABLE_CONSOLE_OUTPUT": "true",
"N8N_API_URL": "https://your-n8n-instance.com",
"N8N_API_KEY": "your-api-key"
}
}
}
}
```
### 2. Configure n8n Connection
1. Replace `https://your-n8n-instance.com` with your actual n8n URL
2. Replace `your-api-key` with your n8n API key
### 3. Enable MCP Server
1. Click "Enable MCP Server" button in Cursor
2. Go to Cursor Settings
3. Search for "mcp"
4. Confirm MCP is working
### 4. Set Up Project Instructions
1. In your Cursor chat, invoke "create rule" and hit Tab
2. Name the rule (e.g., "n8n-mcp")
3. Set rule type to "always"
4. Copy the Claude Project instructions from the [main README's Claude Project Setup section](../README.md#-claude-project-setup)

View File

@@ -64,6 +64,7 @@ docker run -d \
| `PORT` | HTTP server port | `3000` | No | | `PORT` | HTTP server port | `3000` | No |
| `NODE_ENV` | Environment: `development` or `production` | `production` | No | | `NODE_ENV` | Environment: `development` or `production` | `production` | No |
| `LOG_LEVEL` | Logging level: `debug`, `info`, `warn`, `error` | `info` | No | | `LOG_LEVEL` | Logging level: `debug`, `info`, `warn`, `error` | `info` | No |
| `NODE_DB_PATH` | Custom database path (v2.7.16+) | `/app/data/nodes.db` | No |
*Either `AUTH_TOKEN` or `AUTH_TOKEN_FILE` must be set for HTTP mode. If both are set, `AUTH_TOKEN` takes precedence. *Either `AUTH_TOKEN` or `AUTH_TOKEN_FILE` must be set for HTTP mode. If both are set, `AUTH_TOKEN` takes precedence.
@@ -342,6 +343,28 @@ docker run --rm \
alpine tar xzf /backup/n8n-mcp-backup.tar.gz -C /target alpine tar xzf /backup/n8n-mcp-backup.tar.gz -C /target
``` ```
### Custom Database Path (v2.7.16+)
You can specify a custom database location using `NODE_DB_PATH`:
```bash
# Use custom path within mounted volume
docker run -d \
--name n8n-mcp \
-e MCP_MODE=http \
-e AUTH_TOKEN=your-token \
-e NODE_DB_PATH=/app/data/custom/my-nodes.db \
-v n8n-mcp-data:/app/data \
-p 3000:3000 \
ghcr.io/czlonkowski/n8n-mcp:latest
```
**Important Notes:**
- The path must end with `.db`
- For data persistence, ensure the path is within a mounted volume
- Paths outside mounted volumes will be lost on container restart
- The directory will be created automatically if it doesn't exist
## 🐛 Troubleshooting ## 🐛 Troubleshooting
### Common Issues ### Common Issues
@@ -506,4 +529,4 @@ services:
--- ---
*Last updated: June 2025 - Docker implementation v1.0* *Last updated: July 2025 - Docker implementation v1.1*

View File

@@ -0,0 +1,349 @@
# Docker Troubleshooting Guide
This guide helps resolve common issues when running n8n-mcp with Docker, especially when connecting to n8n instances.
## Table of Contents
- [Common Issues](#common-issues)
- [502 Bad Gateway Errors](#502-bad-gateway-errors)
- [Custom Database Path Not Working](#custom-database-path-not-working-v27160)
- [Container Name Conflicts](#container-name-conflicts)
- [n8n API Connection Issues](#n8n-api-connection-issues)
- [Docker Networking](#docker-networking)
- [Quick Solutions](#quick-solutions)
- [Debugging Steps](#debugging-steps)
## Common Issues
### Custom Database Path Not Working (v2.7.16+)
**Symptoms:**
- `NODE_DB_PATH` environment variable is set but ignored
- Database always created at `/app/data/nodes.db`
- Custom path setting has no effect
**Root Cause:** Fixed in v2.7.16. Earlier versions had hardcoded paths in docker-entrypoint.sh.
**Solutions:**
1. **Update to v2.7.16 or later:**
```bash
docker pull ghcr.io/czlonkowski/n8n-mcp:latest
```
2. **Ensure path ends with .db:**
```bash
# Correct
NODE_DB_PATH=/app/data/custom/my-nodes.db
# Incorrect (will be rejected)
NODE_DB_PATH=/app/data/custom/my-nodes
```
3. **Use path within mounted volume for persistence:**
```yaml
services:
n8n-mcp:
environment:
NODE_DB_PATH: /app/data/custom/nodes.db
volumes:
- n8n-mcp-data:/app/data # Ensure parent directory is mounted
```
### 502 Bad Gateway Errors
**Symptoms:**
- `n8n_health_check` returns 502 error
- All n8n management API calls fail
- n8n web UI is accessible but API is not
**Root Cause:** Network connectivity issues between n8n-mcp container and n8n instance.
**Solutions:**
#### 1. When n8n runs in Docker on same machine
Use Docker's special hostnames instead of `localhost`:
```json
{
"mcpServers": {
"n8n-mcp": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "N8N_API_URL=http://host.docker.internal:5678",
"-e", "N8N_API_KEY=your-api-key",
"ghcr.io/czlonkowski/n8n-mcp:latest"
]
}
}
}
```
**Alternative hostnames to try:**
- `host.docker.internal` (Docker Desktop on macOS/Windows)
- `172.17.0.1` (Default Docker bridge IP on Linux)
- Your machine's actual IP address (e.g., `192.168.1.100`)
#### 2. When both containers are in same Docker network
```bash
# Create a shared network
docker network create n8n-network
# Run n8n in the network
docker run -d --name n8n --network n8n-network -p 5678:5678 n8nio/n8n
# Configure n8n-mcp to use container name
```
```json
{
"N8N_API_URL": "http://n8n:5678"
}
```
#### 3. For Docker Compose setups
```yaml
# docker-compose.yml
services:
n8n:
image: n8nio/n8n
container_name: n8n
networks:
- n8n-net
ports:
- "5678:5678"
n8n-mcp:
image: ghcr.io/czlonkowski/n8n-mcp:latest
environment:
N8N_API_URL: http://n8n:5678
N8N_API_KEY: ${N8N_API_KEY}
networks:
- n8n-net
networks:
n8n-net:
driver: bridge
```
### Container Name Conflicts
**Symptoms:**
- Error: "Container with name '/n8n-mcp-container' already exists"
- Claude Desktop shows duplicate containers
**Root Cause:** Claude Desktop bug that spawns containers twice.
**Solutions:**
1. **Remove container name (Recommended):**
```json
{
"command": "docker",
"args": [
"run", "-i", "--rm",
// Remove: "--name", "n8n-mcp-container",
"ghcr.io/czlonkowski/n8n-mcp:latest"
]
}
```
2. **Manual cleanup when it happens:**
```bash
docker rm -f n8n-mcp-container
```
3. **Use HTTP mode instead** (avoids the issue entirely)
### n8n API Connection Issues
**Symptoms:**
- API calls fail but n8n web UI works
- Authentication errors
- API endpoints return 404
**Solutions:**
1. **Verify n8n API is enabled:**
- Check n8n settings → REST API is enabled
- Ensure API key is valid and not expired
2. **Test API directly:**
```bash
# From host machine
curl -H "X-N8N-API-KEY: your-key" http://localhost:5678/api/v1/workflows
# From inside Docker container
docker run --rm curlimages/curl \
-H "X-N8N-API-KEY: your-key" \
http://host.docker.internal:5678/api/v1/workflows
```
3. **Check n8n environment variables:**
```yaml
environment:
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=user
- N8N_BASIC_AUTH_PASSWORD=password
```
## Docker Networking
### Understanding Docker Network Modes
| Scenario | Use This URL | Why |
|----------|--------------|-----|
| n8n on host, n8n-mcp in Docker | `http://host.docker.internal:5678` | Docker can't reach host's localhost |
| Both in same Docker network | `http://container-name:5678` | Direct container-to-container |
| n8n behind reverse proxy | `http://your-domain.com` | Use public URL |
| Local development | `http://YOUR_LOCAL_IP:5678` | Use machine's IP address |
### Finding Your Configuration
```bash
# Check if n8n is running in Docker
docker ps | grep n8n
# Find Docker network
docker network ls
# Get container details
docker inspect n8n | grep NetworkMode
# Find your local IP
# macOS/Linux
ifconfig | grep "inet " | grep -v 127.0.0.1
# Windows
ipconfig | findstr IPv4
```
## Quick Solutions
### Solution 1: Use Host Network (Linux only)
```json
{
"command": "docker",
"args": [
"run", "-i", "--rm",
"--network", "host",
"-e", "N8N_API_URL=http://localhost:5678",
"ghcr.io/czlonkowski/n8n-mcp:latest"
]
}
```
### Solution 2: Use Your Machine's IP
```json
{
"N8N_API_URL": "http://192.168.1.100:5678" // Replace with your IP
}
```
### Solution 3: HTTP Mode Deployment
Deploy n8n-mcp as HTTP server to avoid stdio/Docker issues:
```bash
# Start HTTP server
docker run -d \
-p 3000:3000 \
-e MCP_MODE=http \
-e AUTH_TOKEN=your-token \
-e N8N_API_URL=http://host.docker.internal:5678 \
-e N8N_API_KEY=your-n8n-key \
ghcr.io/czlonkowski/n8n-mcp:latest
# Configure Claude with mcp-remote
```
## Debugging Steps
### 1. Enable Debug Logging
```json
{
"env": {
"LOG_LEVEL": "debug",
"DEBUG_MCP": "true"
}
}
```
### 2. Test Connectivity
```bash
# Test from n8n-mcp container
docker run --rm ghcr.io/czlonkowski/n8n-mcp:latest \
sh -c "apk add curl && curl -v http://host.docker.internal:5678/api/v1/workflows"
```
### 3. Check Docker Logs
```bash
# View n8n-mcp logs
docker logs $(docker ps -q -f ancestor=ghcr.io/czlonkowski/n8n-mcp:latest)
# View n8n logs
docker logs n8n
```
### 4. Validate Environment
```bash
# Check what n8n-mcp sees
docker run --rm ghcr.io/czlonkowski/n8n-mcp:latest \
sh -c "env | grep N8N"
```
### 5. Network Diagnostics
```bash
# Check Docker networks
docker network inspect bridge
# Test DNS resolution
docker run --rm busybox nslookup host.docker.internal
```
## Platform-Specific Notes
### Docker Desktop (macOS/Windows)
- `host.docker.internal` works out of the box
- Ensure Docker Desktop is running
- Check Docker Desktop settings → Resources → Network
### Linux
- `host.docker.internal` requires Docker 20.10+
- Alternative: Use `--add-host=host.docker.internal:host-gateway`
- Or use the Docker bridge IP: `172.17.0.1`
### Windows with WSL2
- Use `host.docker.internal` or WSL2 IP
- Check firewall rules for port 5678
- Ensure n8n binds to `0.0.0.0` not `127.0.0.1`
## Still Having Issues?
1. **Check n8n logs** for API-related errors
2. **Verify firewall/security** isn't blocking connections
3. **Try simpler setup** - Run n8n-mcp on host instead of Docker
4. **Report issue** with debug logs at [GitHub Issues](https://github.com/czlonkowski/n8n-mcp/issues)
## Useful Commands
```bash
# Remove all n8n-mcp containers
docker rm -f $(docker ps -aq -f ancestor=ghcr.io/czlonkowski/n8n-mcp:latest)
# Test n8n API with curl
curl -H "X-N8N-API-KEY: your-key" http://localhost:5678/api/v1/workflows
# Run interactive debug session
docker run -it --rm \
-e LOG_LEVEL=debug \
-e N8N_API_URL=http://host.docker.internal:5678 \
-e N8N_API_KEY=your-key \
ghcr.io/czlonkowski/n8n-mcp:latest \
sh
# Check container networking
docker run --rm alpine ping -c 4 host.docker.internal
```

View File

@@ -151,6 +151,8 @@ Skip HTTP entirely and use stdio mode directly:
| `LOG_LEVEL` | Log verbosity | `info` | | `LOG_LEVEL` | Log verbosity | `info` |
| `NODE_ENV` | Environment | `production` | | `NODE_ENV` | Environment | `production` |
| `TRUST_PROXY` | Trust proxy headers for correct IP logging | `0` | | `TRUST_PROXY` | Trust proxy headers for correct IP logging | `0` |
| `BASE_URL` | Public URL for the server (v2.7.14+) | Auto-detected |
| `PUBLIC_URL` | Alternative to BASE_URL | Auto-detected |
### n8n Management Tools (Optional) ### n8n Management Tools (Optional)
@@ -200,6 +202,39 @@ When configured, you get **16 additional tools** (total: 38 tools):
## 🌐 Reverse Proxy Configuration ## 🌐 Reverse Proxy Configuration
### URL Configuration (v2.7.14+)
n8n-MCP now intelligently detects the correct URL for your deployment:
1. **Explicit Configuration** (highest priority):
```bash
BASE_URL=https://n8n-mcp.example.com # Explicitly set public URL
# or
PUBLIC_URL=https://your-domain.com:8443
```
2. **Auto-Detection from Proxy Headers** (when TRUST_PROXY is enabled):
- Detects from `X-Forwarded-Proto` and `X-Forwarded-Host` headers
- Perfect for Cloudflare, Nginx, and other proxies
3. **Fallback** (when not configured):
- Uses `HOST` and `PORT` configuration
- Shows `localhost` when bound to `0.0.0.0`
**Example scenarios:**
```bash
# Behind Cloudflare (auto-detected)
TRUST_PROXY=1
# Console shows: https://n8n-mcp.example.com
# Explicit configuration
BASE_URL=https://api.mycompany.com/mcp
# Console shows: https://api.mycompany.com/mcp
# Local development (no proxy)
# Console shows: http://localhost:3000
```
### Trust Proxy for Correct IP Logging ### Trust Proxy for Correct IP Logging
When running n8n-MCP behind a reverse proxy (Nginx, Traefik, etc.), enable trust proxy to log real client IPs instead of proxy IPs: When running n8n-MCP behind a reverse proxy (Nginx, Traefik, etc.), enable trust proxy to log real client IPs instead of proxy IPs:

View File

@@ -0,0 +1,201 @@
# Visual Studio Code Setup
:white_check_mark: This n8n MCP server is compatible with VS Code + GitHub Copilot (Chat in IDE).
## Preconditions
Assuming you've already deployed the n8n MCP server and connected it to the n8n API, and it's available at:
`https://n8n.your.production.url/`
💡 The deployment process is documented in the [HTTP Deployment Guide](./HTTP_DEPLOYMENT.md).
## Step 1
Start by creating a new VS Code project folder.
## Step 2
Create a file: `.vscode/mcp.json`
```json
{
"inputs": [
{
"type": "promptString",
"id": "n8n-mcp-token",
"description": "Your n8n-MCP AUTH_TOKEN",
"password": true
}
],
"servers": {
"n8n-mcp": {
"type": "http",
"url": "https://n8n.your.production.url/mcp",
"headers": {
"Authorization": "Bearer ${input:n8n-mcp-token}"
}
}
}
}
```
💡 The `inputs` block ensures the token is requested interactively — no need to hardcode secrets.
## Step 3
GitHub Copilot does not provide access to "thinking models" for unpaid users. To improve results, install the official [Sequential Thinking MCP server](https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking) referenced in the [VS Code docs](https://code.visualstudio.com/mcp#:~:text=Install%20Linear-,Sequential%20Thinking,-Model%20Context%20Protocol). This lightweight add-on can turn any LLM into a thinking model by enabling step-by-step reasoning. It's highly recommended to use the n8n-mcp server in combination with a sequential thinking model to generate more accurate outputs.
🔧 Alternatively, you can try enabling this setting in Copilot to unlock "thinking mode" behavior:
![VS Code Settings > GitHub > Copilot > Chat > Agent: Thinking Tool](./img/vsc_ghcp_chat_thinking_tool.png)
_(Note: I havent tested this setting myself, as I use the Sequential Thinking MCP instead)_
## Step 4
For the best results when using n8n-MCP with VS Code, use these enhanced system instructions (copy to your projects `.github/copilot-instructions.md`):
```markdown
You are an expert in n8n automation software using n8n-MCP tools. Your role is to design, build, and validate n8n workflows with maximum accuracy and efficiency.
## Core Workflow Process
1. **ALWAYS start new conversation with**: `tools_documentation()` to understand best practices and available tools.
2. **Discovery Phase** - Find the right nodes:
- Think deeply about user request and the logic you are going to build to fulfill it. Ask follow-up questions to clarify the user's intent, if something is unclear. Then, proceed with the rest of your instructions.
- `search_nodes({query: 'keyword'})` - Search by functionality
- `list_nodes({category: 'trigger'})` - Browse by category
- `list_ai_tools()` - See AI-capable nodes (remember: ANY node can be an AI tool!)
3. **Configuration Phase** - Get node details efficiently:
- `get_node_essentials(nodeType)` - Start here! Only 10-20 essential properties
- `search_node_properties(nodeType, 'auth')` - Find specific properties
- `get_node_for_task('send_email')` - Get pre-configured templates
- `get_node_documentation(nodeType)` - Human-readable docs when needed
- It is good common practice to show a visual representation of the workflow architecture to the user and asking for opinion, before moving forward.
4. **Pre-Validation Phase** - Validate BEFORE building:
- `validate_node_minimal(nodeType, config)` - Quick required fields check
- `validate_node_operation(nodeType, config, profile)` - Full operation-aware validation
- Fix any validation errors before proceeding
5. **Building Phase** - Create the workflow:
- Use validated configurations from step 4
- Connect nodes with proper structure
- Add error handling where appropriate
- Use expressions like $json, $node["NodeName"].json
- Build the workflow in an artifact for easy editing downstream (unless the user asked to create in n8n instance)
6. **Workflow Validation Phase** - Validate complete workflow:
- `validate_workflow(workflow)` - Complete validation including connections
- `validate_workflow_connections(workflow)` - Check structure and AI tool connections
- `validate_workflow_expressions(workflow)` - Validate all n8n expressions
- Fix any issues found before deployment
7. **Deployment Phase** (if n8n API configured):
- `n8n_create_workflow(workflow)` - Deploy validated workflow
- `n8n_validate_workflow({id: 'workflow-id'})` - Post-deployment validation
- `n8n_update_partial_workflow()` - Make incremental updates using diffs
- `n8n_trigger_webhook_workflow()` - Test webhook workflows
## Key Insights
- **USE CODE NODE ONLY WHEN IT IS NECESSARY** - always prefer to use standard nodes over code node. Use code node only when you are sure you need it.
- **VALIDATE EARLY AND OFTEN** - Catch errors before they reach deployment
- **USE DIFF UPDATES** - Use n8n_update_partial_workflow for 80-90% token savings
- **ANY node can be an AI tool** - not just those with usableAsTool=true
- **Pre-validate configurations** - Use validate_node_minimal before building
- **Post-validate workflows** - Always validate complete workflows before deployment
- **Incremental updates** - Use diff operations for existing workflows
- **Test thoroughly** - Validate both locally and after deployment to n8n
## Validation Strategy
### Before Building:
1. validate_node_minimal() - Check required fields
2. validate_node_operation() - Full configuration validation
3. Fix all errors before proceeding
### After Building:
1. validate_workflow() - Complete workflow validation
2. validate_workflow_connections() - Structure validation
3. validate_workflow_expressions() - Expression syntax check
### After Deployment:
1. n8n_validate_workflow({id}) - Validate deployed workflow
2. n8n_list_executions() - Monitor execution status
3. n8n_update_partial_workflow() - Fix issues using diffs
## Response Structure
1. **Discovery**: Show available nodes and options
2. **Pre-Validation**: Validate node configurations first
3. **Configuration**: Show only validated, working configs
4. **Building**: Construct workflow with validated components
5. **Workflow Validation**: Full workflow validation results
6. **Deployment**: Deploy only after all validations pass
7. **Post-Validation**: Verify deployment succeeded
## Example Workflow
### 1. Discovery & Configuration
search_nodes({query: 'slack'})
get_node_essentials('n8n-nodes-base.slack')
### 2. Pre-Validation
validate_node_minimal('n8n-nodes-base.slack', {resource:'message', operation:'send'})
validate_node_operation('n8n-nodes-base.slack', fullConfig, 'runtime')
### 3. Build Workflow
// Create workflow JSON with validated configs
### 4. Workflow Validation
validate_workflow(workflowJson)
validate_workflow_connections(workflowJson)
validate_workflow_expressions(workflowJson)
### 5. Deploy (if configured)
n8n_create_workflow(validatedWorkflow)
n8n_validate_workflow({id: createdWorkflowId})
### 6. Update Using Diffs
n8n_update_partial_workflow({
workflowId: id,
operations: [
{type: 'updateNode', nodeId: 'slack1', changes: {position: [100, 200]}}
]
})
## Important Rules
- ALWAYS validate before building
- ALWAYS validate after building
- NEVER deploy unvalidated workflows
- USE diff operations for updates (80-90% token savings)
- STATE validation results clearly
- FIX all errors before proceeding
```
This helps the agent produce higher-quality, well-structured n8n workflows.
🔧 Important: To ensure the instructions are always included, make sure this checkbox is enabled in your Copilot settings:
![VS Code Settings > GitHub > Copilot > Chat > Code Generation: Use Instruction Files](./img/vsc_ghcp_chat_instruction_files.png)
## Step 5
Switch GitHub Copilot to Agent mode:
![VS Code > GitHub Copilot Chat > Edit files in your workspace in agent mode](./img/vsc_ghcp_chat_agent_mode.png)
## Step 6 - Try it!
Heres an example prompt I used:
```
#fetch https://blog.n8n.io/rag-chatbot/
use #sequentialthinking and #n8n-mcp tools to build a new n8n workflow step-by-step following the guidelines in the blog.
In the end, please deploy a fully-functional n8n workflow.
```
🧪 My result wasnt perfect (a bit messy workflow), but I'm genuinely happy that it created anything autonomously 😄 Stay tuned for updates!

69
docs/WINDSURF_SETUP.md Normal file
View File

@@ -0,0 +1,69 @@
# Windsurf Setup
Connect n8n-MCP to Windsurf IDE for enhanced n8n workflow development with AI assistance.
[![n8n-mcp Windsurf Setup Tutorial](./img/windsurf_tut.png)](https://www.youtube.com/watch?v=klxxT1__izg)
## Video Tutorial
Watch the complete setup process: [n8n-MCP Windsurf Setup Tutorial](https://www.youtube.com/watch?v=klxxT1__izg)
## Setup Process
### 1. Access MCP Configuration
1. Go to Settings in Windsurf
2. Navigate to Windsurf Settings
3. Go to MCP Servers > Manage Plugins
4. Click "View Raw Config"
### 2. Add n8n-MCP Configuration
Copy the configuration from this repository and add it to your MCP config:
**Basic configuration (documentation tools only):**
```json
{
"mcpServers": {
"n8n-mcp": {
"command": "npx",
"args": ["n8n-mcp"],
"env": {
"MCP_MODE": "stdio",
"LOG_LEVEL": "error",
"DISABLE_CONSOLE_OUTPUT": "true"
}
}
}
}
```
**Full configuration (with n8n management tools):**
```json
{
"mcpServers": {
"n8n-mcp": {
"command": "npx",
"args": ["n8n-mcp"],
"env": {
"MCP_MODE": "stdio",
"LOG_LEVEL": "error",
"DISABLE_CONSOLE_OUTPUT": "true",
"N8N_API_URL": "https://your-n8n-instance.com",
"N8N_API_KEY": "your-api-key"
}
}
}
}
```
### 3. Configure n8n Connection
1. Replace `https://your-n8n-instance.com` with your actual n8n URL
2. Replace `your-api-key` with your n8n API key
3. Click refresh to apply the changes
### 4. Set Up Project Instructions
1. Create a `.windsurfrules` file in your project root
2. Copy the Claude Project instructions from the [main README's Claude Project Setup section](../README.md#-claude-project-setup)

BIN
docs/img/cc_command.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
docs/img/cc_connected.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/img/cursor_tut.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
docs/img/windsurf_tut.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.7.13", "version": "2.7.16",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {
@@ -41,11 +41,13 @@
"test:workflow-diff": "node dist/scripts/test-workflow-diff.js", "test:workflow-diff": "node dist/scripts/test-workflow-diff.js",
"test:transactional-diff": "node dist/scripts/test-transactional-diff.js", "test:transactional-diff": "node dist/scripts/test-transactional-diff.js",
"test:tools-documentation": "node dist/scripts/test-tools-documentation.js", "test:tools-documentation": "node dist/scripts/test-tools-documentation.js",
"test:url-configuration": "npm run build && ts-node scripts/test-url-configuration.ts",
"test:search-improvements": "node dist/scripts/test-search-improvements.js", "test:search-improvements": "node dist/scripts/test-search-improvements.js",
"test:fts5-search": "node dist/scripts/test-fts5-search.js", "test:fts5-search": "node dist/scripts/test-fts5-search.js",
"migrate:fts5": "node dist/scripts/migrate-nodes-fts.js", "migrate:fts5": "node dist/scripts/migrate-nodes-fts.js",
"test:mcp:update-partial": "node dist/scripts/test-mcp-n8n-update-partial.js", "test:mcp:update-partial": "node dist/scripts/test-mcp-n8n-update-partial.js",
"test:update-partial:debug": "node dist/scripts/test-update-partial-debug.js", "test:update-partial:debug": "node dist/scripts/test-update-partial-debug.js",
"test:issue-45-fix": "node dist/scripts/test-issue-45-fix.js",
"test:auth-logging": "tsx scripts/test-auth-logging.ts", "test:auth-logging": "tsx scripts/test-auth-logging.ts",
"sanitize:templates": "node dist/scripts/sanitize-templates.js", "sanitize:templates": "node dist/scripts/sanitize-templates.js",
"db:rebuild": "node dist/scripts/rebuild-database.js", "db:rebuild": "node dist/scripts/rebuild-database.js",

View File

@@ -8,7 +8,10 @@
const http = require('http'); const http = require('http');
const readline = require('readline'); const readline = require('readline');
const MCP_URL = process.env.MCP_URL || 'http://localhost:3000/mcp'; // Use MCP_URL from environment or construct from HOST/PORT if available
const defaultHost = process.env.HOST || 'localhost';
const defaultPort = process.env.PORT || '3000';
const MCP_URL = process.env.MCP_URL || `http://${defaultHost}:${defaultPort}/mcp`;
const AUTH_TOKEN = process.env.AUTH_TOKEN || process.argv[2]; const AUTH_TOKEN = process.env.AUTH_TOKEN || process.argv[2];
if (!AUTH_TOKEN) { if (!AUTH_TOKEN) {

96
scripts/test-security.ts Normal file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env node
import axios from 'axios';
import { spawn } from 'child_process';
async function testMaliciousHeaders() {
console.log('🔒 Testing Security Fixes...\n');
// Start server with TRUST_PROXY enabled
const serverProcess = spawn('node', ['dist/mcp/index.js'], {
env: {
...process.env,
MCP_MODE: 'http',
AUTH_TOKEN: 'test-security-token-32-characters-long',
PORT: '3999',
TRUST_PROXY: '1'
}
});
// Wait for server to start
await new Promise(resolve => {
serverProcess.stdout.on('data', (data) => {
if (data.toString().includes('Press Ctrl+C to stop')) {
resolve(undefined);
}
});
});
const testCases = [
{
name: 'Valid proxy headers',
headers: {
'X-Forwarded-Host': 'example.com',
'X-Forwarded-Proto': 'https'
}
},
{
name: 'Malicious host header (with path)',
headers: {
'X-Forwarded-Host': 'evil.com/path/to/evil',
'X-Forwarded-Proto': 'https'
}
},
{
name: 'Malicious host header (with @)',
headers: {
'X-Forwarded-Host': 'user@evil.com',
'X-Forwarded-Proto': 'https'
}
},
{
name: 'Invalid hostname (multiple dots)',
headers: {
'X-Forwarded-Host': '.....',
'X-Forwarded-Proto': 'https'
}
},
{
name: 'IPv6 address',
headers: {
'X-Forwarded-Host': '[::1]:3000',
'X-Forwarded-Proto': 'https'
}
}
];
for (const testCase of testCases) {
try {
const response = await axios.get('http://localhost:3999/', {
headers: testCase.headers,
timeout: 2000
});
const endpoints = response.data.endpoints;
const healthUrl = endpoints?.health?.url || 'N/A';
console.log(`${testCase.name}`);
console.log(` Response: ${healthUrl}`);
// Check if malicious headers were blocked
if (testCase.name.includes('Malicious') || testCase.name.includes('Invalid')) {
if (healthUrl.includes('evil.com') || healthUrl.includes('@') || healthUrl.includes('.....')) {
console.log(' ❌ SECURITY ISSUE: Malicious header was not blocked!');
} else {
console.log(' ✅ Malicious header was blocked');
}
}
} catch (error) {
console.log(`${testCase.name} - Request failed`);
}
console.log('');
}
serverProcess.kill();
}
testMaliciousHeaders().catch(console.error);

192
scripts/test-url-configuration.ts Executable file
View File

@@ -0,0 +1,192 @@
#!/usr/bin/env node
/**
* Test script for URL configuration in n8n-MCP HTTP server
* Tests various BASE_URL, TRUST_PROXY, and proxy header scenarios
*/
import axios from 'axios';
import { spawn } from 'child_process';
import { logger } from '../src/utils/logger';
interface TestCase {
name: string;
env: Record<string, string>;
expectedUrls?: {
health: string;
mcp: string;
};
proxyHeaders?: Record<string, string>;
}
const testCases: TestCase[] = [
{
name: 'Default configuration (no BASE_URL)',
env: {
MCP_MODE: 'http',
AUTH_TOKEN: 'test-token-for-testing-only',
PORT: '3001'
},
expectedUrls: {
health: 'http://localhost:3001/health',
mcp: 'http://localhost:3001/mcp'
}
},
{
name: 'With BASE_URL configured',
env: {
MCP_MODE: 'http',
AUTH_TOKEN: 'test-token-for-testing-only',
PORT: '3002',
BASE_URL: 'https://n8n-mcp.example.com'
},
expectedUrls: {
health: 'https://n8n-mcp.example.com/health',
mcp: 'https://n8n-mcp.example.com/mcp'
}
},
{
name: 'With PUBLIC_URL configured',
env: {
MCP_MODE: 'http',
AUTH_TOKEN: 'test-token-for-testing-only',
PORT: '3003',
PUBLIC_URL: 'https://api.company.com/mcp'
},
expectedUrls: {
health: 'https://api.company.com/mcp/health',
mcp: 'https://api.company.com/mcp/mcp'
}
},
{
name: 'With TRUST_PROXY and proxy headers',
env: {
MCP_MODE: 'http',
AUTH_TOKEN: 'test-token-for-testing-only',
PORT: '3004',
TRUST_PROXY: '1'
},
proxyHeaders: {
'X-Forwarded-Proto': 'https',
'X-Forwarded-Host': 'proxy.example.com'
}
},
{
name: 'Fixed HTTP implementation',
env: {
MCP_MODE: 'http',
USE_FIXED_HTTP: 'true',
AUTH_TOKEN: 'test-token-for-testing-only',
PORT: '3005',
BASE_URL: 'https://fixed.example.com'
},
expectedUrls: {
health: 'https://fixed.example.com/health',
mcp: 'https://fixed.example.com/mcp'
}
}
];
async function runTest(testCase: TestCase): Promise<void> {
console.log(`\n🧪 Testing: ${testCase.name}`);
console.log('Environment:', testCase.env);
const serverProcess = spawn('node', ['dist/mcp/index.js'], {
env: { ...process.env, ...testCase.env }
});
let serverOutput = '';
let serverStarted = false;
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
serverProcess.kill();
reject(new Error('Server startup timeout'));
}, 10000);
serverProcess.stdout.on('data', (data) => {
const output = data.toString();
serverOutput += output;
if (output.includes('Press Ctrl+C to stop the server')) {
serverStarted = true;
clearTimeout(timeout);
// Give server a moment to fully initialize
setTimeout(async () => {
try {
// Test root endpoint
const rootUrl = `http://localhost:${testCase.env.PORT}/`;
const rootResponse = await axios.get(rootUrl, {
headers: testCase.proxyHeaders || {}
});
console.log('✅ Root endpoint response:');
console.log(` - Endpoints: ${JSON.stringify(rootResponse.data.endpoints, null, 2)}`);
// Test health endpoint
const healthUrl = `http://localhost:${testCase.env.PORT}/health`;
const healthResponse = await axios.get(healthUrl);
console.log(`✅ Health endpoint status: ${healthResponse.data.status}`);
// Test MCP info endpoint
const mcpUrl = `http://localhost:${testCase.env.PORT}/mcp`;
const mcpResponse = await axios.get(mcpUrl);
console.log(`✅ MCP info endpoint: ${mcpResponse.data.description}`);
// Check console output
if (testCase.expectedUrls) {
const outputContainsExpectedUrls =
serverOutput.includes(testCase.expectedUrls.health) &&
serverOutput.includes(testCase.expectedUrls.mcp);
if (outputContainsExpectedUrls) {
console.log('✅ Console output shows expected URLs');
} else {
console.log('❌ Console output does not show expected URLs');
console.log('Expected:', testCase.expectedUrls);
}
}
serverProcess.kill();
resolve();
} catch (error) {
console.error('❌ Test failed:', error instanceof Error ? error.message : String(error));
serverProcess.kill();
reject(error);
}
}, 500);
}
});
serverProcess.stderr.on('data', (data) => {
console.error('Server error:', data.toString());
});
serverProcess.on('close', (code) => {
if (!serverStarted) {
reject(new Error(`Server exited with code ${code} before starting`));
} else {
resolve();
}
});
});
}
async function main() {
console.log('🚀 n8n-MCP URL Configuration Test Suite');
console.log('======================================');
for (const testCase of testCases) {
try {
await runTest(testCase);
console.log('✅ Test passed\n');
} catch (error) {
console.error('❌ Test failed:', error instanceof Error ? error.message : String(error));
console.log('\n');
}
}
console.log('✨ All tests completed');
}
main().catch(console.error);

View File

@@ -11,6 +11,8 @@ import { ConsoleManager } from './utils/console-manager';
import { logger } from './utils/logger'; import { logger } from './utils/logger';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import { getStartupBaseUrl, formatEndpointUrls, detectBaseUrl } from './utils/url-detector';
import { PROJECT_VERSION } from './utils/version';
dotenv.config(); dotenv.config();
@@ -244,12 +246,44 @@ export class SingleSessionHTTPServer {
next(); next();
}); });
// Root endpoint with API information
app.get('/', (req, res) => {
const port = parseInt(process.env.PORT || '3000');
const host = process.env.HOST || '0.0.0.0';
const baseUrl = detectBaseUrl(req, host, port);
const endpoints = formatEndpointUrls(baseUrl);
res.json({
name: 'n8n Documentation MCP Server',
version: PROJECT_VERSION,
description: 'Model Context Protocol server providing comprehensive n8n node documentation and workflow management',
endpoints: {
health: {
url: endpoints.health,
method: 'GET',
description: 'Health check and status information'
},
mcp: {
url: endpoints.mcp,
method: 'GET/POST',
description: 'MCP endpoint - GET for info, POST for JSON-RPC'
}
},
authentication: {
type: 'Bearer Token',
header: 'Authorization: Bearer <token>',
required_for: ['POST /mcp']
},
documentation: 'https://github.com/czlonkowski/n8n-mcp'
});
});
// Health check endpoint (no body parsing needed for GET) // Health check endpoint (no body parsing needed for GET)
app.get('/health', (req, res) => { app.get('/health', (req, res) => {
res.json({ res.json({
status: 'ok', status: 'ok',
mode: 'single-session', mode: 'single-session',
version: '2.3.2', version: PROJECT_VERSION,
uptime: Math.floor(process.uptime()), uptime: Math.floor(process.uptime()),
sessionActive: !!this.session, sessionActive: !!this.session,
sessionAge: this.session sessionAge: this.session
@@ -264,6 +298,35 @@ export class SingleSessionHTTPServer {
}); });
}); });
// MCP information endpoint (no auth required for discovery)
app.get('/mcp', (req, res) => {
res.json({
description: 'n8n Documentation MCP Server',
version: PROJECT_VERSION,
endpoints: {
mcp: {
method: 'POST',
path: '/mcp',
description: 'Main MCP JSON-RPC endpoint',
authentication: 'Bearer token required'
},
health: {
method: 'GET',
path: '/health',
description: 'Health check endpoint',
authentication: 'None'
},
root: {
method: 'GET',
path: '/',
description: 'API information',
authentication: 'None'
}
},
documentation: 'https://github.com/czlonkowski/n8n-mcp'
});
});
// Main MCP endpoint with authentication // Main MCP endpoint with authentication
app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => { app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => {
// Enhanced authentication check with specific logging // Enhanced authentication check with specific logging
@@ -361,9 +424,14 @@ export class SingleSessionHTTPServer {
this.expressServer = app.listen(port, host, () => { this.expressServer = app.listen(port, host, () => {
logger.info(`n8n MCP Single-Session HTTP Server started`, { port, host }); logger.info(`n8n MCP Single-Session HTTP Server started`, { port, host });
// Detect the base URL using our utility
const baseUrl = getStartupBaseUrl(host, port);
const endpoints = formatEndpointUrls(baseUrl);
console.log(`n8n MCP Single-Session HTTP Server running on ${host}:${port}`); console.log(`n8n MCP Single-Session HTTP Server running on ${host}:${port}`);
console.log(`Health check: http://localhost:${port}/health`); console.log(`Health check: ${endpoints.health}`);
console.log(`MCP endpoint: http://localhost:${port}/mcp`); console.log(`MCP endpoint: ${endpoints.mcp}`);
console.log('\nPress Ctrl+C to stop the server'); console.log('\nPress Ctrl+C to stop the server');
// Start periodic warning timer if using default token // Start periodic warning timer if using default token
@@ -375,6 +443,12 @@ export class SingleSessionHTTPServer {
} }
}, 300000); // Every 5 minutes }, 300000); // Every 5 minutes
} }
if (process.env.BASE_URL || process.env.PUBLIC_URL) {
console.log(`\nPublic URL configured: ${baseUrl}`);
} else if (process.env.TRUST_PROXY && Number(process.env.TRUST_PROXY) > 0) {
console.log(`\nNote: TRUST_PROXY is enabled. URLs will be auto-detected from proxy headers.`);
}
}); });
// Handle server errors // Handle server errors

View File

@@ -13,6 +13,7 @@ import { PROJECT_VERSION } from './utils/version';
import { isN8nApiConfigured } from './config/n8n-api'; import { isN8nApiConfigured } from './config/n8n-api';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { getStartupBaseUrl, formatEndpointUrls, detectBaseUrl } from './utils/url-detector';
dotenv.config(); dotenv.config();
@@ -159,6 +160,38 @@ export async function startFixedHTTPServer() {
const mcpServer = new N8NDocumentationMCPServer(); const mcpServer = new N8NDocumentationMCPServer();
logger.info('Created persistent MCP server instance'); logger.info('Created persistent MCP server instance');
// Root endpoint with API information
app.get('/', (req, res) => {
const port = parseInt(process.env.PORT || '3000');
const host = process.env.HOST || '0.0.0.0';
const baseUrl = detectBaseUrl(req, host, port);
const endpoints = formatEndpointUrls(baseUrl);
res.json({
name: 'n8n Documentation MCP Server',
version: PROJECT_VERSION,
description: 'Model Context Protocol server providing comprehensive n8n node documentation and workflow management',
endpoints: {
health: {
url: endpoints.health,
method: 'GET',
description: 'Health check and status information'
},
mcp: {
url: endpoints.mcp,
method: 'GET/POST',
description: 'MCP endpoint - GET for info, POST for JSON-RPC'
}
},
authentication: {
type: 'Bearer Token',
header: 'Authorization: Bearer <token>',
required_for: ['POST /mcp']
},
documentation: 'https://github.com/czlonkowski/n8n-mcp'
});
});
// Health check endpoint // Health check endpoint
app.get('/health', (req, res) => { app.get('/health', (req, res) => {
res.json({ res.json({
@@ -195,6 +228,35 @@ export async function startFixedHTTPServer() {
} }
}); });
// MCP information endpoint (no auth required for discovery)
app.get('/mcp', (req, res) => {
res.json({
description: 'n8n Documentation MCP Server',
version: PROJECT_VERSION,
endpoints: {
mcp: {
method: 'POST',
path: '/mcp',
description: 'Main MCP JSON-RPC endpoint',
authentication: 'Bearer token required'
},
health: {
method: 'GET',
path: '/health',
description: 'Health check endpoint',
authentication: 'None'
},
root: {
method: 'GET',
path: '/',
description: 'API information',
authentication: 'None'
}
},
documentation: 'https://github.com/czlonkowski/n8n-mcp'
});
});
// Main MCP endpoint - handle each request with custom transport handling // Main MCP endpoint - handle each request with custom transport handling
app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => { app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => {
const startTime = Date.now(); const startTime = Date.now();
@@ -428,9 +490,14 @@ export async function startFixedHTTPServer() {
expressServer = app.listen(port, host, () => { expressServer = app.listen(port, host, () => {
logger.info(`n8n MCP Fixed HTTP Server started`, { port, host }); logger.info(`n8n MCP Fixed HTTP Server started`, { port, host });
// Detect the base URL using our utility
const baseUrl = getStartupBaseUrl(host, port);
const endpoints = formatEndpointUrls(baseUrl);
console.log(`n8n MCP Fixed HTTP Server running on ${host}:${port}`); console.log(`n8n MCP Fixed HTTP Server running on ${host}:${port}`);
console.log(`Health check: http://localhost:${port}/health`); console.log(`Health check: ${endpoints.health}`);
console.log(`MCP endpoint: http://localhost:${port}/mcp`); console.log(`MCP endpoint: ${endpoints.mcp}`);
console.log('\nPress Ctrl+C to stop the server'); console.log('\nPress Ctrl+C to stop the server');
// Start periodic warning timer if using default token // Start periodic warning timer if using default token
@@ -442,6 +509,12 @@ export async function startFixedHTTPServer() {
} }
}, 300000); // Every 5 minutes }, 300000); // Every 5 minutes
} }
if (process.env.BASE_URL || process.env.PUBLIC_URL) {
console.log(`\nPublic URL configured: ${baseUrl}`);
} else if (process.env.TRUST_PROXY && Number(process.env.TRUST_PROXY) > 0) {
console.log(`\nNote: TRUST_PROXY is enabled. URLs will be auto-detected from proxy headers.`);
}
}); });
// Handle errors // Handle errors

View File

@@ -15,7 +15,8 @@ import * as path from 'path';
async function rebuild() { async function rebuild() {
console.log('🔄 Rebuilding n8n node database...\n'); console.log('🔄 Rebuilding n8n node database...\n');
const db = await createDatabaseAdapter('./data/nodes.db'); const dbPath = process.env.NODE_DB_PATH || './data/nodes.db';
const db = await createDatabaseAdapter(dbPath);
const loader = new N8nNodeLoader(); const loader = new N8nNodeLoader();
const parser = new NodeParser(); const parser = new NodeParser();
const mapper = new DocsMapper(); const mapper = new DocsMapper();

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env node
/**
* Test for Issue #45 Fix: Partial Update Tool Validation/Execution Discrepancy
*
* This test verifies that the cleanWorkflowForUpdate function no longer adds
* default settings to workflows during updates, which was causing the n8n API
* to reject requests with "settings must NOT have additional properties".
*/
import { config } from 'dotenv';
import { logger } from '../utils/logger';
import { cleanWorkflowForUpdate, cleanWorkflowForCreate } from '../services/n8n-validation';
import { Workflow } from '../types/n8n-api';
// Load environment variables
config();
function testCleanWorkflowFunctions() {
logger.info('Testing Issue #45 Fix: cleanWorkflowForUpdate should not add default settings\n');
// Test 1: cleanWorkflowForUpdate with workflow without settings
logger.info('=== Test 1: cleanWorkflowForUpdate without settings ===');
const workflowWithoutSettings: Workflow = {
id: 'test-123',
name: 'Test Workflow',
nodes: [],
connections: {},
active: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
versionId: 'version-123'
};
const cleanedUpdate = cleanWorkflowForUpdate(workflowWithoutSettings);
if ('settings' in cleanedUpdate) {
logger.error('❌ FAIL: cleanWorkflowForUpdate added settings when it should not have');
logger.error(' Found settings:', JSON.stringify(cleanedUpdate.settings));
} else {
logger.info('✅ PASS: cleanWorkflowForUpdate did not add settings');
}
// Test 2: cleanWorkflowForUpdate with existing settings
logger.info('\n=== Test 2: cleanWorkflowForUpdate with existing settings ===');
const workflowWithSettings: Workflow = {
...workflowWithoutSettings,
settings: {
executionOrder: 'v1',
saveDataErrorExecution: 'none',
saveDataSuccessExecution: 'none',
saveManualExecutions: false,
saveExecutionProgress: false
}
};
const cleanedUpdate2 = cleanWorkflowForUpdate(workflowWithSettings);
if ('settings' in cleanedUpdate2) {
const settingsMatch = JSON.stringify(cleanedUpdate2.settings) === JSON.stringify(workflowWithSettings.settings);
if (settingsMatch) {
logger.info('✅ PASS: cleanWorkflowForUpdate preserved existing settings without modification');
} else {
logger.error('❌ FAIL: cleanWorkflowForUpdate modified existing settings');
logger.error(' Original:', JSON.stringify(workflowWithSettings.settings));
logger.error(' Cleaned:', JSON.stringify(cleanedUpdate2.settings));
}
} else {
logger.error('❌ FAIL: cleanWorkflowForUpdate removed existing settings');
}
// Test 3: cleanWorkflowForUpdate with partial settings
logger.info('\n=== Test 3: cleanWorkflowForUpdate with partial settings ===');
const workflowWithPartialSettings: Workflow = {
...workflowWithoutSettings,
settings: {
executionOrder: 'v1'
// Missing other default properties
}
};
const cleanedUpdate3 = cleanWorkflowForUpdate(workflowWithPartialSettings);
if ('settings' in cleanedUpdate3) {
const settingsKeys = cleanedUpdate3.settings ? Object.keys(cleanedUpdate3.settings) : [];
const hasOnlyExecutionOrder = settingsKeys.length === 1 &&
cleanedUpdate3.settings?.executionOrder === 'v1';
if (hasOnlyExecutionOrder) {
logger.info('✅ PASS: cleanWorkflowForUpdate preserved partial settings without adding defaults');
} else {
logger.error('❌ FAIL: cleanWorkflowForUpdate added default properties to partial settings');
logger.error(' Original keys:', Object.keys(workflowWithPartialSettings.settings || {}));
logger.error(' Cleaned keys:', settingsKeys);
}
} else {
logger.error('❌ FAIL: cleanWorkflowForUpdate removed partial settings');
}
// Test 4: Verify cleanWorkflowForCreate still adds defaults
logger.info('\n=== Test 4: cleanWorkflowForCreate should add default settings ===');
const newWorkflow = {
name: 'New Workflow',
nodes: [],
connections: {}
};
const cleanedCreate = cleanWorkflowForCreate(newWorkflow);
if ('settings' in cleanedCreate && cleanedCreate.settings) {
const hasDefaults =
cleanedCreate.settings.executionOrder === 'v1' &&
cleanedCreate.settings.saveDataErrorExecution === 'all' &&
cleanedCreate.settings.saveDataSuccessExecution === 'all' &&
cleanedCreate.settings.saveManualExecutions === true &&
cleanedCreate.settings.saveExecutionProgress === true;
if (hasDefaults) {
logger.info('✅ PASS: cleanWorkflowForCreate correctly adds default settings');
} else {
logger.error('❌ FAIL: cleanWorkflowForCreate added settings but not with correct defaults');
logger.error(' Settings:', JSON.stringify(cleanedCreate.settings));
}
} else {
logger.error('❌ FAIL: cleanWorkflowForCreate did not add default settings');
}
// Test 5: Verify read-only fields are removed
logger.info('\n=== Test 5: cleanWorkflowForUpdate removes read-only fields ===');
const workflowWithReadOnly: any = {
...workflowWithoutSettings,
staticData: { some: 'data' },
pinData: { node1: 'data' },
tags: ['tag1', 'tag2'],
isArchived: true,
usedCredentials: ['cred1'],
sharedWithProjects: ['proj1'],
triggerCount: 5,
shared: true,
active: true
};
const cleanedReadOnly = cleanWorkflowForUpdate(workflowWithReadOnly);
const removedFields = [
'id', 'createdAt', 'updatedAt', 'versionId', 'meta',
'staticData', 'pinData', 'tags', 'isArchived',
'usedCredentials', 'sharedWithProjects', 'triggerCount',
'shared', 'active'
];
const hasRemovedFields = removedFields.some(field => field in cleanedReadOnly);
if (!hasRemovedFields) {
logger.info('✅ PASS: cleanWorkflowForUpdate correctly removed all read-only fields');
} else {
const foundFields = removedFields.filter(field => field in cleanedReadOnly);
logger.error('❌ FAIL: cleanWorkflowForUpdate did not remove these fields:', foundFields);
}
logger.info('\n=== Test Summary ===');
logger.info('All tests completed. The fix ensures that cleanWorkflowForUpdate only removes fields');
logger.info('without adding default settings, preventing the n8n API validation error.');
}
// Run the tests
testCleanWorkflowFunctions();

View File

@@ -93,6 +93,19 @@ export function cleanWorkflowForCreate(workflow: Partial<Workflow>): Partial<Wor
return cleanedWorkflow; return cleanedWorkflow;
} }
/**
* Clean workflow data for update operations.
*
* This function removes read-only and computed fields that should not be sent
* in API update requests. It does NOT add any default values or new fields.
*
* Note: Unlike cleanWorkflowForCreate, this function does not add default settings.
* The n8n API will reject update requests that include properties not present in
* the original workflow ("settings must NOT have additional properties" error).
*
* @param workflow - The workflow object to clean
* @returns A cleaned partial workflow suitable for API updates
*/
export function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow> { export function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow> {
const { const {
// Remove read-only/computed fields // Remove read-only/computed fields
@@ -116,11 +129,6 @@ export function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow> {
...cleanedWorkflow ...cleanedWorkflow
} = workflow as any; } = workflow as any;
// Ensure settings are present
if (!cleanedWorkflow.settings) {
cleanedWorkflow.settings = defaultWorkflowSettings;
}
return cleanedWorkflow; return cleanedWorkflow;
} }

111
src/utils/url-detector.ts Normal file
View File

@@ -0,0 +1,111 @@
import { Request } from 'express';
import { logger } from './logger';
/**
* Validates a hostname to prevent header injection attacks
*/
function isValidHostname(host: string): boolean {
// Allow alphanumeric, dots, hyphens, and optional port
return /^[a-zA-Z0-9.-]+(:[0-9]+)?$/.test(host) && host.length < 256;
}
/**
* Validates a URL string
*/
function isValidUrl(url: string): boolean {
try {
const parsed = new URL(url);
// Only allow http and https protocols
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
} catch {
return false;
}
}
/**
* Detects the base URL for the server, considering:
* 1. Explicitly configured BASE_URL or PUBLIC_URL
* 2. Proxy headers (X-Forwarded-Proto, X-Forwarded-Host)
* 3. Host and port configuration
*/
export function detectBaseUrl(req: Request | null, host: string, port: number): string {
try {
// 1. Check for explicitly configured URL
const configuredUrl = process.env.BASE_URL || process.env.PUBLIC_URL;
if (configuredUrl) {
if (isValidUrl(configuredUrl)) {
logger.debug('Using configured BASE_URL/PUBLIC_URL', { url: configuredUrl });
return configuredUrl.replace(/\/$/, ''); // Remove trailing slash
} else {
logger.warn('Invalid BASE_URL/PUBLIC_URL configured, falling back to auto-detection', { url: configuredUrl });
}
}
// 2. If we have a request, try to detect from proxy headers
if (req && process.env.TRUST_PROXY && Number(process.env.TRUST_PROXY) > 0) {
const proto = req.get('X-Forwarded-Proto') || req.protocol || 'http';
const forwardedHost = req.get('X-Forwarded-Host');
const hostHeader = req.get('Host');
const detectedHost = forwardedHost || hostHeader;
if (detectedHost && isValidHostname(detectedHost)) {
const baseUrl = `${proto}://${detectedHost}`;
logger.debug('Detected URL from proxy headers', {
proto,
forwardedHost,
hostHeader,
baseUrl
});
return baseUrl;
} else if (detectedHost) {
logger.warn('Invalid hostname detected in proxy headers, using fallback', { detectedHost });
}
}
// 3. Fall back to configured host and port
const displayHost = host === '0.0.0.0' ? 'localhost' : host;
const protocol = 'http'; // Default to http for local bindings
// Don't show standard ports (for http only in this fallback case)
const needsPort = port !== 80;
const baseUrl = needsPort ?
`${protocol}://${displayHost}:${port}` :
`${protocol}://${displayHost}`;
logger.debug('Using fallback URL from host/port', {
host,
displayHost,
port,
baseUrl
});
return baseUrl;
} catch (error) {
logger.error('Error detecting base URL, using fallback', error);
// Safe fallback
return `http://localhost:${port}`;
}
}
/**
* Gets the base URL for console display during startup
* This is used when we don't have a request object yet
*/
export function getStartupBaseUrl(host: string, port: number): string {
return detectBaseUrl(null, host, port);
}
/**
* Formats endpoint URLs for display
*/
export function formatEndpointUrls(baseUrl: string): {
health: string;
mcp: string;
root: string;
} {
return {
health: `${baseUrl}/health`,
mcp: `${baseUrl}/mcp`,
root: baseUrl
};
}