feat: add n8n integration with MCP Client Tool support

- Add N8N_MODE environment variable for n8n-specific behavior
- Implement HTTP Streamable transport with multiple session support
- Add protocol version endpoint (GET /mcp) for n8n compatibility
- Support multiple initialize requests for stateless n8n clients
- Add Docker configuration for n8n deployment
- Add test script with persistent volume support
- Add comprehensive unit tests for n8n mode
- Fix session management to handle per-request transport pattern

BREAKING CHANGE: Server now creates new transport for each initialize request
when running in n8n mode to support n8n's stateless client architecture

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-08-01 00:34:31 +02:00
parent a4053de998
commit a597ef5a92
23 changed files with 2395 additions and 481 deletions

View File

@@ -1,78 +0,0 @@
#!/usr/bin/env node
/**
* Debug the essentials implementation
*/
const { N8NDocumentationMCPServer } = require('../dist/mcp/server');
const { PropertyFilter } = require('../dist/services/property-filter');
const { ExampleGenerator } = require('../dist/services/example-generator');
async function debugEssentials() {
console.log('🔍 Debugging essentials implementation\n');
try {
// Initialize server
const server = new N8NDocumentationMCPServer();
await new Promise(resolve => setTimeout(resolve, 1000));
const nodeType = 'nodes-base.httpRequest';
// Step 1: Get raw node info
console.log('Step 1: Getting raw node info...');
const nodeInfo = await server.executeTool('get_node_info', { nodeType });
console.log('✅ Got node info');
console.log(' Node type:', nodeInfo.nodeType);
console.log(' Display name:', nodeInfo.displayName);
console.log(' Properties count:', nodeInfo.properties?.length);
console.log(' Properties type:', typeof nodeInfo.properties);
console.log(' First property:', nodeInfo.properties?.[0]?.name);
// Step 2: Test PropertyFilter directly
console.log('\nStep 2: Testing PropertyFilter...');
const properties = nodeInfo.properties || [];
console.log(' Input properties count:', properties.length);
const essentials = PropertyFilter.getEssentials(properties, nodeType);
console.log(' Essential results:');
console.log(' - Required:', essentials.required?.length || 0);
console.log(' - Common:', essentials.common?.length || 0);
console.log(' - Required names:', essentials.required?.map(p => p.name).join(', ') || 'none');
console.log(' - Common names:', essentials.common?.map(p => p.name).join(', ') || 'none');
// Step 3: Test ExampleGenerator
console.log('\nStep 3: Testing ExampleGenerator...');
const examples = ExampleGenerator.getExamples(nodeType, essentials);
console.log(' Example keys:', Object.keys(examples));
console.log(' Minimal example:', JSON.stringify(examples.minimal || {}, null, 2));
// Step 4: Test the full tool
console.log('\nStep 4: Testing get_node_essentials tool...');
const essentialsResult = await server.executeTool('get_node_essentials', { nodeType });
console.log('✅ Tool executed');
console.log(' Result keys:', Object.keys(essentialsResult));
console.log(' Node type from result:', essentialsResult.nodeType);
console.log(' Required props:', essentialsResult.requiredProperties?.length || 0);
console.log(' Common props:', essentialsResult.commonProperties?.length || 0);
// Compare property counts
console.log('\n📊 Summary:');
console.log(' Full properties:', nodeInfo.properties?.length || 0);
console.log(' Essential properties:',
(essentialsResult.requiredProperties?.length || 0) +
(essentialsResult.commonProperties?.length || 0)
);
console.log(' Reduction:',
Math.round((1 - ((essentialsResult.requiredProperties?.length || 0) +
(essentialsResult.commonProperties?.length || 0)) /
(nodeInfo.properties?.length || 1)) * 100) + '%'
);
} catch (error) {
console.error('\n❌ Error:', error);
console.error('Stack:', error.stack);
}
process.exit(0);
}
debugEssentials().catch(console.error);

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env node
import { N8NDocumentationMCPServer } from '../src/mcp/server';
async function debugFuzzy() {
const server = new N8NDocumentationMCPServer();
await new Promise(resolve => setTimeout(resolve, 1000));
// Get the actual implementation
const serverAny = server as any;
// Test nodes we expect to find
const testNodes = [
{ node_type: 'nodes-base.slack', display_name: 'Slack', description: 'Consume Slack API' },
{ node_type: 'nodes-base.webhook', display_name: 'Webhook', description: 'Handle webhooks' },
{ node_type: 'nodes-base.httpRequest', display_name: 'HTTP Request', description: 'Make HTTP requests' },
{ node_type: 'nodes-base.emailSend', display_name: 'Send Email', description: 'Send emails' }
];
const testQueries = ['slak', 'webook', 'htpp', 'emial'];
console.log('Testing fuzzy scoring...\n');
for (const query of testQueries) {
console.log(`\nQuery: "${query}"`);
console.log('-'.repeat(40));
for (const node of testNodes) {
const score = serverAny.calculateFuzzyScore(node, query);
const distance = serverAny.getEditDistance(query, node.display_name.toLowerCase());
console.log(`${node.display_name.padEnd(15)} - Score: ${score.toFixed(0).padStart(4)}, Distance: ${distance}`);
}
// Test actual search
console.log('\nActual search result:');
const result = await server.executeTool('search_nodes', {
query: query,
mode: 'FUZZY',
limit: 5
});
console.log(`Found ${result.results.length} results`);
if (result.results.length > 0) {
console.log('Top result:', result.results[0].displayName);
}
}
}
debugFuzzy().catch(console.error);

