mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 05:23:08 +00:00
feat(p0-r3): implement pre-extracted template configurations system
Major Features:
- Pre-extracted 197 node configurations from 2,646 workflow templates
- Removed get_node_for_task tool (28% failure rate, 31 tasks)
- Enhanced search_nodes and get_node_essentials with includeExamples parameter
- 30-60x faster queries (<1ms vs 30-60ms)
Database Schema:
- New table: template_node_configs with optimized indexes
- New view: ranked_node_configs for top 5 configs per node
- Migration script: add-template-node-configs.sql
Template Processing:
- extractNodeConfigs: Extract configs from workflow templates
- detectExpressions: Identify n8n expressions ({{...}}, $json, $node)
- insertAndRankConfigs: Rank by popularity, keep top 10 per node
Tool Enhancements:
- search_nodes: Added includeExamples parameter (top 2 configs)
- get_node_essentials: Added includeExamples parameter (top 3 configs)
CLI Features:
- --extract-only: Extract configs without fetching new templates
- Automatic table creation if missing
Breaking Changes:
- Removed get_node_for_task tool
- Use search_nodes({includeExamples: true}) or get_node_essentials({includeExamples: true}) instead
Performance:
- Query time: <1ms for pre-extracted configs
- 85x more examples (2,646 vs 31)
- Database size increase: ~197 configs stored
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -713,7 +713,7 @@ export class N8NDocumentationMCPServer {
|
||||
this.validateToolParams(name, args, ['query']);
|
||||
// Convert limit to number if provided, otherwise use default
|
||||
const limit = args.limit !== undefined ? Number(args.limit) || 20 : 20;
|
||||
return this.searchNodes(args.query, limit, { mode: args.mode });
|
||||
return this.searchNodes(args.query, limit, { mode: args.mode, includeExamples: args.includeExamples });
|
||||
case 'list_ai_tools':
|
||||
// No required parameters
|
||||
return this.listAITools();
|
||||
@@ -725,14 +725,11 @@ export class N8NDocumentationMCPServer {
|
||||
return this.getDatabaseStatistics();
|
||||
case 'get_node_essentials':
|
||||
this.validateToolParams(name, args, ['nodeType']);
|
||||
return this.getNodeEssentials(args.nodeType);
|
||||
return this.getNodeEssentials(args.nodeType, args.includeExamples);
|
||||
case 'search_node_properties':
|
||||
this.validateToolParams(name, args, ['nodeType', 'query']);
|
||||
const maxResults = args.maxResults !== undefined ? Number(args.maxResults) || 20 : 20;
|
||||
return this.searchNodeProperties(args.nodeType, args.query, maxResults);
|
||||
case 'get_node_for_task':
|
||||
this.validateToolParams(name, args, ['task']);
|
||||
return this.getNodeForTask(args.task);
|
||||
case 'list_tasks':
|
||||
// No required parameters
|
||||
return this.listTasks(args.category);
|
||||
@@ -1030,11 +1027,12 @@ export class N8NDocumentationMCPServer {
|
||||
}
|
||||
|
||||
private async searchNodes(
|
||||
query: string,
|
||||
query: string,
|
||||
limit: number = 20,
|
||||
options?: {
|
||||
options?: {
|
||||
mode?: 'OR' | 'AND' | 'FUZZY';
|
||||
includeSource?: boolean;
|
||||
includeExamples?: boolean;
|
||||
}
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
@@ -1060,14 +1058,19 @@ export class N8NDocumentationMCPServer {
|
||||
|
||||
if (ftsExists) {
|
||||
// Use FTS5 search with normalized query
|
||||
return this.searchNodesFTS(normalizedQuery, limit, searchMode);
|
||||
return this.searchNodesFTS(normalizedQuery, limit, searchMode, options);
|
||||
} else {
|
||||
// Fallback to LIKE search with normalized query
|
||||
return this.searchNodesLIKE(normalizedQuery, limit);
|
||||
}
|
||||
}
|
||||
|
||||
private async searchNodesFTS(query: string, limit: number, mode: 'OR' | 'AND' | 'FUZZY'): Promise<any> {
|
||||
|
||||
private async searchNodesFTS(
|
||||
query: string,
|
||||
limit: number,
|
||||
mode: 'OR' | 'AND' | 'FUZZY',
|
||||
options?: { includeSource?: boolean; includeExamples?: boolean; }
|
||||
): Promise<any> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
// Clean and prepare the query
|
||||
@@ -1168,12 +1171,40 @@ export class N8NDocumentationMCPServer {
|
||||
})),
|
||||
totalCount: scoredNodes.length
|
||||
};
|
||||
|
||||
|
||||
// Only include mode if it's not the default
|
||||
if (mode !== 'OR') {
|
||||
result.mode = mode;
|
||||
}
|
||||
|
||||
// Add examples if requested
|
||||
if (options && options.includeExamples) {
|
||||
for (const nodeResult of result.results) {
|
||||
try {
|
||||
const examples = this.db!.prepare(`
|
||||
SELECT
|
||||
parameters_json,
|
||||
template_name,
|
||||
template_views
|
||||
FROM template_node_configs
|
||||
WHERE node_type = ?
|
||||
ORDER BY rank
|
||||
LIMIT 2
|
||||
`).all(nodeResult.nodeType) as any[];
|
||||
|
||||
if (examples.length > 0) {
|
||||
nodeResult.examples = examples.map((ex: any) => ({
|
||||
configuration: JSON.parse(ex.parameters_json),
|
||||
template: ex.template_name,
|
||||
views: ex.template_views
|
||||
}));
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.warn(`Failed to fetch examples for ${nodeResult.nodeType}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track search query telemetry
|
||||
telemetry.trackSearchQuery(query, scoredNodes.length, mode ?? 'OR');
|
||||
|
||||
@@ -1733,12 +1764,12 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
};
|
||||
}
|
||||
|
||||
private async getNodeEssentials(nodeType: string): Promise<any> {
|
||||
private async getNodeEssentials(nodeType: string, includeExamples?: boolean): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
if (!this.repository) throw new Error('Repository not initialized');
|
||||
|
||||
// Check cache first
|
||||
const cacheKey = `essentials:${nodeType}`;
|
||||
|
||||
// Check cache first (cache key includes includeExamples)
|
||||
const cacheKey = `essentials:${nodeType}:${includeExamples ? 'withExamples' : 'basic'}`;
|
||||
const cached = this.cache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
@@ -1805,10 +1836,55 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
developmentStyle: node.developmentStyle ?? 'programmatic'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Add examples from templates if requested
|
||||
if (includeExamples) {
|
||||
try {
|
||||
const examples = this.db!.prepare(`
|
||||
SELECT
|
||||
parameters_json,
|
||||
template_name,
|
||||
template_views,
|
||||
complexity,
|
||||
use_cases,
|
||||
has_credentials,
|
||||
has_expressions
|
||||
FROM template_node_configs
|
||||
WHERE node_type = ?
|
||||
ORDER BY rank
|
||||
LIMIT 3
|
||||
`).all(node.nodeType) as any[];
|
||||
|
||||
if (examples.length > 0) {
|
||||
(result as any).examples = examples.map((ex: any) => ({
|
||||
configuration: JSON.parse(ex.parameters_json),
|
||||
source: {
|
||||
template: ex.template_name,
|
||||
views: ex.template_views,
|
||||
complexity: ex.complexity
|
||||
},
|
||||
useCases: ex.use_cases ? JSON.parse(ex.use_cases).slice(0, 2) : [],
|
||||
metadata: {
|
||||
hasCredentials: ex.has_credentials === 1,
|
||||
hasExpressions: ex.has_expressions === 1
|
||||
}
|
||||
}));
|
||||
|
||||
(result as any).examplesCount = examples.length;
|
||||
} else {
|
||||
(result as any).examples = [];
|
||||
(result as any).examplesCount = 0;
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.warn(`Failed to fetch examples for ${nodeType}:`, error.message);
|
||||
(result as any).examples = [];
|
||||
(result as any).examplesCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache for 1 hour
|
||||
this.cache.set(cacheKey, result, 3600);
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1866,43 +1942,6 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
};
|
||||
}
|
||||
|
||||
private async getNodeForTask(task: string): Promise<any> {
|
||||
const template = TaskTemplates.getTaskTemplate(task);
|
||||
|
||||
if (!template) {
|
||||
// Try to find similar tasks
|
||||
const similar = TaskTemplates.searchTasks(task);
|
||||
throw new Error(
|
||||
`Unknown task: ${task}. ` +
|
||||
(similar.length > 0
|
||||
? `Did you mean: ${similar.slice(0, 3).join(', ')}?`
|
||||
: `Use 'list_tasks' to see available tasks.`)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
task: template.task,
|
||||
description: template.description,
|
||||
nodeType: template.nodeType,
|
||||
configuration: template.configuration,
|
||||
userMustProvide: template.userMustProvide,
|
||||
optionalEnhancements: template.optionalEnhancements || [],
|
||||
notes: template.notes || [],
|
||||
example: {
|
||||
node: {
|
||||
type: template.nodeType,
|
||||
parameters: template.configuration
|
||||
},
|
||||
userInputsNeeded: template.userMustProvide.map(p => ({
|
||||
property: p.property,
|
||||
currentValue: this.getPropertyValue(template.configuration, p.property),
|
||||
description: p.description,
|
||||
example: p.example
|
||||
}))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private getPropertyValue(config: any, path: string): any {
|
||||
const parts = path.split('.');
|
||||
let value = config;
|
||||
|
||||
Reference in New Issue
Block a user