chore: update n8n to v1.101.1

- Updated n8n from 1.100.1 to 1.101.1
- Updated n8n-core from 1.99.0 to 1.100.0
- Updated n8n-workflow from 1.97.0 to 1.98.0
- Updated @n8n/n8n-nodes-langchain from 1.99.0 to 1.100.1
- Rebuilt node database with 528 nodes
- All validation tests passing
- Bumped version to 2.7.12

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-07-11 00:48:43 +02:00
parent 53d8c8452f
commit f525303748
16 changed files with 5231 additions and 1009 deletions

View File

@@ -2,9 +2,9 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub stars](https://img.shields.io/github/stars/czlonkowski/n8n-mcp?style=social)](https://github.com/czlonkowski/n8n-mcp)
[![Version](https://img.shields.io/badge/version-2.7.11-blue.svg)](https://github.com/czlonkowski/n8n-mcp)
[![Version](https://img.shields.io/badge/version-2.7.12-blue.svg)](https://github.com/czlonkowski/n8n-mcp)
[![npm version](https://img.shields.io/npm/v/n8n-mcp.svg)](https://www.npmjs.com/package/n8n-mcp)
[![n8n version](https://img.shields.io/badge/n8n-v1.100.1-orange.svg)](https://github.com/n8n-io/n8n)
[![n8n version](https://img.shields.io/badge/n8n-v1.101.1-orange.svg)](https://github.com/n8n-io/n8n)
[![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fczlonkowski%2Fn8n--mcp-green.svg)](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp)
A Model Context Protocol (MCP) server that provides AI assistants with comprehensive access to n8n node documentation, properties, and operations. Deploy in minutes to give Claude and other AI assistants deep knowledge about n8n's 525+ workflow automation nodes.
@@ -13,7 +13,7 @@ A Model Context Protocol (MCP) server that provides AI assistants with comprehen
n8n-MCP serves as a bridge between n8n's workflow automation platform and AI models, enabling them to understand and work with n8n nodes effectively. It provides structured access to:
- 📚 **525 n8n nodes** from both n8n-nodes-base and @n8n/n8n-nodes-langchain
- 📚 **528 n8n nodes** from both n8n-nodes-base and @n8n/n8n-nodes-langchain
- 🔧 **Node properties** - 99% coverage with detailed schemas
-**Node operations** - 63.6% coverage of available actions
- 📄 **Documentation** - 90% coverage from official n8n docs (including AI nodes)
@@ -568,10 +568,10 @@ npm run dev:http # HTTP dev mode
## 📊 Metrics & Coverage
Current database coverage (n8n v1.100.1):
Current database coverage (n8n v1.101.1):
-**525/525** nodes loaded (100%)
-**520** nodes with properties (99%)
-**528/528** nodes loaded (100%)
-**520** nodes with properties (98.5%)
-**470** nodes with documentation (90%)
-**263** AI-capable tools detected
-**AI Agent & LangChain nodes** fully documented

Binary file not shown.

View File

@@ -5,6 +5,17 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.7.12] - 2025-07-10
### Updated
- **n8n Dependencies**: Updated to latest versions for compatibility and new features
- n8n: 1.100.1 → 1.101.1
- n8n-core: 1.99.0 → 1.100.0
- n8n-workflow: 1.97.0 → 1.98.0
- @n8n/n8n-nodes-langchain: 1.99.0 → 1.100.1
- **Node Database**: Rebuilt with 528 nodes from updated n8n packages
- All validation tests passing with updated dependencies
## [2.7.11] - 2025-07-10
### Enhanced
@@ -554,6 +565,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Basic n8n and MCP integration
- Core workflow automation features
[2.7.12]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.11...v2.7.12
[2.7.11]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.10...v2.7.11
[2.7.10]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.8...v2.7.10
[2.7.8]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.5...v2.7.8

BIN
nodes.db Normal file

Binary file not shown.

5341
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp",
"version": "2.7.11",
"version": "2.7.12",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js",
"bin": {
@@ -93,14 +93,14 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.13.2",
"@n8n/n8n-nodes-langchain": "^1.99.0",
"@n8n/n8n-nodes-langchain": "^1.100.1",
"axios": "^1.10.0",
"better-sqlite3": "^11.10.0",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"n8n": "^1.100.1",
"n8n-core": "^1.99.0",
"n8n-workflow": "^1.97.0",
"n8n": "^1.101.1",
"n8n-core": "^1.100.0",
"n8n-workflow": "^1.98.0",
"sql.js": "^1.13.0",
"uuid": "^10.0.0"
}

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp-runtime",
"version": "2.7.10",
"version": "2.7.12",
"description": "n8n MCP Server Runtime Dependencies Only",
"private": true,
"dependencies": {

48
scripts/debug-fuzzy.ts Normal file
View File

@@ -0,0 +1,48 @@
#!/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

@@ -0,0 +1,114 @@
#!/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

@@ -0,0 +1,130 @@
#!/usr/bin/env node
import * as path from 'path';
import { createDatabaseAdapter } from '../src/database/database-adapter';
import { logger } from '../src/utils/logger';
/**
* Migrate existing database to add FTS5 support for nodes
*/
async function migrateNodesFTS() {
logger.info('Starting nodes FTS5 migration...');
const dbPath = path.join(process.cwd(), 'data', 'nodes.db');
const db = await createDatabaseAdapter(dbPath);
try {
// Check if nodes_fts already exists
const tableExists = db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name='nodes_fts'
`).get();
if (tableExists) {
logger.info('nodes_fts table already exists, skipping migration');
return;
}
logger.info('Creating nodes_fts virtual table...');
// Create the FTS5 virtual table
db.prepare(`
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
node_type,
display_name,
description,
documentation,
operations,
content=nodes,
content_rowid=rowid,
tokenize='porter'
)
`).run();
// Populate the FTS table with existing data
logger.info('Populating nodes_fts with existing data...');
const nodes = db.prepare('SELECT rowid, * FROM nodes').all() as any[];
logger.info(`Migrating ${nodes.length} nodes to FTS index...`);
const insertStmt = db.prepare(`
INSERT INTO nodes_fts(rowid, node_type, display_name, description, documentation, operations)
VALUES (?, ?, ?, ?, ?, ?)
`);
for (const node of nodes) {
insertStmt.run(
node.rowid,
node.node_type,
node.display_name,
node.description || '',
node.documentation || '',
node.operations || ''
);
}
// Create triggers to keep FTS in sync
logger.info('Creating synchronization triggers...');
db.prepare(`
CREATE TRIGGER IF NOT EXISTS nodes_fts_insert AFTER INSERT ON nodes
BEGIN
INSERT INTO nodes_fts(rowid, node_type, display_name, description, documentation, operations)
VALUES (new.rowid, new.node_type, new.display_name, new.description, new.documentation, new.operations);
END
`).run();
db.prepare(`
CREATE TRIGGER IF NOT EXISTS nodes_fts_update AFTER UPDATE ON nodes
BEGIN
UPDATE nodes_fts
SET node_type = new.node_type,
display_name = new.display_name,
description = new.description,
documentation = new.documentation,
operations = new.operations
WHERE rowid = new.rowid;
END
`).run();
db.prepare(`
CREATE TRIGGER IF NOT EXISTS nodes_fts_delete AFTER DELETE ON nodes
BEGIN
DELETE FROM nodes_fts WHERE rowid = old.rowid;
END
`).run();
// Test the FTS search
logger.info('Testing FTS search...');
const testResults = db.prepare(`
SELECT n.* FROM nodes n
JOIN nodes_fts ON n.rowid = nodes_fts.rowid
WHERE nodes_fts MATCH 'webhook'
ORDER BY rank
LIMIT 5
`).all();
logger.info(`FTS test search found ${testResults.length} results for 'webhook'`);
// Persist if using sql.js
if ('persist' in db) {
logger.info('Persisting database changes...');
(db as any).persist();
}
logger.info('✅ FTS5 migration completed successfully!');
} catch (error) {
logger.error('Migration failed:', error);
throw error;
} finally {
db.close();
}
}
// Run migration
migrateNodesFTS().catch(error => {
logger.error('Migration error:', error);
process.exit(1);
});

162
scripts/test-fts5-search.ts Normal file
View File

@@ -0,0 +1,162 @@
#!/usr/bin/env node
import { N8NDocumentationMCPServer } from '../src/mcp/server';
interface SearchTest {
query: string;
mode?: 'OR' | 'AND' | 'FUZZY';
description: string;
expectedTop?: string[];
}
async function testFTS5Search() {
console.log('Testing FTS5 Search Implementation\n');
console.log('='.repeat(50));
const server = new N8NDocumentationMCPServer();
// Wait for initialization
await new Promise(resolve => setTimeout(resolve, 1000));
const tests: SearchTest[] = [
{
query: 'webhook',
description: 'Basic search - should return Webhook node first',
expectedTop: ['nodes-base.webhook']
},
{
query: 'http call',
description: 'Multi-word OR search - should return HTTP Request node first',
expectedTop: ['nodes-base.httpRequest']
},
{
query: 'send message',
mode: 'AND',
description: 'AND mode - only nodes with both "send" AND "message"',
},
{
query: 'slak',
mode: 'FUZZY',
description: 'FUZZY mode - should find Slack despite typo',
expectedTop: ['nodes-base.slack']
},
{
query: '"email trigger"',
description: 'Exact phrase search with quotes',
},
{
query: 'http',
mode: 'FUZZY',
description: 'FUZZY mode with common term',
expectedTop: ['nodes-base.httpRequest']
},
{
query: 'google sheets',
mode: 'AND',
description: 'AND mode - find Google Sheets node',
expectedTop: ['nodes-base.googleSheets']
},
{
query: 'webhook trigger',
mode: 'OR',
description: 'OR mode - should return nodes with either word',
}
];
let passedTests = 0;
let failedTests = 0;
for (const test of tests) {
console.log(`\n${test.description}`);
console.log(`Query: "${test.query}" (Mode: ${test.mode || 'OR'})`);
console.log('-'.repeat(40));
try {
const results = await server.executeTool('search_nodes', {
query: test.query,
mode: test.mode,
limit: 5
});
if (!results.results || results.results.length === 0) {
console.log('❌ No results found');
if (test.expectedTop) {
failedTests++;
}
continue;
}
console.log(`Found ${results.results.length} results:`);
results.results.forEach((node: any, index: number) => {
const marker = test.expectedTop && index === 0 && test.expectedTop.includes(node.nodeType) ? ' ✅' : '';
console.log(` ${index + 1}. ${node.nodeType} - ${node.displayName}${marker}`);
});
// Verify search mode is returned
if (results.mode) {
console.log(`\nSearch mode used: ${results.mode}`);
}
// Check expected results
if (test.expectedTop) {
const firstResult = results.results[0];
if (test.expectedTop.includes(firstResult.nodeType)) {
console.log('✅ Test passed: Expected node found at top');
passedTests++;
} else {
console.log('❌ Test failed: Expected node not at top');
console.log(` Expected: ${test.expectedTop.join(' or ')}`);
console.log(` Got: ${firstResult.nodeType}`);
failedTests++;
}
} else {
// Test without specific expectations
console.log('✅ Search completed successfully');
passedTests++;
}
} catch (error) {
console.log(`❌ Error: ${error}`);
failedTests++;
}
}
console.log('\n' + '='.repeat(50));
console.log('FTS5 Feature Tests');
console.log('='.repeat(50));
// Test FTS5-specific features
console.log('\n1. Testing relevance ranking...');
const webhookResult = await server.executeTool('search_nodes', {
query: 'webhook',
limit: 10
});
console.log(` Primary "Webhook" node position: #${webhookResult.results.findIndex((r: any) => r.nodeType === 'nodes-base.webhook') + 1}`);
console.log('\n2. Testing fuzzy matching with various typos...');
const typoTests = ['webook', 'htpp', 'slck', 'googl sheet'];
for (const typo of typoTests) {
const result = await server.executeTool('search_nodes', {
query: typo,
mode: 'FUZZY',
limit: 1
});
if (result.results.length > 0) {
console.log(` "${typo}" → ${result.results[0].displayName}`);
} else {
console.log(` "${typo}" → No results ❌`);
}
}
console.log('\n' + '='.repeat(50));
console.log(`Test Summary: ${passedTests} passed, ${failedTests} failed`);
console.log('='.repeat(50));
process.exit(failedTests > 0 ? 1 : 0);
}
// Run tests
testFTS5Search().catch(error => {
console.error('Test execution failed:', error);
process.exit(1);
});

76
scripts/test-fuzzy-fix.ts Normal file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env node
import { N8NDocumentationMCPServer } from '../src/mcp/server';
async function testFuzzyFix() {
console.log('Testing FUZZY mode fix...\n');
const server = new N8NDocumentationMCPServer();
// Wait for initialization
await new Promise(resolve => setTimeout(resolve, 1000));
// Test 1: FUZZY mode with typo
console.log('Test 1: FUZZY mode with "slak" (typo for "slack")');
const fuzzyResult = await server.executeTool('search_nodes', {
query: 'slak',
mode: 'FUZZY',
limit: 5
});
console.log(`Results: ${fuzzyResult.results.length} found`);
if (fuzzyResult.results.length > 0) {
console.log('✅ FUZZY mode now finds results!');
fuzzyResult.results.forEach((node: any, i: number) => {
console.log(` ${i + 1}. ${node.nodeType} - ${node.displayName}`);
});
} else {
console.log('❌ FUZZY mode still not working');
}
// Test 2: AND mode with explanation
console.log('\n\nTest 2: AND mode with "send message"');
const andResult = await server.executeTool('search_nodes', {
query: 'send message',
mode: 'AND',
limit: 5
});
console.log(`Results: ${andResult.results.length} found`);
if (andResult.searchInfo) {
console.log('✅ AND mode now includes search info:');
console.log(` ${andResult.searchInfo.message}`);
console.log(` Tip: ${andResult.searchInfo.tip}`);
}
console.log('\nFirst 5 results:');
andResult.results.slice(0, 5).forEach((node: any, i: number) => {
console.log(` ${i + 1}. ${node.nodeType} - ${node.displayName}`);
});
// Test 3: More typos
console.log('\n\nTest 3: More FUZZY tests');
const typos = ['htpp', 'webook', 'slck', 'emial'];
for (const typo of typos) {
const result = await server.executeTool('search_nodes', {
query: typo,
mode: 'FUZZY',
limit: 1
});
if (result.results.length > 0) {
console.log(`✅ "${typo}" → ${result.results[0].displayName}`);
} else {
console.log(`❌ "${typo}" → No results`);
}
}
process.exit(0);
}
// Run tests
testFuzzyFix().catch(error => {
console.error('Test failed:', error);
process.exit(1);
});

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env node
import { N8NDocumentationMCPServer } from '../src/mcp/server';
async function testSimple() {
const server = new N8NDocumentationMCPServer();
await new Promise(resolve => setTimeout(resolve, 1000));
// Just test one query
const result = await server.executeTool('search_nodes', {
query: 'slak',
mode: 'FUZZY',
limit: 5
});
console.log('Query: "slak" (FUZZY mode)');
console.log(`Results: ${result.results.length}`);
if (result.results.length === 0) {
// Let's check with a lower threshold
const serverAny = server as any;
const slackNode = {
node_type: 'nodes-base.slack',
display_name: 'Slack',
description: 'Consume Slack API'
};
const score = serverAny.calculateFuzzyScore(slackNode, 'slak');
console.log(`\nSlack node score for "slak": ${score}`);
console.log('Current threshold: 400');
console.log('Should it match?', score >= 400 ? 'YES' : 'NO');
} else {
result.results.forEach((r: any, i: number) => {
console.log(`${i + 1}. ${r.displayName}`);
});
}
}
testSimple().catch(console.error);

View File

@@ -0,0 +1,46 @@
#\!/usr/bin/env node
import { N8NDocumentationMCPServer } from '../src/mcp/server';
async function testHttpSearch() {
const server = new N8NDocumentationMCPServer();
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Testing search for "http"...\n');
const result = await server.executeTool('search_nodes', {
query: 'http',
limit: 50 // Get more results to see where HTTP Request is
});
console.log(`Total results: ${result.results.length}\n`);
// Find HTTP Request node in results
const httpRequestIndex = result.results.findIndex((r: any) =>
r.nodeType === 'nodes-base.httpRequest'
);
if (httpRequestIndex === -1) {
console.log('❌ HTTP Request node NOT FOUND in results\!');
} else {
console.log(`✅ HTTP Request found at position ${httpRequestIndex + 1}`);
}
console.log('\nTop 10 results:');
result.results.slice(0, 10).forEach((r: any, i: number) => {
console.log(`${i + 1}. ${r.nodeType} - ${r.displayName}`);
});
// Also check LIKE search directly
console.log('\n\nTesting LIKE search fallback:');
const serverAny = server as any;
const likeResult = await serverAny.searchNodesLIKE('http', 20);
console.log(`LIKE search found ${likeResult.results.length} results`);
console.log('Top 5 LIKE results:');
likeResult.results.slice(0, 5).forEach((r: any, i: number) => {
console.log(`${i + 1}. ${r.nodeType} - ${r.displayName}`);
});
}
testHttpSearch().catch(console.error);

113
scripts/test-mcp-search.ts Normal file
View File

@@ -0,0 +1,113 @@
#!/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 };

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env node
import { N8NDocumentationMCPServer } from '../src/mcp/server';
interface SearchTestCase {
query: string;
expectedTop: string[];
description: string;
}
async function testSearchImprovements() {
console.log('Testing search improvements...\n');
const server = new N8NDocumentationMCPServer();
// Wait for initialization
await new Promise(resolve => setTimeout(resolve, 1000));
const testCases: SearchTestCase[] = [
{
query: 'webhook',
expectedTop: ['nodes-base.webhook'],
description: 'Primary webhook node should appear first'
},
{
query: 'http',
expectedTop: ['nodes-base.httpRequest'],
description: 'HTTP Request node should appear first'
},
{
query: 'http call',
expectedTop: ['nodes-base.httpRequest'],
description: 'HTTP Request node should appear first for "http call"'
},
{
query: 'slack',
expectedTop: ['nodes-base.slack'],
description: 'Slack node should appear first'
},
{
query: 'email',
expectedTop: ['nodes-base.emailSend', 'nodes-base.gmail', 'nodes-base.emailReadImap'],
description: 'Email-related nodes should appear first'
},
{
query: 'http request',
expectedTop: ['nodes-base.httpRequest'],
description: 'HTTP Request node should appear first for exact name'
}
];
let passedTests = 0;
let failedTests = 0;
for (const testCase of testCases) {
try {
console.log(`\nTest: ${testCase.description}`);
console.log(`Query: "${testCase.query}"`);
const results = await server.executeTool('search_nodes', {
query: testCase.query,
limit: 10
});
if (!results.results || results.results.length === 0) {
console.log('❌ No results found');
failedTests++;
continue;
}
console.log(`Found ${results.results.length} results`);
console.log('Top 5 results:');
const top5 = results.results.slice(0, 5);
top5.forEach((node: any, index: number) => {
const isExpected = testCase.expectedTop.includes(node.nodeType);
const marker = index === 0 && isExpected ? '✅' : index === 0 && !isExpected ? '❌' : '';
console.log(` ${index + 1}. ${node.nodeType} - ${node.displayName} ${marker}`);
});
// Check if any expected node appears in top position
const firstResult = results.results[0];
if (testCase.expectedTop.includes(firstResult.nodeType)) {
console.log('✅ Test passed: Expected node found at top position');
passedTests++;
} else {
console.log('❌ Test failed: Expected nodes not at top position');
console.log(` Expected one of: ${testCase.expectedTop.join(', ')}`);
console.log(` Got: ${firstResult.nodeType}`);
failedTests++;
}
} catch (error) {
console.log(`❌ Test failed with error: ${error}`);
failedTests++;
}
}
console.log('\n' + '='.repeat(50));
console.log(`Test Summary: ${passedTests} passed, ${failedTests} failed`);
console.log('='.repeat(50));
// Test the old problematic queries to ensure improvement
console.log('\n\nTesting Original Problem Scenarios:');
console.log('=====================================\n');
// Test webhook query that was problematic
console.log('1. Testing "webhook" query (was returning service-specific webhooks first):');
const webhookResult = await server.executeTool('search_nodes', { query: 'webhook', limit: 10 });
const webhookFirst = webhookResult.results[0];
if (webhookFirst.nodeType === 'nodes-base.webhook') {
console.log(' ✅ SUCCESS: Primary Webhook node now appears first!');
} else {
console.log(` ❌ FAILED: Got ${webhookFirst.nodeType} instead of nodes-base.webhook`);
console.log(` First 3 results: ${webhookResult.results.slice(0, 3).map((r: any) => r.nodeType).join(', ')}`);
}
// Test http call query
console.log('\n2. Testing "http call" query (was not finding HTTP Request easily):');
const httpCallResult = await server.executeTool('search_nodes', { query: 'http call', limit: 10 });
const httpCallFirst = httpCallResult.results[0];
if (httpCallFirst.nodeType === 'nodes-base.httpRequest') {
console.log(' ✅ SUCCESS: HTTP Request node now appears first!');
} else {
console.log(` ❌ FAILED: Got ${httpCallFirst.nodeType} instead of nodes-base.httpRequest`);
console.log(` First 3 results: ${httpCallResult.results.slice(0, 3).map((r: any) => r.nodeType).join(', ')}`);
}
process.exit(failedTests > 0 ? 1 : 0);
}
// Run tests
testSearchImprovements().catch(error => {
console.error('Test execution failed:', error);
process.exit(1);
});