View File

@@ -1,56 +0,0 @@
#!/usr/bin/env node
/**
* Debug script to check node data structure
*/
const { N8NDocumentationMCPServer } = require('../dist/mcp/server');
async function debugNode() {
console.log('🔍 Debugging node data\n');
try {
// Initialize server
const server = new N8NDocumentationMCPServer();
await new Promise(resolve => setTimeout(resolve, 1000));
// Get node info directly
const nodeType = 'nodes-base.httpRequest';
console.log(`Checking node: ${nodeType}\n`);
try {
const nodeInfo = await server.executeTool('get_node_info', { nodeType });
console.log('Node info retrieved successfully');
console.log('Node type:', nodeInfo.nodeType);
console.log('Has properties:', !!nodeInfo.properties);
console.log('Properties count:', nodeInfo.properties?.length || 0);
console.log('Has operations:', !!nodeInfo.operations);
console.log('Operations:', nodeInfo.operations);
console.log('Operations type:', typeof nodeInfo.operations);
console.log('Operations length:', nodeInfo.operations?.length);
// Check raw data
console.log('\n📊 Raw data check:');
console.log('properties_schema type:', typeof nodeInfo.properties_schema);
console.log('operations type:', typeof nodeInfo.operations);
// Check if operations is a string that needs parsing
if (typeof nodeInfo.operations === 'string') {
console.log('\nOperations is a string, trying to parse:');
console.log('Operations string:', nodeInfo.operations);
console.log('Operations length:', nodeInfo.operations.length);
console.log('First 100 chars:', nodeInfo.operations.substring(0, 100));
}
} catch (error) {
console.error('Error getting node info:', error);
}
} catch (error) {
console.error('Fatal error:', error);
}
process.exit(0);
}
debugNode().catch(console.error);

View File

