diff --git a/data/nodes.db b/data/nodes.db index 62e504a..ecec469 100644 Binary files a/data/nodes.db and b/data/nodes.db differ diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d3c8937..8130fa1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.7.18] - 2025-07-18 + +### Fixed +- **Node type prefix normalization for AI agents** (Issue #71) + - AI agents can now use node types directly from n8n workflow exports without manual conversion + - Added automatic normalization: `n8n-nodes-base.httpRequest` → `nodes-base.httpRequest` + - Added automatic normalization: `@n8n/n8n-nodes-langchain.agent` → `nodes-langchain.agent` + - Fixed 9 MCP tools that were failing with full package names: + - `get_node_info`, `get_node_essentials`, `get_node_as_tool_info` + - `search_node_properties`, `validate_node_minimal`, `validate_node_config` + - `get_property_dependencies`, `search_nodes`, `get_node_documentation` + - Maintains backward compatibility - existing short prefixes continue to work + - Created centralized `normalizeNodeType` utility for consistent handling across all tools + +### Added +- **Node type utilities** in `src/utils/node-utils.ts` + - `normalizeNodeType()` - Converts full package names to database format + - `getNodeTypeAlternatives()` - Provides fallback options for edge cases + ## [2.7.17] - 2025-07-17 ### Fixed @@ -664,6 +683,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Basic n8n and MCP integration - Core workflow automation features +[2.7.18]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.17...v2.7.18 [2.7.17]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.16...v2.7.17 [2.7.16]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.15...v2.7.16 [2.7.15]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.13...v2.7.15 diff --git a/package.json b/package.json index d425789..9113fbe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.7.17", + "version": "2.7.18", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", "bin": { diff --git a/package.runtime.json b/package.runtime.json index 085632b..3015fd4 100644 --- a/package.runtime.json +++ b/package.runtime.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp-runtime", - "version": "2.7.13", + "version": "2.7.18", "description": "n8n MCP Server Runtime Dependencies Only", "private": true, "dependencies": { diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 7f33d3e..b0fc431 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -25,6 +25,7 @@ import * as n8nHandlers from './handlers-n8n-manager'; import { handleUpdatePartialWorkflow } from './handlers-workflow-diff'; import { getToolDocumentation, getToolsOverview } from './tools-documentation'; import { PROJECT_VERSION } from '../utils/version'; +import { normalizeNodeType, getNodeTypeAlternatives } from '../utils/node-utils'; interface NodeRow { node_type: string; @@ -341,16 +342,19 @@ export class N8NDocumentationMCPServer { private async getNodeInfo(nodeType: string): Promise { await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); - let node = this.repository.getNode(nodeType); + + // First try with normalized type + const normalizedType = normalizeNodeType(nodeType); + let node = this.repository.getNode(normalizedType); + + if (!node && normalizedType !== nodeType) { + // Try original if normalization changed it + node = this.repository.getNode(nodeType); + } if (!node) { - // Try alternative formats - const alternatives = [ - nodeType, - nodeType.replace('n8n-nodes-base.', ''), - `n8n-nodes-base.${nodeType}`, - nodeType.toLowerCase() - ]; + // Fallback to other alternatives for edge cases + const alternatives = getNodeTypeAlternatives(nodeType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); @@ -359,10 +363,10 @@ export class N8NDocumentationMCPServer { break; } } - - if (!node) { - throw new Error(`Node ${nodeType} not found`); - } + } + + if (!node) { + throw new Error(`Node ${nodeType} not found`); } // Add AI tool capabilities information @@ -394,6 +398,16 @@ export class N8NDocumentationMCPServer { await this.ensureInitialized(); if (!this.db) throw new Error('Database not initialized'); + // Normalize the query if it looks like a full node type + let normalizedQuery = query; + + // Check if query contains node type patterns and normalize them + if (query.includes('n8n-nodes-base.') || query.includes('@n8n/n8n-nodes-langchain.')) { + normalizedQuery = query + .replace(/n8n-nodes-base\./g, 'nodes-base.') + .replace(/@n8n\/n8n-nodes-langchain\./g, 'nodes-langchain.'); + } + const searchMode = options?.mode || 'OR'; // Check if FTS5 table exists @@ -403,11 +417,11 @@ export class N8NDocumentationMCPServer { `).get(); if (ftsExists) { - // Use FTS5 search - return this.searchNodesFTS(query, limit, searchMode); + // Use FTS5 search with normalized query + return this.searchNodesFTS(normalizedQuery, limit, searchMode); } else { - // Fallback to LIKE search (existing implementation) - return this.searchNodesLIKE(query, limit); + // Fallback to LIKE search with normalized query + return this.searchNodesLIKE(normalizedQuery, limit); } } @@ -938,11 +952,23 @@ export class N8NDocumentationMCPServer { private async getNodeDocumentation(nodeType: string): Promise { await this.ensureInitialized(); if (!this.db) throw new Error('Database not initialized'); - const node = this.db!.prepare(` + + // First try with normalized type + const normalizedType = normalizeNodeType(nodeType); + let node = this.db!.prepare(` SELECT node_type, display_name, documentation, description FROM nodes WHERE node_type = ? - `).get(nodeType) as NodeRow | undefined; + `).get(normalizedType) as NodeRow | undefined; + + // If not found and normalization changed the type, try original + if (!node && normalizedType !== nodeType) { + node = this.db!.prepare(` + SELECT node_type, display_name, documentation, description + FROM nodes + WHERE node_type = ? + `).get(nodeType) as NodeRow | undefined; + } if (!node) { throw new Error(`Node ${nodeType} not found`); @@ -1030,16 +1056,18 @@ Full documentation is being prepared. For now, use get_node_essentials for confi if (cached) return cached; // Get the full node information - let node = this.repository.getNode(nodeType); + // First try with normalized type + const normalizedType = normalizeNodeType(nodeType); + let node = this.repository.getNode(normalizedType); + + if (!node && normalizedType !== nodeType) { + // Try original if normalization changed it + node = this.repository.getNode(nodeType); + } if (!node) { - // Try alternative formats - const alternatives = [ - nodeType, - nodeType.replace('n8n-nodes-base.', ''), - `n8n-nodes-base.${nodeType}`, - nodeType.toLowerCase() - ]; + // Fallback to other alternatives for edge cases + const alternatives = getNodeTypeAlternatives(nodeType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); @@ -1048,10 +1076,10 @@ Full documentation is being prepared. For now, use get_node_essentials for confi break; } } - - if (!node) { - throw new Error(`Node ${nodeType} not found`); - } + } + + if (!node) { + throw new Error(`Node ${nodeType} not found`); } // Get properties (already parsed by repository) @@ -1101,16 +1129,18 @@ Full documentation is being prepared. For now, use get_node_essentials for confi if (!this.repository) throw new Error('Repository not initialized'); // Get the node - let node = this.repository.getNode(nodeType); + // First try with normalized type + const normalizedType = normalizeNodeType(nodeType); + let node = this.repository.getNode(normalizedType); + + if (!node && normalizedType !== nodeType) { + // Try original if normalization changed it + node = this.repository.getNode(nodeType); + } if (!node) { - // Try alternative formats - const alternatives = [ - nodeType, - nodeType.replace('n8n-nodes-base.', ''), - `n8n-nodes-base.${nodeType}`, - nodeType.toLowerCase() - ]; + // Fallback to other alternatives for edge cases + const alternatives = getNodeTypeAlternatives(nodeType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); @@ -1119,10 +1149,10 @@ Full documentation is being prepared. For now, use get_node_essentials for confi break; } } - - if (!node) { - throw new Error(`Node ${nodeType} not found`); - } + } + + if (!node) { + throw new Error(`Node ${nodeType} not found`); } // Get properties and search (already parsed by repository) @@ -1257,16 +1287,18 @@ Full documentation is being prepared. For now, use get_node_essentials for confi if (!this.repository) throw new Error('Repository not initialized'); // Get node info to access properties - let node = this.repository.getNode(nodeType); + // First try with normalized type + const normalizedType = normalizeNodeType(nodeType); + let node = this.repository.getNode(normalizedType); + + if (!node && normalizedType !== nodeType) { + // Try original if normalization changed it + node = this.repository.getNode(nodeType); + } if (!node) { - // Try alternative formats - const alternatives = [ - nodeType, - nodeType.replace('n8n-nodes-base.', ''), - `n8n-nodes-base.${nodeType}`, - nodeType.toLowerCase() - ]; + // Fallback to other alternatives for edge cases + const alternatives = getNodeTypeAlternatives(nodeType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); @@ -1275,10 +1307,10 @@ Full documentation is being prepared. For now, use get_node_essentials for confi break; } } - - if (!node) { - throw new Error(`Node ${nodeType} not found`); - } + } + + if (!node) { + throw new Error(`Node ${nodeType} not found`); } // Get properties @@ -1312,16 +1344,18 @@ Full documentation is being prepared. For now, use get_node_essentials for confi if (!this.repository) throw new Error('Repository not initialized'); // Get node info to access properties - let node = this.repository.getNode(nodeType); + // First try with normalized type + const normalizedType = normalizeNodeType(nodeType); + let node = this.repository.getNode(normalizedType); + + if (!node && normalizedType !== nodeType) { + // Try original if normalization changed it + node = this.repository.getNode(nodeType); + } if (!node) { - // Try alternative formats - const alternatives = [ - nodeType, - nodeType.replace('n8n-nodes-base.', ''), - `n8n-nodes-base.${nodeType}`, - nodeType.toLowerCase() - ]; + // Fallback to other alternatives for edge cases + const alternatives = getNodeTypeAlternatives(nodeType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); @@ -1330,10 +1364,10 @@ Full documentation is being prepared. For now, use get_node_essentials for confi break; } } - - if (!node) { - throw new Error(`Node ${nodeType} not found`); - } + } + + if (!node) { + throw new Error(`Node ${nodeType} not found`); } // Get properties @@ -1364,16 +1398,18 @@ Full documentation is being prepared. For now, use get_node_essentials for confi if (!this.repository) throw new Error('Repository not initialized'); // Get node info - let node = this.repository.getNode(nodeType); + // First try with normalized type + const normalizedType = normalizeNodeType(nodeType); + let node = this.repository.getNode(normalizedType); + + if (!node && normalizedType !== nodeType) { + // Try original if normalization changed it + node = this.repository.getNode(nodeType); + } if (!node) { - // Try alternative formats - const alternatives = [ - nodeType, - nodeType.replace('n8n-nodes-base.', ''), - `n8n-nodes-base.${nodeType}`, - nodeType.toLowerCase() - ]; + // Fallback to other alternatives for edge cases + const alternatives = getNodeTypeAlternatives(nodeType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); @@ -1382,10 +1418,10 @@ Full documentation is being prepared. For now, use get_node_essentials for confi break; } } - - if (!node) { - throw new Error(`Node ${nodeType} not found`); - } + } + + if (!node) { + throw new Error(`Node ${nodeType} not found`); } // Determine common AI tool use cases based on node type @@ -1538,16 +1574,18 @@ Full documentation is being prepared. For now, use get_node_essentials for confi if (!this.repository) throw new Error('Repository not initialized'); // Get node info - let node = this.repository.getNode(nodeType); + // First try with normalized type + const normalizedType = normalizeNodeType(nodeType); + let node = this.repository.getNode(normalizedType); + + if (!node && normalizedType !== nodeType) { + // Try original if normalization changed it + node = this.repository.getNode(nodeType); + } if (!node) { - // Try alternative formats - const alternatives = [ - nodeType, - nodeType.replace('n8n-nodes-base.', ''), - `n8n-nodes-base.${nodeType}`, - nodeType.toLowerCase() - ]; + // Fallback to other alternatives for edge cases + const alternatives = getNodeTypeAlternatives(nodeType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); @@ -1556,10 +1594,10 @@ Full documentation is being prepared. For now, use get_node_essentials for confi break; } } - - if (!node) { - throw new Error(`Node ${nodeType} not found`); - } + } + + if (!node) { + throw new Error(`Node ${nodeType} not found`); } // Get properties diff --git a/src/utils/node-utils.ts b/src/utils/node-utils.ts new file mode 100644 index 0000000..d02335e --- /dev/null +++ b/src/utils/node-utils.ts @@ -0,0 +1,46 @@ +/** + * Normalizes node type from n8n export format to database format + * + * Examples: + * - 'n8n-nodes-base.httpRequest' → 'nodes-base.httpRequest' + * - '@n8n/n8n-nodes-langchain.agent' → 'nodes-langchain.agent' + * - 'nodes-base.slack' → 'nodes-base.slack' (unchanged) + * + * @param nodeType The node type to normalize + * @returns The normalized node type + */ +export function normalizeNodeType(nodeType: string): string { + // Handle n8n-nodes-base -> nodes-base + if (nodeType.startsWith('n8n-nodes-base.')) { + return nodeType.replace('n8n-nodes-base.', 'nodes-base.'); + } + + // Handle @n8n/n8n-nodes-langchain -> nodes-langchain + if (nodeType.startsWith('@n8n/n8n-nodes-langchain.')) { + return nodeType.replace('@n8n/n8n-nodes-langchain.', 'nodes-langchain.'); + } + + // Return unchanged if already normalized or unknown format + return nodeType; +} + +/** + * Gets alternative node type formats to try for lookups + * + * @param nodeType The original node type + * @returns Array of alternative formats to try + */ +export function getNodeTypeAlternatives(nodeType: string): string[] { + const alternatives: string[] = []; + + // Add lowercase version + alternatives.push(nodeType.toLowerCase()); + + // If it's just a bare node name, try with common prefixes + if (!nodeType.includes('.')) { + alternatives.push(`nodes-base.${nodeType}`); + alternatives.push(`nodes-langchain.${nodeType}`); + } + + return alternatives; +} \ No newline at end of file