Skip to main content
Glama

n8n-MCP

by 88-888
docker-config.test.ts15.3 kB
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'; import { execSync, spawn } from 'child_process'; import path from 'path'; import fs from 'fs'; import os from 'os'; import { exec, waitForHealthy, isRunningInHttpMode, getProcessEnv } from './test-helpers'; // Skip tests if not in CI or if Docker is not available const SKIP_DOCKER_TESTS = process.env.CI !== 'true' && !process.env.RUN_DOCKER_TESTS; const describeDocker = SKIP_DOCKER_TESTS ? describe.skip : describe; // Helper to check if Docker is available async function isDockerAvailable(): Promise<boolean> { try { await exec('docker --version'); return true; } catch { return false; } } // Helper to generate unique container names function generateContainerName(suffix: string): string { return `n8n-mcp-test-${Date.now()}-${suffix}`; } // Helper to clean up containers async function cleanupContainer(containerName: string) { try { await exec(`docker stop ${containerName}`); await exec(`docker rm ${containerName}`); } catch { // Ignore errors - container might not exist } } describeDocker('Docker Config File Integration', () => { let tempDir: string; let dockerAvailable: boolean; const imageName = 'n8n-mcp-test:latest'; const containers: string[] = []; beforeAll(async () => { dockerAvailable = await isDockerAvailable(); if (!dockerAvailable) { console.warn('Docker not available, skipping Docker integration tests'); return; } // Check if image exists let imageExists = false; try { await exec(`docker image inspect ${imageName}`); imageExists = true; } catch { imageExists = false; } // Build test image if in CI or if explicitly requested or if image doesn't exist if (!imageExists || process.env.CI === 'true' || process.env.BUILD_DOCKER_TEST_IMAGE === 'true') { const projectRoot = path.resolve(__dirname, '../../../'); console.log('Building Docker image for tests...'); try { execSync(`docker build -t ${imageName} .`, { cwd: projectRoot, stdio: 'inherit' }); console.log('Docker image built successfully'); } catch (error) { console.error('Failed to build Docker image:', error); throw new Error('Docker image build failed - tests cannot continue'); } } else { console.log(`Using existing Docker image: ${imageName}`); } }, 60000); // Increase timeout to 60s for Docker build beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-config-test-')); }); afterEach(async () => { // Clean up containers for (const container of containers) { await cleanupContainer(container); } containers.length = 0; // Clean up temp directory if (fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true }); } }); describe('Config file loading', () => { it('should load config.json and set environment variables', async () => { if (!dockerAvailable) return; const containerName = generateContainerName('config-load'); containers.push(containerName); // Create config file const configPath = path.join(tempDir, 'config.json'); const config = { mcp_mode: 'http', auth_token: 'test-token-from-config', port: 3456, database: { path: '/data/custom.db' } }; fs.writeFileSync(configPath, JSON.stringify(config)); // Run container with config file mounted const { stdout } = await exec( `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} sh -c "env | grep -E '^(MCP_MODE|AUTH_TOKEN|PORT|DATABASE_PATH)=' | sort"` ); const envVars = stdout.trim().split('\n').reduce((acc, line) => { const [key, value] = line.split('='); acc[key] = value; return acc; }, {} as Record<string, string>); expect(envVars.MCP_MODE).toBe('http'); expect(envVars.AUTH_TOKEN).toBe('test-token-from-config'); expect(envVars.PORT).toBe('3456'); expect(envVars.DATABASE_PATH).toBe('/data/custom.db'); }); it('should give precedence to environment variables over config file', async () => { if (!dockerAvailable) return; const containerName = generateContainerName('env-precedence'); containers.push(containerName); // Create config file const configPath = path.join(tempDir, 'config.json'); const config = { mcp_mode: 'stdio', auth_token: 'config-token', custom_var: 'from-config' }; fs.writeFileSync(configPath, JSON.stringify(config)); // Run container with both env vars and config file const { stdout } = await exec( `docker run --name ${containerName} ` + `-e MCP_MODE=http ` + `-e AUTH_TOKEN=env-token ` + `-v "${configPath}:/app/config.json:ro" ` + `${imageName} sh -c "env | grep -E '^(MCP_MODE|AUTH_TOKEN|CUSTOM_VAR)=' | sort"` ); const envVars = stdout.trim().split('\n').reduce((acc, line) => { const [key, value] = line.split('='); acc[key] = value; return acc; }, {} as Record<string, string>); expect(envVars.MCP_MODE).toBe('http'); // From env var expect(envVars.AUTH_TOKEN).toBe('env-token'); // From env var expect(envVars.CUSTOM_VAR).toBe('from-config'); // From config file }); it('should handle missing config file gracefully', async () => { if (!dockerAvailable) return; const containerName = generateContainerName('no-config'); containers.push(containerName); // Run container without config file const { stdout, stderr } = await exec( `docker run --name ${containerName} ${imageName} echo "Container started successfully"` ); expect(stdout.trim()).toBe('Container started successfully'); expect(stderr).toBe(''); }); it('should handle invalid JSON in config file gracefully', async () => { if (!dockerAvailable) return; const containerName = generateContainerName('invalid-json'); containers.push(containerName); // Create invalid config file const configPath = path.join(tempDir, 'config.json'); fs.writeFileSync(configPath, '{ invalid json }'); // Container should still start despite invalid config const { stdout } = await exec( `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} echo "Started despite invalid config"` ); expect(stdout.trim()).toBe('Started despite invalid config'); }); }); describe('n8n-mcp serve command', () => { it('should automatically set MCP_MODE=http for "n8n-mcp serve" command', async () => { if (!dockerAvailable) return; const containerName = generateContainerName('serve-command'); containers.push(containerName); // Run container with n8n-mcp serve command // Start the container in detached mode await exec( `docker run -d --name ${containerName} -e AUTH_TOKEN=test-token -p 13001:3000 ${imageName} n8n-mcp serve` ); // Give it time to start await new Promise(resolve => setTimeout(resolve, 3000)); // Verify it's running in HTTP mode by checking the health endpoint const { stdout } = await exec( `docker exec ${containerName} curl -s http://localhost:3000/health || echo 'Server not responding'` ); // If HTTP mode is active, health endpoint should respond expect(stdout).toContain('ok'); }); it('should preserve additional arguments when using "n8n-mcp serve"', async () => { if (!dockerAvailable) return; const containerName = generateContainerName('serve-args'); containers.push(containerName); // Test that additional arguments are passed through // Note: This test is checking the command construction, not actual execution const result = await exec( `docker run --name ${containerName} ${imageName} sh -c "set -x; n8n-mcp serve --port 8080 2>&1 | grep -E 'node.*index.js.*--port.*8080' || echo 'Pattern not found'"` ); // The serve command should transform to node command with arguments preserved expect(result.stdout).toBeTruthy(); }); }); describe('Database initialization', () => { it('should initialize database when not present', async () => { if (!dockerAvailable) return; const containerName = generateContainerName('db-init'); containers.push(containerName); // Run container and check database initialization const { stdout } = await exec( `docker run --name ${containerName} ${imageName} sh -c "ls -la /app/data/nodes.db && echo 'Database initialized'"` ); expect(stdout).toContain('nodes.db'); expect(stdout).toContain('Database initialized'); }); it('should respect NODE_DB_PATH from config file', async () => { if (!dockerAvailable) return; const containerName = generateContainerName('custom-db-path'); containers.push(containerName); // Create config with custom database path const configPath = path.join(tempDir, 'config.json'); const config = { NODE_DB_PATH: '/app/data/custom/custom.db' // Use uppercase and a writable path }; fs.writeFileSync(configPath, JSON.stringify(config)); // Run container in detached mode to check environment after initialization // Set MCP_MODE=http so the server keeps running (stdio mode exits when stdin is closed in detached mode) await exec( `docker run -d --name ${containerName} -e MCP_MODE=http -e AUTH_TOKEN=test -v "${configPath}:/app/config.json:ro" ${imageName}` ); // Give it time to load config and start await new Promise(resolve => setTimeout(resolve, 2000)); // Check the actual process environment const { stdout } = await exec( `docker exec ${containerName} sh -c "cat /proc/1/environ | tr '\\0' '\\n' | grep NODE_DB_PATH || echo 'NODE_DB_PATH not found'"` ); expect(stdout.trim()).toBe('NODE_DB_PATH=/app/data/custom/custom.db'); }); }); describe('Authentication configuration', () => { it('should enforce AUTH_TOKEN requirement in HTTP mode', async () => { if (!dockerAvailable) return; const containerName = generateContainerName('auth-required'); containers.push(containerName); // Try to run in HTTP mode without auth token try { await exec( `docker run --name ${containerName} -e MCP_MODE=http ${imageName} echo "Should not reach here"` ); expect.fail('Container should have exited with error'); } catch (error: any) { expect(error.stderr).toContain('AUTH_TOKEN or AUTH_TOKEN_FILE is required for HTTP mode'); } }); it('should accept AUTH_TOKEN from config file', async () => { if (!dockerAvailable) return; const containerName = generateContainerName('auth-config'); containers.push(containerName); // Create config with auth token const configPath = path.join(tempDir, 'config.json'); const config = { mcp_mode: 'http', auth_token: 'config-auth-token' }; fs.writeFileSync(configPath, JSON.stringify(config)); // Run container with config file const { stdout } = await exec( `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} sh -c "env | grep AUTH_TOKEN"` ); expect(stdout.trim()).toBe('AUTH_TOKEN=config-auth-token'); }); }); describe('Security and permissions', () => { it('should handle malicious config values safely', async () => { if (!dockerAvailable) return; const containerName = generateContainerName('security-test'); containers.push(containerName); // Create config with potentially malicious values const configPath = path.join(tempDir, 'config.json'); const config = { malicious1: "'; echo 'hacked' > /tmp/hacked.txt; '", malicious2: "$( touch /tmp/command-injection.txt )", malicious3: "`touch /tmp/backtick-injection.txt`" }; fs.writeFileSync(configPath, JSON.stringify(config)); // Run container and check that no files were created const { stdout } = await exec( `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} sh -c "ls -la /tmp/ | grep -E '(hacked|injection)' || echo 'No malicious files created'"` ); expect(stdout.trim()).toBe('No malicious files created'); }); it('should run as non-root user by default', async () => { if (!dockerAvailable) return; const containerName = generateContainerName('non-root'); containers.push(containerName); // Check user inside container const { stdout } = await exec( `docker run --name ${containerName} ${imageName} whoami` ); expect(stdout.trim()).toBe('nodejs'); }); }); describe('Complex configuration scenarios', () => { it('should handle nested configuration with all supported types', async () => { if (!dockerAvailable) return; const containerName = generateContainerName('complex-config'); containers.push(containerName); // Create complex config const configPath = path.join(tempDir, 'config.json'); const config = { server: { http: { port: 8080, host: '0.0.0.0', ssl: { enabled: true, cert_path: '/certs/server.crt' } } }, features: { debug: false, metrics: true, logging: { level: 'info', format: 'json' } }, limits: { max_connections: 100, timeout_seconds: 30 } }; fs.writeFileSync(configPath, JSON.stringify(config)); // Run container and verify all variables const { stdout } = await exec( `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} sh -c "env | grep -E '^(SERVER_|FEATURES_|LIMITS_)' | sort"` ); const lines = stdout.trim().split('\n'); const envVars = lines.reduce((acc, line) => { const [key, value] = line.split('='); acc[key] = value; return acc; }, {} as Record<string, string>); // Verify nested values are correctly flattened expect(envVars.SERVER_HTTP_PORT).toBe('8080'); expect(envVars.SERVER_HTTP_HOST).toBe('0.0.0.0'); expect(envVars.SERVER_HTTP_SSL_ENABLED).toBe('true'); expect(envVars.SERVER_HTTP_SSL_CERT_PATH).toBe('/certs/server.crt'); expect(envVars.FEATURES_DEBUG).toBe('false'); expect(envVars.FEATURES_METRICS).toBe('true'); expect(envVars.FEATURES_LOGGING_LEVEL).toBe('info'); expect(envVars.FEATURES_LOGGING_FORMAT).toBe('json'); expect(envVars.LIMITS_MAX_CONNECTIONS).toBe('100'); expect(envVars.LIMITS_TIMEOUT_SECONDS).toBe('30'); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/88-888/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server