feat: Implement n8n-MCP Enhancement Plan v2.1 Final

- Implement simple node loader supporting n8n-nodes-base and langchain packages
- Create parser handling declarative, programmatic, and versioned nodes
- Build documentation mapper with 89% coverage (405/457 nodes)
- Setup SQLite database with minimal schema
- Create rebuild script for one-command database updates
- Implement validation script for critical nodes
- Update MCP server with documentation-focused tools
- Add npm scripts for streamlined workflow

Successfully loads 457/458 nodes with accurate documentation mapping.
Versioned node detection working (46 nodes detected).
3/4 critical nodes pass validation tests.

Known limitations:
- Slack operations extraction incomplete for some versioned nodes
- One langchain node fails due to missing dependency
- No AI tools detected (none have usableAsTool flag)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-06-12 14:18:19 +02:00
parent b50025081a
commit 8bf670c31e
21 changed files with 9206 additions and 790 deletions

313
src/mcp/server-update.ts Normal file
View File

@@ -0,0 +1,313 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import Database from 'better-sqlite3';
import { n8nDocumentationTools } from './tools-update';
import { logger } from '../utils/logger';
interface NodeRow {
node_type: string;
package_name: string;
display_name: string;
description?: string;
category?: string;
development_style?: string;
is_ai_tool: number;
is_trigger: number;
is_webhook: number;
is_versioned: number;
version?: string;
documentation?: string;
properties_schema?: string;
operations?: string;
credentials_required?: string;
}
export class N8NDocumentationMCPServer {
private server: Server;
private db: Database.Database;
constructor() {
this.db = new Database('./data/nodes.db');
logger.info('Initializing n8n Documentation MCP server');
this.server = new Server(
{
name: 'n8n-documentation-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
private setupHandlers(): void {
// Handle tool listing
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: n8nDocumentationTools,
}));
// Handle tool execution
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
logger.debug(`Executing tool: ${name}`, { args });
const result = await this.executeTool(name, args);
logger.debug(`Tool ${name} executed successfully`);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
logger.error(`Error executing tool ${name}`, error);
return {
content: [
{
type: 'text',
text: `Error executing tool ${name}: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
});
}
private async executeTool(name: string, args: any): Promise<any> {
switch (name) {
case 'list_nodes':
return this.listNodes(args);
case 'get_node_info':
return this.getNodeInfo(args.nodeType);
case 'search_nodes':
return this.searchNodes(args.query, args.limit);
case 'list_ai_tools':
return this.listAITools();
case 'get_node_documentation':
return this.getNodeDocumentation(args.nodeType);
case 'get_database_statistics':
return this.getDatabaseStatistics();
default:
throw new Error(`Unknown tool: ${name}`);
}
}
private listNodes(filters: any = {}): any {
let query = 'SELECT * FROM nodes WHERE 1=1';
const params: any[] = [];
if (filters.package) {
query += ' AND package_name = ?';
params.push(filters.package);
}
if (filters.category) {
query += ' AND category = ?';
params.push(filters.category);
}
if (filters.developmentStyle) {
query += ' AND development_style = ?';
params.push(filters.developmentStyle);
}
if (filters.isAITool !== undefined) {
query += ' AND is_ai_tool = ?';
params.push(filters.isAITool ? 1 : 0);
}
query += ' ORDER BY display_name';
if (filters.limit) {
query += ' LIMIT ?';
params.push(filters.limit);
}
const nodes = this.db.prepare(query).all(...params) as NodeRow[];
return {
nodes: nodes.map(node => ({
nodeType: node.node_type,
displayName: node.display_name,
description: node.description,
category: node.category,
package: node.package_name,
developmentStyle: node.development_style,
isAITool: !!node.is_ai_tool,
isTrigger: !!node.is_trigger,
isVersioned: !!node.is_versioned,
})),
totalCount: nodes.length,
};
}
private getNodeInfo(nodeType: string): any {
const node = this.db.prepare(`
SELECT * FROM nodes WHERE node_type = ?
`).get(nodeType) as NodeRow | undefined;
if (!node) {
throw new Error(`Node ${nodeType} not found`);
}
return {
nodeType: node.node_type,
displayName: node.display_name,
description: node.description,
category: node.category,
developmentStyle: node.development_style,
package: node.package_name,
isAITool: !!node.is_ai_tool,
isTrigger: !!node.is_trigger,
isWebhook: !!node.is_webhook,
isVersioned: !!node.is_versioned,
version: node.version,
properties: JSON.parse(node.properties_schema || '[]'),
operations: JSON.parse(node.operations || '[]'),
credentials: JSON.parse(node.credentials_required || '[]'),
hasDocumentation: !!node.documentation,
};
}
private searchNodes(query: string, limit: number = 20): any {
// Simple search across multiple fields
const searchQuery = `%${query}%`;
const nodes = this.db.prepare(`
SELECT * FROM nodes
WHERE node_type LIKE ?
OR display_name LIKE ?
OR description LIKE ?
OR documentation LIKE ?
ORDER BY
CASE
WHEN node_type LIKE ? THEN 1
WHEN display_name LIKE ? THEN 2
ELSE 3
END
LIMIT ?
`).all(
searchQuery, searchQuery, searchQuery, searchQuery,
searchQuery, searchQuery,
limit
) as NodeRow[];
return {
query,
results: nodes.map(node => ({
nodeType: node.node_type,
displayName: node.display_name,
description: node.description,
category: node.category,
package: node.package_name,
relevance: this.calculateRelevance(node, query),
})),
totalCount: nodes.length,
};
}
private calculateRelevance(node: NodeRow, query: string): string {
const lowerQuery = query.toLowerCase();
if (node.node_type.toLowerCase().includes(lowerQuery)) return 'high';
if (node.display_name.toLowerCase().includes(lowerQuery)) return 'high';
if (node.description?.toLowerCase().includes(lowerQuery)) return 'medium';
return 'low';
}
private listAITools(): any {
const tools = this.db.prepare(`
SELECT node_type, display_name, description, package_name
FROM nodes
WHERE is_ai_tool = 1
ORDER BY display_name
`).all() as NodeRow[];
return {
tools: tools.map(tool => ({
nodeType: tool.node_type,
displayName: tool.display_name,
description: tool.description,
package: tool.package_name,
})),
totalCount: tools.length,
requirements: {
environmentVariable: 'N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true',
nodeProperty: 'usableAsTool: true',
},
};
}
private getNodeDocumentation(nodeType: string): any {
const node = this.db.prepare(`
SELECT node_type, display_name, documentation
FROM nodes
WHERE node_type = ?
`).get(nodeType) as NodeRow | undefined;
if (!node) {
throw new Error(`Node ${nodeType} not found`);
}
return {
nodeType: node.node_type,
displayName: node.display_name,
documentation: node.documentation || 'No documentation available',
hasDocumentation: !!node.documentation,
};
}
private getDatabaseStatistics(): any {
const stats = this.db.prepare(`
SELECT
COUNT(*) as total,
SUM(is_ai_tool) as ai_tools,
SUM(is_trigger) as triggers,
SUM(is_versioned) as versioned,
SUM(CASE WHEN documentation IS NOT NULL THEN 1 ELSE 0 END) as with_docs,
COUNT(DISTINCT package_name) as packages,
COUNT(DISTINCT category) as categories
FROM nodes
`).get() as any;
const packages = this.db.prepare(`
SELECT package_name, COUNT(*) as count
FROM nodes
GROUP BY package_name
`).all() as any[];
return {
totalNodes: stats.total,
statistics: {
aiTools: stats.ai_tools,
triggers: stats.triggers,
versionedNodes: stats.versioned,
nodesWithDocumentation: stats.with_docs,
documentationCoverage: Math.round((stats.with_docs / stats.total) * 100) + '%',
uniquePackages: stats.packages,
uniqueCategories: stats.categories,
},
packageBreakdown: packages.map(pkg => ({
package: pkg.package_name,
nodeCount: pkg.count,
})),
};
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
logger.info('n8n Documentation MCP Server running on stdio transport');
}
}