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:
313
src/mcp/server-update.ts
Normal file
313
src/mcp/server-update.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user