@@ -1,114 +0,0 @@
#!/usr/bin/env npx tsx
/**
* Debug template search issues
*/
import { createDatabaseAdapter } from '../src/database/database-adapter';
import { TemplateRepository } from '../src/templates/template-repository';
async function debug() {
console.log('🔍 Debugging template search...\n');
const db = await createDatabaseAdapter('./data/nodes.db');
// Check FTS5 support
const hasFTS5 = db.checkFTS5Support();
console.log(`FTS5 support: ${hasFTS5}`);
// Check template count
const templateCount = db.prepare('SELECT COUNT(*) as count FROM templates').get() as { count: number };
console.log(`Total templates: ${templateCount.count}`);
// Check FTS5 tables
const ftsTables = db.prepare(`
SELECT name FROM sqlite_master
WHERE type IN ('table', 'virtual') AND name LIKE 'templates_fts%'
ORDER BY name
`).all() as { name: string }[];
console.log('\nFTS5 tables:');
ftsTables.forEach(t => console.log(` - ${t.name}`));
// Check FTS5 content
if (hasFTS5) {
try {
const ftsCount = db.prepare('SELECT COUNT(*) as count FROM templates_fts').get() as { count: number };
console.log(`\nFTS5 entries: ${ftsCount.count}`);
} catch (error) {
console.log('\nFTS5 query error:', error);
}
}
// Test template repository
console.log('\n📋 Testing TemplateRepository...');
const repo = new TemplateRepository(db);
// Test different searches
const searches = ['webhook', 'api', 'automation'];
for (const query of searches) {
console.log(`\n🔎 Searching for "${query}"...`);
// Direct SQL LIKE search
const likeResults = db.prepare(`
SELECT COUNT(*) as count FROM templates
WHERE name LIKE ? OR description LIKE ?
`).get(`%${query}%`, `%${query}%`) as { count: number };
console.log(` LIKE search matches: ${likeResults.count}`);
// Repository search
try {
const repoResults = repo.searchTemplates(query, 5);
console.log(` Repository search returned: ${repoResults.length} results`);
if (repoResults.length > 0) {
console.log(` First result: ${repoResults[0].name}`);
}
} catch (error) {
console.log(` Repository search error:`, error);
}
// Direct FTS5 search if available
if (hasFTS5) {
try {
const ftsQuery = `"${query}"`;
const ftsResults = db.prepare(`
SELECT COUNT(*) as count
FROM templates t
JOIN templates_fts ON t.id = templates_fts.rowid
WHERE templates_fts MATCH ?
`).get(ftsQuery) as { count: number };
console.log(` Direct FTS5 matches: ${ftsResults.count}`);
} catch (error) {
console.log(` Direct FTS5 error:`, error);
}
}
}
// Check if templates_fts is properly synced
if (hasFTS5) {
console.log('\n🔄 Checking FTS5 sync...');
try {
// Get a few template IDs and check if they're in FTS
const templates = db.prepare('SELECT id, name FROM templates LIMIT 5').all() as { id: number, name: string }[];
for (const template of templates) {
try {
const inFTS = db.prepare('SELECT rowid FROM templates_fts WHERE rowid = ?').get(template.id);
console.log(` Template ${template.id} "${template.name.substring(0, 30)}...": ${inFTS ? 'IN FTS' : 'NOT IN FTS'}`);
} catch (error) {
console.log(` Error checking template ${template.id}:`, error);
}
}
} catch (error) {
console.log(' FTS sync check error:', error);
}
}
db.close();
}
// Run if called directly
if (require.main === module) {
debug().catch(console.error);
}
export { debug };

View File

