From f76e2247f9a51dfde56c329a1a29ef422392ab63 Mon Sep 17 00:00:00 2001 From: czlonkowski <56956555+czlonkowski@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:25:20 +0200 Subject: [PATCH] fix: enhance node type format normalization for better AI agent compatibility (Issue #74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added support for n8n-nodes-langchain.* → nodes-langchain.* normalization - 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 - Updated all 7 node lookup locations to use normalized types for alternatives - Enhanced getNodeTypeAlternatives() to normalize all generated alternatives 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) Bump version to 2.7.19 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 2 +- data/nodes.db | Bin 26120192 -> 26120192 bytes docs/CHANGELOG.md | 19 ++++++++++ package.json | 2 +- package.runtime.json | 2 +- src/mcp/server.ts | 29 ++++++++++---- src/utils/node-utils.ts | 81 +++++++++++++++++++++++++++++++++++++++- 7 files changed, 124 insertions(+), 11 deletions(-) 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 ceae74f33f76711b2cea7b2dfc885d06666ed444..e27e392b2770f709acc5e4fbc88c9750e080dd09 100644 GIT binary patch delta 1935 zcmY+^cW@MS7{>A4yKq2qkPC1jBtQa$-a_vXdgu^(e@FscC`srYLV4*VphzqTv3IOU zL?tMSioKu|6|iF=M6rQ_&jbJ9&V0Tz@BV(fv$M0axrGP)xwJhlD%Ejf&O1(Y1IKZi zI*xm6N0U}=>S`EVgU8@Cgc-^he1@`ya)$DTa6^Q_Z>V6XXoxff3{i$khRTL2hM*zZ zP}LA)sAh;Y#2Km^Y8Yx7;tdIgT87$&I)+3;T|+%XeM197Lqj7&V?z@|Q$sUDb3>A$ zg`uUPm7%qvjiIffouR#Il^WEgB1Vi;-|W=J&*H;gchG>kHgHjFWhHHyr3b&6Fha}uGKDPs0>mDD?^l_$}lBW8Lo^_Mk=F} z(aIQQtTIj+?-s{rOmNSg_E+F9+X*;M%?l6hu8Hw z{(VCyy2-w9&4HV?mU{C&g%jOkPvIo@YIEO>8{V{D;@xw$tLrQFot@-fj@*?H7Uuci z^(Ee&N5g#ApW)I?m!0wYBfN3$!l`rTWoBoE&NA_fR0st%vYvK1y^1#q3SW zNprJeW3oB;9Vd94PEjdcqzm~)+`yzO{DzBirYh5v=}Jh+RAwkMm08McB}>Uxa+EpB zTxFgzUs<3mR2C_Vl_knjWtp;ES)r^{Rw=p4YGsYGR#~T*vR>JsY*aQWo0TofR%M%8 LT$GdNro{gTI+LeK delta 1897 zcmXBVcTf~(9LDim#D=Gw1w26o!LBHZa@esWHtY@M0Zu)vsMxT9`m=$cX{4Ayim8T} z7!x#!m&EkmOst8fn;4_%J$WAU$LBlqdw1V=XLfdGi>i*gi)c@D<8+tHd(q{J?C5g2 z5?n5)dT-YxC%qU3#}HzuWe7FYHq=x69}7+@G^7-UE_3^oig3^fch3^$B0j5MSfMj6r# zqYYyWV-4dB;|&>x35JP=NruUWDTb+rX@=>B8HSmLS%yr*Y{MKwmVxgl+mK_(HTVs4 z4f7214Y0ki2Ka{i~Di|c_Hr8*M{ve z{s86C0$NCmXfZ9JrIb(0XgRH*0$NF{Xf>^&wNyy!XgzJ9jkJl1sF*g>7TQWi+h{xO zpq;dfN~n}}QyG=h9@H=y(5-YE?W5c24yvRox|8mryXhXz@jKWN(05MG*lWX;fhCTtTa)YD$SG#B~odwc$F4PloGA9R9Y#m zl^7*fX`{p`@k(2zozh!rN>?R8>85m75|tiGPbEp|rSw*k6`zu#^ildM z{gnR70A-*uNJ&)&D?^l_$}nZPGC~=tq$#76bY-+MMj5M&Q^qS9$^>PiGD(@NOi`vP z)0FAT3}vP=%L&H#Go1@(LS3$i%aw(P;+>3OXlki*AiO$bwv)=`$n}*`-3pwcLEphb z=SX;2)oiD_YL1hrOJS7<$CqkH;$UuRAJTxFgzUkNCA$^vDfvPfC1EK!y!`N}e7 zxw1kjP*y6dl-0@_Wvx=EtW(x28+m!9f4rQmZODR!GmEBIT KCcn%{i}?@Xl%|0I 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; } /**