diff --git a/README.md b/README.md index 2d46011..2536113 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![GitHub stars](https://img.shields.io/github/stars/czlonkowski/n8n-mcp?style=social)](https://github.com/czlonkowski/n8n-mcp) -[![Version](https://img.shields.io/badge/version-2.7.18-blue.svg)](https://github.com/czlonkowski/n8n-mcp) +[![Version](https://img.shields.io/badge/version-2.7.19-blue.svg)](https://github.com/czlonkowski/n8n-mcp) [![npm version](https://img.shields.io/npm/v/n8n-mcp.svg)](https://www.npmjs.com/package/n8n-mcp) [![n8n version](https://img.shields.io/badge/n8n-v1.102.4-orange.svg)](https://github.com/n8n-io/n8n) [![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fczlonkowski%2Fn8n--mcp-green.svg)](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp) diff --git a/data/nodes.db b/data/nodes.db index ceae74f..e27e392 100644 Binary files a/data/nodes.db and b/data/nodes.db differ diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7ce95ee..1ad01a4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,24 @@ 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.19] - 2025-07-18 + +### Fixed +- **Enhanced node type format normalization** (Issue #74) + - Fixed issue where `n8n-nodes-langchain.chattrigger` (incorrect format) was not being normalized + - Added support for `n8n-nodes-langchain.*` → `nodes-langchain.*` normalization (without @n8n/ prefix) + - Implemented case-insensitive node name matching (e.g., `chattrigger` → `chatTrigger`) + - Added smart camelCase detection for common patterns (trigger, request, sheets, etc.) + - Fixed `get_node_documentation` tool to use same normalization logic as other tools + - All MCP tools now consistently handle various format variations: + - `nodes-langchain.chatTrigger` (correct format) + - `n8n-nodes-langchain.chatTrigger` (package format) + - `n8n-nodes-langchain.chattrigger` (package + wrong case) + - `nodes-langchain.chattrigger` (wrong case only) + - `@n8n/n8n-nodes-langchain.chatTrigger` (full npm format) + - Updated all 7 node lookup locations to use normalized types for alternatives generation + - Enhanced `getNodeTypeAlternatives()` to normalize all generated alternatives + ## [2.7.18] - 2025-07-18 ### Fixed @@ -712,6 +730,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.19]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.18...v2.7.19 [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 diff --git a/package.json b/package.json index 8b5dbe9..42056ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.7.18", + "version": "2.7.19", "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 3015fd4..067163b 100644 --- a/package.runtime.json +++ b/package.runtime.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp-runtime", - "version": "2.7.18", + "version": "2.7.19", "description": "n8n MCP Server Runtime Dependencies Only", "private": true, "dependencies": { diff --git a/src/mcp/server.ts b/src/mcp/server.ts index ccb0334..7d22734 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -354,7 +354,7 @@ export class N8NDocumentationMCPServer { if (!node) { // Fallback to other alternatives for edge cases - const alternatives = getNodeTypeAlternatives(nodeType); + const alternatives = getNodeTypeAlternatives(normalizedType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); @@ -975,6 +975,21 @@ export class N8NDocumentationMCPServer { `).get(nodeType) as NodeRow | undefined; } + // If still not found, try alternatives + if (!node) { + const alternatives = getNodeTypeAlternatives(normalizedType); + + for (const alt of alternatives) { + node = this.db!.prepare(` + SELECT node_type, display_name, documentation, description + FROM nodes + WHERE node_type = ? + `).get(alt) as NodeRow | undefined; + + if (node) break; + } + } + if (!node) { throw new Error(`Node ${nodeType} not found`); } @@ -1072,7 +1087,7 @@ Full documentation is being prepared. For now, use get_node_essentials for confi if (!node) { // Fallback to other alternatives for edge cases - const alternatives = getNodeTypeAlternatives(nodeType); + const alternatives = getNodeTypeAlternatives(normalizedType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); @@ -1146,7 +1161,7 @@ Full documentation is being prepared. For now, use get_node_essentials for confi if (!node) { // Fallback to other alternatives for edge cases - const alternatives = getNodeTypeAlternatives(nodeType); + const alternatives = getNodeTypeAlternatives(normalizedType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); @@ -1304,7 +1319,7 @@ Full documentation is being prepared. For now, use get_node_essentials for confi if (!node) { // Fallback to other alternatives for edge cases - const alternatives = getNodeTypeAlternatives(nodeType); + const alternatives = getNodeTypeAlternatives(normalizedType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); @@ -1362,7 +1377,7 @@ Full documentation is being prepared. For now, use get_node_essentials for confi if (!node) { // Fallback to other alternatives for edge cases - const alternatives = getNodeTypeAlternatives(nodeType); + const alternatives = getNodeTypeAlternatives(normalizedType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); @@ -1416,7 +1431,7 @@ Full documentation is being prepared. For now, use get_node_essentials for confi if (!node) { // Fallback to other alternatives for edge cases - const alternatives = getNodeTypeAlternatives(nodeType); + const alternatives = getNodeTypeAlternatives(normalizedType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); @@ -1593,7 +1608,7 @@ Full documentation is being prepared. For now, use get_node_essentials for confi if (!node) { // Fallback to other alternatives for edge cases - const alternatives = getNodeTypeAlternatives(nodeType); + const alternatives = getNodeTypeAlternatives(normalizedType); for (const alt of alternatives) { const found = this.repository!.getNode(alt); diff --git a/src/utils/node-utils.ts b/src/utils/node-utils.ts index 8ab8073..d6a3d3d 100644 --- a/src/utils/node-utils.ts +++ b/src/utils/node-utils.ts @@ -4,6 +4,7 @@ * Examples: * - 'n8n-nodes-base.httpRequest' → 'nodes-base.httpRequest' * - '@n8n/n8n-nodes-langchain.agent' → 'nodes-langchain.agent' + * - 'n8n-nodes-langchain.chatTrigger' → 'nodes-langchain.chatTrigger' * - 'nodes-base.slack' → 'nodes-base.slack' (unchanged) * * @param nodeType The node type to normalize @@ -20,6 +21,11 @@ export function normalizeNodeType(nodeType: string): string { return nodeType.replace('@n8n/n8n-nodes-langchain.', 'nodes-langchain.'); } + // Handle n8n-nodes-langchain -> nodes-langchain (without @n8n/ prefix) + if (nodeType.startsWith('n8n-nodes-langchain.')) { + return nodeType.replace('n8n-nodes-langchain.', 'nodes-langchain.'); + } + // Return unchanged if already normalized or unknown format return nodeType; } @@ -36,13 +42,86 @@ export function getNodeTypeAlternatives(nodeType: string): string[] { // Add lowercase version alternatives.push(nodeType.toLowerCase()); + // If it has a prefix, try case variations on the node name part + if (nodeType.includes('.')) { + const [prefix, nodeName] = nodeType.split('.'); + + // Try different case variations for the node name + if (nodeName && nodeName.toLowerCase() !== nodeName) { + alternatives.push(`${prefix}.${nodeName.toLowerCase()}`); + } + + // For camelCase names like "chatTrigger", also try with capital first letter variations + // e.g., "chattrigger" -> "chatTrigger" + if (nodeName && nodeName.toLowerCase() === nodeName && nodeName.length > 1) { + // Try to detect common patterns and create camelCase version + const camelCaseVariants = generateCamelCaseVariants(nodeName); + camelCaseVariants.forEach(variant => { + alternatives.push(`${prefix}.${variant}`); + }); + } + } + // 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}`); + + // Also try camelCase variants for bare names + const camelCaseVariants = generateCamelCaseVariants(nodeType); + camelCaseVariants.forEach(variant => { + alternatives.push(`nodes-base.${variant}`); + alternatives.push(`nodes-langchain.${variant}`); + }); } - return alternatives; + // Normalize all alternatives and combine with originals + const normalizedAlternatives = alternatives.map(alt => normalizeNodeType(alt)); + + // Combine original alternatives with normalized ones and remove duplicates + return [...new Set([...alternatives, ...normalizedAlternatives])]; +} + +/** + * Generate camelCase variants for a lowercase string + * @param str The lowercase string + * @returns Array of possible camelCase variants + */ +function generateCamelCaseVariants(str: string): string[] { + const variants: string[] = []; + + // Common patterns for n8n nodes + const patterns = [ + // Pattern: wordTrigger (e.g., chatTrigger, webhookTrigger) + /^(.+)(trigger|node|request|response)$/i, + // Pattern: httpRequest, mysqlDatabase + /^(http|mysql|postgres|mongo|redis|mqtt|smtp|imap|ftp|ssh|api)(.+)$/i, + // Pattern: googleSheets, microsoftTeams + /^(google|microsoft|amazon|slack|discord|telegram)(.+)$/i, + ]; + + for (const pattern of patterns) { + const match = str.toLowerCase().match(pattern); + if (match) { + const [, first, second] = match; + // Capitalize the second part + variants.push(first.toLowerCase() + second.charAt(0).toUpperCase() + second.slice(1).toLowerCase()); + } + } + + // Generic camelCase: capitalize after common word boundaries + if (variants.length === 0) { + // Try splitting on common boundaries and capitalizing + const words = str.split(/[-_\s]+/); + if (words.length > 1) { + const camelCase = words[0].toLowerCase() + words.slice(1).map(w => + w.charAt(0).toUpperCase() + w.slice(1).toLowerCase() + ).join(''); + variants.push(camelCase); + } + } + + return variants; } /**