@@ -1,113 +0,0 @@
#!/usr/bin/env npx tsx
/**
* Test MCP search behavior
*/
import { createDatabaseAdapter } from '../src/database/database-adapter';
import { TemplateService } from '../src/templates/template-service';
import { TemplateRepository } from '../src/templates/template-repository';
async function testMCPSearch() {
console.log('🔍 Testing MCP search behavior...\n');
// Set MCP_MODE to simulate Docker environment
process.env.MCP_MODE = 'stdio';
console.log('Environment: MCP_MODE =', process.env.MCP_MODE);
const db = await createDatabaseAdapter('./data/nodes.db');
// Test 1: Direct repository search
console.log('\n1⃣ Testing TemplateRepository directly:');
const repo = new TemplateRepository(db);
try {
const repoResults = repo.searchTemplates('webhook', 5);
console.log(` Repository search returned: ${repoResults.length} results`);
if (repoResults.length > 0) {
console.log(` First result: ${repoResults[0].name}`);
}
} catch (error) {
console.log(' Repository search error:', error);
}
// Test 2: Service layer search (what MCP uses)
console.log('\n2⃣ Testing TemplateService (MCP layer):');
const service = new TemplateService(db);
try {
const serviceResults = await service.searchTemplates('webhook', 5);
console.log(` Service search returned: ${serviceResults.length} results`);
if (serviceResults.length > 0) {
console.log(` First result: ${serviceResults[0].name}`);
}
} catch (error) {
console.log(' Service search error:', error);
}
// Test 3: Test with empty query
console.log('\n3⃣ Testing with empty query:');
try {
const emptyResults = await service.searchTemplates('', 5);
console.log(` Empty query returned: ${emptyResults.length} results`);
} catch (error) {
console.log(' Empty query error:', error);
}
// Test 4: Test getTemplatesForTask (which works)
console.log('\n4⃣ Testing getTemplatesForTask (control):');
try {
const taskResults = await service.getTemplatesForTask('webhook_processing');
console.log(` Task search returned: ${taskResults.length} results`);
if (taskResults.length > 0) {
console.log(` First result: ${taskResults[0].name}`);
}
} catch (error) {
console.log(' Task search error:', error);
}
// Test 5: Direct SQL queries
console.log('\n5⃣ Testing direct SQL queries:');
try {
// Count templates
const count = db.prepare('SELECT COUNT(*) as count FROM templates').get() as { count: number };
console.log(` Total templates: ${count.count}`);
// Test LIKE search
const likeResults = db.prepare(`
SELECT COUNT(*) as count FROM templates
WHERE name LIKE '%webhook%' OR description LIKE '%webhook%'
`).get() as { count: number };
console.log(` LIKE search for 'webhook': ${likeResults.count} results`);
// Check if FTS5 table exists
const ftsExists = db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name='templates_fts'
`).get() as { name: string } | undefined;
console.log(` FTS5 table exists: ${ftsExists ? 'Yes' : 'No'}`);
if (ftsExists) {
// Test FTS5 search
try {
const ftsResults = db.prepare(`
SELECT COUNT(*) as count FROM templates t
JOIN templates_fts ON t.id = templates_fts.rowid
WHERE templates_fts MATCH 'webhook'
`).get() as { count: number };
console.log(` FTS5 search for 'webhook': ${ftsResults.count} results`);
} catch (ftsError) {
console.log(` FTS5 search error:`, ftsError);
}
}
} catch (error) {
console.log(' Direct SQL error:', error);
}
db.close();
}
// Run if called directly
if (require.main === module) {
testMCPSearch().catch(console.error);
}
export { testMCPSearch };

154
scripts/test-n8n-integration.sh Executable file
View File

@@ -0,0 +1,154 @@
#!/bin/bash
# Script to test n8n integration with n8n-mcp server
set -e
echo "🚀 Starting n8n integration test environment..."
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Configuration
N8N_PORT=5678
MCP_PORT=3001
AUTH_TOKEN="test-token-for-n8n-testing-minimum-32-chars"
# n8n data directory for persistence
N8N_DATA_DIR="$HOME/.n8n-mcp-test"
# Function to cleanup on exit
cleanup() {
echo -e "\n${YELLOW}🧹 Cleaning up...${NC}"
# Stop n8n container
if docker ps -q -f name=n8n-test > /dev/null 2>&1; then
echo "Stopping n8n container..."
docker stop n8n-test >/dev/null 2>&1 || true
docker rm n8n-test >/dev/null 2>&1 || true
fi
# Kill MCP server if running
if [ -n "$MCP_PID" ] && kill -0 $MCP_PID 2>/dev/null; then
echo "Stopping MCP server..."
kill $MCP_PID 2>/dev/null || true
fi
echo -e "${GREEN}✅ Cleanup complete${NC}"
}
# Set trap to cleanup on exit
trap cleanup EXIT INT TERM
# Check if we're in the right directory
if [ ! -f "package.json" ] || [ ! -d "dist" ]; then
echo -e "${RED}❌ Error: Must run from n8n-mcp directory${NC}"
echo "Please cd to /Users/romualdczlonkowski/Pliki/n8n-mcp/n8n-mcp"
exit 1
fi
# Always build the project to ensure latest changes
echo -e "${YELLOW}📦 Building project...${NC}"
npm run build
# Create n8n data directory if it doesn't exist
if [ ! -d "$N8N_DATA_DIR" ]; then
echo -e "${YELLOW}📁 Creating n8n data directory: $N8N_DATA_DIR${NC}"
mkdir -p "$N8N_DATA_DIR"
fi
# Start n8n in Docker with persistent volume
echo -e "\n${GREEN}🐳 Starting n8n container with persistent data...${NC}"
docker run -d \
--name n8n-test \
-p ${N8N_PORT}:5678 \
-v "${N8N_DATA_DIR}:/home/node/.n8n" \
-e N8N_BASIC_AUTH_ACTIVE=false \
-e N8N_HOST=localhost \
-e N8N_PORT=5678 \
-e N8N_PROTOCOL=http \
-e NODE_ENV=development \
-e N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true \
n8nio/n8n:latest
# Wait for n8n to be ready
echo -e "${YELLOW}⏳ Waiting for n8n to start...${NC}"
for i in {1..30}; do
if curl -s http://localhost:${N8N_PORT}/ >/dev/null 2>&1; then
echo -e "${GREEN}✅ n8n is ready!${NC}"
break
fi
if [ $i -eq 30 ]; then
echo -e "${RED}❌ n8n failed to start${NC}"
exit 1
fi
sleep 1
done
# Start MCP server
echo -e "\n${GREEN}🚀 Starting MCP server in n8n mode...${NC}"
N8N_MODE=true \
MCP_MODE=http \
AUTH_TOKEN="${AUTH_TOKEN}" \
PORT=${MCP_PORT} \
node dist/mcp/index.js > /tmp/mcp-server.log 2>&1 &
MCP_PID=$!
# Show log file location
echo -e "${YELLOW}📄 MCP server logs: /tmp/mcp-server.log${NC}"
# Wait for MCP server to be ready
echo -e "${YELLOW}⏳ Waiting for MCP server to start...${NC}"
for i in {1..10}; do
if curl -s http://localhost:${MCP_PORT}/health >/dev/null 2>&1; then
echo -e "${GREEN}✅ MCP server is ready!${NC}"
break
fi
if [ $i -eq 10 ]; then
echo -e "${RED}❌ MCP server failed to start${NC}"
exit 1
fi
sleep 1
done
# Show status and test endpoints
echo -e "\n${GREEN}🎉 Both services are running!${NC}"
echo -e "\n📍 Service URLs:"
echo -e " • n8n: http://localhost:${N8N_PORT}"
echo -e " • MCP server: http://localhost:${MCP_PORT}"
echo -e "\n🔑 Auth token: ${AUTH_TOKEN}"
echo -e "\n💾 n8n data stored in: ${N8N_DATA_DIR}"
echo -e " (Your workflows, credentials, and settings are preserved between runs)"
# Test MCP protocol endpoint
echo -e "\n${YELLOW}🧪 Testing MCP protocol endpoint...${NC}"
echo "Response from GET /mcp:"
curl -s http://localhost:${MCP_PORT}/mcp | jq '.' || curl -s http://localhost:${MCP_PORT}/mcp
# Test MCP initialization
echo -e "\n${YELLOW}🧪 Testing MCP initialization...${NC}"
echo "Response from POST /mcp (initialize):"
curl -s -X POST http://localhost:${MCP_PORT}/mcp \
-H "Authorization: Bearer ${AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{}},"id":1}' \
| jq '.' || echo "(Install jq for pretty JSON output)"
echo -e "\n${GREEN}✅ Setup complete!${NC}"
echo -e "\n📝 Next steps:"
echo -e " 1. Open n8n at http://localhost:${N8N_PORT}"
echo -e " 2. Create a workflow with the AI Agent node"
echo -e " 3. Add MCP Client Tool node"
echo -e " 4. Configure it with:"
echo -e " • Transport: HTTP"
echo -e " • URL: http://host.docker.internal:${MCP_PORT}/mcp"
echo -e " • Auth: Bearer ${AUTH_TOKEN}"
echo -e "\n${YELLOW}Press Ctrl+C to stop both services${NC}"
echo -e "\n${YELLOW}📋 To monitor MCP logs: tail -f /tmp/mcp-server.log${NC}"
echo -e "${YELLOW}📋 To monitor n8n logs: docker logs -f n8n-test${NC}"
# Wait for interrupt
wait $MCP_PID