Implement SQLite database with full-text search for n8n node documentation
Major features implemented: - SQLite storage service with FTS5 for fast node search - Database rebuild mechanism for bulk node extraction - MCP tools: search_nodes, extract_all_nodes, get_node_statistics - Production Docker deployment with persistent storage - Management scripts for database operations - Comprehensive test suite for all functionality Database capabilities: - Stores node source code and metadata - Full-text search by node name or content - No versioning (stores latest only as per requirements) - Supports complete database rebuilds - ~4.5MB database with 500+ nodes indexed Production features: - Automated deployment script - Docker Compose production configuration - Database initialization on first run - Volume persistence for data - Management utilities for operations Documentation: - Updated README with complete instructions - Production deployment guide - Clear troubleshooting section - API reference for all new tools 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -16,15 +16,18 @@ import { N8NApiClient } from '../utils/n8n-client';
|
||||
import { N8NMCPBridge } from '../utils/bridge';
|
||||
import { logger } from '../utils/logger';
|
||||
import { NodeSourceExtractor } from '../utils/node-source-extractor';
|
||||
import { SQLiteStorageService } from '../services/sqlite-storage-service';
|
||||
|
||||
export class N8NMCPServer {
|
||||
private server: Server;
|
||||
private n8nClient: N8NApiClient;
|
||||
private nodeExtractor: NodeSourceExtractor;
|
||||
private nodeStorage: SQLiteStorageService;
|
||||
|
||||
constructor(config: MCPServerConfig, n8nConfig: N8NConfig) {
|
||||
this.n8nClient = new N8NApiClient(n8nConfig);
|
||||
this.nodeExtractor = new NodeSourceExtractor();
|
||||
this.nodeStorage = new SQLiteStorageService();
|
||||
logger.info('Initializing n8n MCP server', { config, n8nConfig });
|
||||
this.server = new Server(
|
||||
{
|
||||
@@ -161,6 +164,12 @@ export class N8NMCPServer {
|
||||
return this.getNodeSourceCode(args);
|
||||
case 'list_available_nodes':
|
||||
return this.listAvailableNodes(args);
|
||||
case 'extract_all_nodes':
|
||||
return this.extractAllNodes(args);
|
||||
case 'search_nodes':
|
||||
return this.searchNodes(args);
|
||||
case 'get_node_statistics':
|
||||
return this.getNodeStatistics(args);
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
@@ -314,6 +323,107 @@ export class N8NMCPServer {
|
||||
}
|
||||
}
|
||||
|
||||
private async extractAllNodes(args: any): Promise<any> {
|
||||
try {
|
||||
logger.info(`Extracting all nodes`, args);
|
||||
|
||||
// Get list of all nodes
|
||||
const allNodes = await this.nodeExtractor.listAvailableNodes();
|
||||
let nodesToExtract = allNodes;
|
||||
|
||||
// Apply filters
|
||||
if (args.packageFilter) {
|
||||
nodesToExtract = nodesToExtract.filter(node =>
|
||||
node.packageName === args.packageFilter ||
|
||||
node.location?.includes(args.packageFilter)
|
||||
);
|
||||
}
|
||||
|
||||
if (args.limit) {
|
||||
nodesToExtract = nodesToExtract.slice(0, args.limit);
|
||||
}
|
||||
|
||||
logger.info(`Extracting ${nodesToExtract.length} nodes...`);
|
||||
|
||||
const extractedNodes = [];
|
||||
const errors = [];
|
||||
|
||||
for (const node of nodesToExtract) {
|
||||
try {
|
||||
const nodeType = node.packageName ? `${node.packageName}.${node.name}` : node.name;
|
||||
const nodeInfo = await this.nodeExtractor.extractNodeSource(nodeType);
|
||||
await this.nodeStorage.storeNode(nodeInfo);
|
||||
extractedNodes.push(nodeType);
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
node: node.name,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const stats = await this.nodeStorage.getStatistics();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
extracted: extractedNodes.length,
|
||||
failed: errors.length,
|
||||
totalStored: stats.totalNodes,
|
||||
errors: errors.slice(0, 10), // Limit error list
|
||||
statistics: stats
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to extract all nodes`, error);
|
||||
throw new Error(`Failed to extract all nodes: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async searchNodes(args: any): Promise<any> {
|
||||
try {
|
||||
logger.info(`Searching nodes`, args);
|
||||
|
||||
const results = await this.nodeStorage.searchNodes({
|
||||
query: args.query,
|
||||
packageName: args.packageName,
|
||||
hasCredentials: args.hasCredentials,
|
||||
limit: args.limit || 20
|
||||
});
|
||||
|
||||
return {
|
||||
nodes: results.map(node => ({
|
||||
nodeType: node.nodeType,
|
||||
name: node.name,
|
||||
packageName: node.packageName,
|
||||
displayName: node.displayName,
|
||||
description: node.description,
|
||||
codeLength: node.codeLength,
|
||||
hasCredentials: node.hasCredentials,
|
||||
location: node.sourceLocation
|
||||
})),
|
||||
total: results.length
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to search nodes`, error);
|
||||
throw new Error(`Failed to search nodes: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async getNodeStatistics(args: any): Promise<any> {
|
||||
try {
|
||||
logger.info(`Getting node statistics`);
|
||||
const stats = await this.nodeStorage.getStatistics();
|
||||
|
||||
return {
|
||||
...stats,
|
||||
formattedTotalSize: `${(stats.totalCodeSize / 1024 / 1024).toFixed(2)} MB`,
|
||||
formattedAverageSize: `${(stats.averageNodeSize / 1024).toFixed(2)} KB`
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get node statistics`, error);
|
||||
throw new Error(`Failed to get node statistics: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
try {
|
||||
logger.info('Starting n8n MCP server...');
|
||||
|
||||
Reference in New Issue
Block a user