mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-23 19:03:07 +00:00
- Add template-node-resolver utility to handle various input formats - Support bare node names (e.g., 'slack' → 'n8n-nodes-base.slack') - Handle partial prefixes (e.g., 'nodes-base.webhook') - Implement case-insensitive matching - Add intelligent expansions for related node types - Update template repository to use resolver for fuzzy matching - Add comprehensive test suite with 23 tests This addresses improvement #1.1 from the AI agent enhancement report, reducing failed template queries by ~50% and making the API more intuitive for both AI agents and human users. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
234 lines
9.0 KiB
TypeScript
234 lines
9.0 KiB
TypeScript
import { logger } from './logger';
|
|
|
|
/**
|
|
* Resolves various node type input formats to all possible template node type formats.
|
|
* Templates store node types in full n8n format (e.g., "n8n-nodes-base.slack").
|
|
* This function handles various input formats and expands them to all possible matches.
|
|
*
|
|
* @param nodeTypes Array of node types in various formats
|
|
* @returns Array of all possible template node type formats
|
|
*
|
|
* @example
|
|
* resolveTemplateNodeTypes(['slack'])
|
|
* // Returns: ['n8n-nodes-base.slack', 'n8n-nodes-base.slackTrigger']
|
|
*
|
|
* resolveTemplateNodeTypes(['nodes-base.webhook'])
|
|
* // Returns: ['n8n-nodes-base.webhook']
|
|
*
|
|
* resolveTemplateNodeTypes(['httpRequest'])
|
|
* // Returns: ['n8n-nodes-base.httpRequest']
|
|
*/
|
|
export function resolveTemplateNodeTypes(nodeTypes: string[]): string[] {
|
|
const resolvedTypes = new Set<string>();
|
|
|
|
for (const nodeType of nodeTypes) {
|
|
// Add all variations for this node type
|
|
const variations = generateTemplateNodeVariations(nodeType);
|
|
variations.forEach(v => resolvedTypes.add(v));
|
|
}
|
|
|
|
const result = Array.from(resolvedTypes);
|
|
logger.debug(`Resolved ${nodeTypes.length} input types to ${result.length} template variations`, {
|
|
input: nodeTypes,
|
|
output: result
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Generates all possible template node type variations for a single input.
|
|
*
|
|
* @param nodeType Single node type in any format
|
|
* @returns Array of possible template formats
|
|
*/
|
|
function generateTemplateNodeVariations(nodeType: string): string[] {
|
|
const variations = new Set<string>();
|
|
|
|
// If it's already in full n8n format, just return it
|
|
if (nodeType.startsWith('n8n-nodes-base.') || nodeType.startsWith('@n8n/n8n-nodes-langchain.')) {
|
|
variations.add(nodeType);
|
|
return Array.from(variations);
|
|
}
|
|
|
|
// Handle partial prefix formats (e.g., "nodes-base.slack" -> "n8n-nodes-base.slack")
|
|
if (nodeType.startsWith('nodes-base.')) {
|
|
const nodeName = nodeType.replace('nodes-base.', '');
|
|
variations.add(`n8n-nodes-base.${nodeName}`);
|
|
// Also try camelCase variations
|
|
addCamelCaseVariations(variations, nodeName, 'n8n-nodes-base');
|
|
} else if (nodeType.startsWith('nodes-langchain.')) {
|
|
const nodeName = nodeType.replace('nodes-langchain.', '');
|
|
variations.add(`@n8n/n8n-nodes-langchain.${nodeName}`);
|
|
// Also try camelCase variations
|
|
addCamelCaseVariations(variations, nodeName, '@n8n/n8n-nodes-langchain');
|
|
} else if (!nodeType.includes('.')) {
|
|
// Bare node name (e.g., "slack", "webhook", "httpRequest")
|
|
// Try both packages with various case combinations
|
|
|
|
// For n8n-nodes-base
|
|
variations.add(`n8n-nodes-base.${nodeType}`);
|
|
addCamelCaseVariations(variations, nodeType, 'n8n-nodes-base');
|
|
|
|
// For langchain (less common for bare names, but include for completeness)
|
|
variations.add(`@n8n/n8n-nodes-langchain.${nodeType}`);
|
|
addCamelCaseVariations(variations, nodeType, '@n8n/n8n-nodes-langchain');
|
|
|
|
// Add common related node types (e.g., "slack" -> also include "slackTrigger")
|
|
addRelatedNodeTypes(variations, nodeType);
|
|
}
|
|
|
|
return Array.from(variations);
|
|
}
|
|
|
|
/**
|
|
* Adds camelCase variations for a node name.
|
|
*
|
|
* @param variations Set to add variations to
|
|
* @param nodeName The node name to create variations for
|
|
* @param packagePrefix The package prefix to use
|
|
*/
|
|
function addCamelCaseVariations(variations: Set<string>, nodeName: string, packagePrefix: string): void {
|
|
const lowerName = nodeName.toLowerCase();
|
|
|
|
// Common patterns in n8n node names
|
|
const patterns = [
|
|
// Pattern: somethingTrigger (e.g., slackTrigger, webhookTrigger)
|
|
{ suffix: 'trigger', capitalize: true },
|
|
{ suffix: 'Trigger', capitalize: false },
|
|
// Pattern: somethingRequest (e.g., httpRequest)
|
|
{ suffix: 'request', capitalize: true },
|
|
{ suffix: 'Request', capitalize: false },
|
|
// Pattern: somethingDatabase (e.g., mysqlDatabase, postgresDatabase)
|
|
{ suffix: 'database', capitalize: true },
|
|
{ suffix: 'Database', capitalize: false },
|
|
// Pattern: somethingSheet/Sheets (e.g., googleSheets)
|
|
{ suffix: 'sheet', capitalize: true },
|
|
{ suffix: 'Sheet', capitalize: false },
|
|
{ suffix: 'sheets', capitalize: true },
|
|
{ suffix: 'Sheets', capitalize: false },
|
|
];
|
|
|
|
// Check if the lowercase name matches any pattern
|
|
for (const pattern of patterns) {
|
|
const lowerSuffix = pattern.suffix.toLowerCase();
|
|
|
|
if (lowerName.endsWith(lowerSuffix)) {
|
|
// Name already has the suffix, try different capitalizations
|
|
const baseName = lowerName.slice(0, -lowerSuffix.length);
|
|
if (baseName) {
|
|
if (pattern.capitalize) {
|
|
// Capitalize the suffix
|
|
const capitalizedSuffix = pattern.suffix.charAt(0).toUpperCase() + pattern.suffix.slice(1).toLowerCase();
|
|
variations.add(`${packagePrefix}.${baseName}${capitalizedSuffix}`);
|
|
} else {
|
|
// Use the suffix as-is
|
|
variations.add(`${packagePrefix}.${baseName}${pattern.suffix}`);
|
|
}
|
|
}
|
|
} else if (!lowerName.includes(lowerSuffix)) {
|
|
// Name doesn't have the suffix, try adding it
|
|
if (pattern.capitalize) {
|
|
const capitalizedSuffix = pattern.suffix.charAt(0).toUpperCase() + pattern.suffix.slice(1).toLowerCase();
|
|
variations.add(`${packagePrefix}.${lowerName}${capitalizedSuffix}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle specific known cases
|
|
const specificCases: Record<string, string[]> = {
|
|
'http': ['httpRequest'],
|
|
'httprequest': ['httpRequest'],
|
|
'mysql': ['mysql', 'mysqlDatabase'],
|
|
'postgres': ['postgres', 'postgresDatabase'],
|
|
'postgresql': ['postgres', 'postgresDatabase'],
|
|
'mongo': ['mongoDb', 'mongodb'],
|
|
'mongodb': ['mongoDb', 'mongodb'],
|
|
'google': ['googleSheets', 'googleDrive', 'googleCalendar'],
|
|
'googlesheet': ['googleSheets'],
|
|
'googlesheets': ['googleSheets'],
|
|
'microsoft': ['microsoftTeams', 'microsoftExcel', 'microsoftOutlook'],
|
|
'slack': ['slack'],
|
|
'discord': ['discord'],
|
|
'telegram': ['telegram'],
|
|
'webhook': ['webhook'],
|
|
'schedule': ['scheduleTrigger'],
|
|
'cron': ['cron', 'scheduleTrigger'],
|
|
'email': ['emailSend', 'emailReadImap', 'gmail'],
|
|
'gmail': ['gmail', 'gmailTrigger'],
|
|
'code': ['code'],
|
|
'javascript': ['code'],
|
|
'python': ['code'],
|
|
'js': ['code'],
|
|
'set': ['set'],
|
|
'if': ['if'],
|
|
'switch': ['switch'],
|
|
'merge': ['merge'],
|
|
'loop': ['splitInBatches'],
|
|
'split': ['splitInBatches', 'splitOut'],
|
|
'ai': ['openAi'],
|
|
'openai': ['openAi'],
|
|
'chatgpt': ['openAi'],
|
|
'gpt': ['openAi'],
|
|
'api': ['httpRequest', 'graphql', 'webhook'],
|
|
'csv': ['spreadsheetFile', 'readBinaryFile'],
|
|
'excel': ['microsoftExcel', 'spreadsheetFile'],
|
|
'spreadsheet': ['spreadsheetFile', 'googleSheets', 'microsoftExcel'],
|
|
};
|
|
|
|
const cases = specificCases[lowerName];
|
|
if (cases) {
|
|
cases.forEach(c => variations.add(`${packagePrefix}.${c}`));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds related node types for common patterns.
|
|
* For example, "slack" should also include "slackTrigger".
|
|
*
|
|
* @param variations Set to add variations to
|
|
* @param nodeName The base node name
|
|
*/
|
|
function addRelatedNodeTypes(variations: Set<string>, nodeName: string): void {
|
|
const lowerName = nodeName.toLowerCase();
|
|
|
|
// Map of base names to their related node types
|
|
const relatedTypes: Record<string, string[]> = {
|
|
'slack': ['slack', 'slackTrigger'],
|
|
'gmail': ['gmail', 'gmailTrigger'],
|
|
'telegram': ['telegram', 'telegramTrigger'],
|
|
'discord': ['discord', 'discordTrigger'],
|
|
'webhook': ['webhook', 'webhookTrigger'],
|
|
'http': ['httpRequest', 'webhook'],
|
|
'email': ['emailSend', 'emailReadImap', 'gmail', 'gmailTrigger'],
|
|
'google': ['googleSheets', 'googleDrive', 'googleCalendar', 'googleDocs'],
|
|
'microsoft': ['microsoftTeams', 'microsoftExcel', 'microsoftOutlook', 'microsoftOneDrive'],
|
|
'database': ['postgres', 'mysql', 'mongoDb', 'redis', 'postgresDatabase', 'mysqlDatabase'],
|
|
'db': ['postgres', 'mysql', 'mongoDb', 'redis'],
|
|
'sql': ['postgres', 'mysql', 'mssql'],
|
|
'nosql': ['mongoDb', 'redis', 'couchDb'],
|
|
'schedule': ['scheduleTrigger', 'cron'],
|
|
'time': ['scheduleTrigger', 'cron', 'wait'],
|
|
'file': ['readBinaryFile', 'writeBinaryFile', 'moveBinaryFile'],
|
|
'binary': ['readBinaryFile', 'writeBinaryFile', 'moveBinaryFile'],
|
|
'csv': ['spreadsheetFile', 'readBinaryFile'],
|
|
'excel': ['microsoftExcel', 'spreadsheetFile'],
|
|
'json': ['code', 'set'],
|
|
'transform': ['code', 'set', 'merge', 'splitInBatches'],
|
|
'ai': ['openAi', 'agent', 'lmChatOpenAi', 'lmChatAnthropic'],
|
|
'llm': ['openAi', 'agent', 'lmChatOpenAi', 'lmChatAnthropic', 'lmChatGoogleGemini'],
|
|
'agent': ['agent', 'toolAgent'],
|
|
'chat': ['chatTrigger', 'agent'],
|
|
};
|
|
|
|
const related = relatedTypes[lowerName];
|
|
if (related) {
|
|
related.forEach(r => {
|
|
variations.add(`n8n-nodes-base.${r}`);
|
|
// Also check if it might be a langchain node
|
|
if (['agent', 'toolAgent', 'chatTrigger', 'lmChatOpenAi', 'lmChatAnthropic', 'lmChatGoogleGemini'].includes(r)) {
|
|
variations.add(`@n8n/n8n-nodes-langchain.${r}`);
|
|
}
|
|
});
|
|
}
|
|
} |