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:
19
src/mcp/index.ts
Normal file
19
src/mcp/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { N8NDocumentationMCPServer } from './server-update';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const server = new N8NDocumentationMCPServer();
|
||||
await server.run();
|
||||
} catch (error) {
|
||||
logger.error('Failed to start MCP server', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
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');
|
||||
}
|
||||
}
|
||||
98
src/mcp/tools-update.ts
Normal file
98
src/mcp/tools-update.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { ToolDefinition } from '../types';
|
||||
|
||||
export const n8nDocumentationTools: ToolDefinition[] = [
|
||||
{
|
||||
name: 'list_nodes',
|
||||
description: 'List all available n8n nodes with filtering options',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
package: {
|
||||
type: 'string',
|
||||
description: 'Filter by package name (e.g., n8n-nodes-base, @n8n/n8n-nodes-langchain)',
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
description: 'Filter by category',
|
||||
},
|
||||
developmentStyle: {
|
||||
type: 'string',
|
||||
enum: ['declarative', 'programmatic'],
|
||||
description: 'Filter by development style',
|
||||
},
|
||||
isAITool: {
|
||||
type: 'boolean',
|
||||
description: 'Filter to show only AI tools',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results to return',
|
||||
default: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_node_info',
|
||||
description: 'Get comprehensive information about a specific n8n node',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeType: {
|
||||
type: 'string',
|
||||
description: 'The node type (e.g., httpRequest, slack, code)',
|
||||
},
|
||||
},
|
||||
required: ['nodeType'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'search_nodes',
|
||||
description: 'Full-text search across all node documentation',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results',
|
||||
default: 20,
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_ai_tools',
|
||||
description: 'List all nodes that can be used as AI Agent tools',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_node_documentation',
|
||||
description: 'Get the full documentation for a specific node',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeType: {
|
||||
type: 'string',
|
||||
description: 'The node type',
|
||||
},
|
||||
},
|
||||
required: ['nodeType'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_database_statistics',
|
||||
description: 'Get statistics about the node database',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user