mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-09 23:03:12 +00:00
fix: resolve node type normalization bug blocking all AI validation (HIGH-01, HIGH-04, HIGH-08)
CRITICAL BUG FIX: NodeTypeNormalizer.normalizeToFullForm() converts TO SHORT form (nodes-langchain.*), but all validation code compared against FULL form (@n8n/n8n-nodes-langchain.*). This caused ALL AI validation to be silently skipped. Impact: - Missing language model detection: NEVER triggered - AI tool connection detection: NEVER triggered - Streaming mode validation: NEVER triggered - AI tool sub-node validation: NEVER triggered ROOT CAUSE: Line 348 in ai-node-validator.ts (and 19 other locations): if (normalizedType === '@n8n/n8n-nodes-langchain.agent') // FULL form But normalizedType is 'nodes-langchain.agent' (SHORT form) Result: Comparison always FALSE, validation never runs FIXES: 1. ai-node-validator.ts (7 locations): - Lines 551, 557, 563: validateAISpecificNodes comparisons - Line 348: checkIfStreamingTarget comparison - Lines 417, 444: validateChatTrigger comparisons - Lines 589-591: hasAINodes array - Lines 606-608, 612: getAINodeCategory comparisons 2. ai-tool-validators.ts (14 locations): - Lines 980-991: AI_TOOL_VALIDATORS keys (13 validators) - Lines 1015-1037: validateAIToolSubNode switch cases (13 cases) 3. ENHANCED streaming validation: - Added validation for AI Agent's own streamResponse setting - Previously only checked streaming FROM Chat Trigger - Now validates BOTH scenarios (lines 259-276) VERIFICATION: - All 25 AI validator unit tests: ✅ PASS - Debug test (missing LM): ✅ PASS - Debug test (AI tools): ✅ PASS - Debug test (streaming): ✅ PASS Resolves: - HIGH-01: Missing language model detection (was never running) - HIGH-04: AI tool connection detection (was never running) - HIGH-08: Streaming mode validation (was never running + incomplete) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -256,15 +256,20 @@ export function validateAIAgent(
|
||||
// 5. Validate streaming mode constraints (CRITICAL)
|
||||
// From spec lines 753-879: AI Agent with streaming MUST NOT have main output connections
|
||||
const isStreamingTarget = checkIfStreamingTarget(node, workflow, reverseConnections);
|
||||
if (isStreamingTarget) {
|
||||
const hasOwnStreamingEnabled = node.parameters?.options?.streamResponse === true;
|
||||
|
||||
if (isStreamingTarget || hasOwnStreamingEnabled) {
|
||||
// Check if AI Agent has any main output connections
|
||||
const agentMainOutput = workflow.connections[node.name]?.main;
|
||||
if (agentMainOutput && agentMainOutput.flat().some((c: any) => c)) {
|
||||
const streamSource = isStreamingTarget
|
||||
? 'connected from Chat Trigger with responseMode="streaming"'
|
||||
: 'has streamResponse=true in options';
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: `AI Agent "${node.name}" is in streaming mode (connected from Chat Trigger with responseMode="streaming") but has outgoing main connections. Remove all main output connections - streaming responses flow back through the Chat Trigger.`,
|
||||
message: `AI Agent "${node.name}" is in streaming mode (${streamSource}) but has outgoing main connections. Remove all main output connections - streaming responses flow back through the Chat Trigger.`,
|
||||
code: 'STREAMING_WITH_MAIN_OUTPUT'
|
||||
});
|
||||
}
|
||||
@@ -345,7 +350,7 @@ function checkIfStreamingTarget(
|
||||
if (!sourceNode) continue;
|
||||
|
||||
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(sourceNode.type);
|
||||
if (normalizedType === '@n8n/n8n-nodes-langchain.chatTrigger') {
|
||||
if (normalizedType === 'nodes-langchain.chatTrigger') {
|
||||
const responseMode = sourceNode.parameters?.options?.responseMode || 'lastNode';
|
||||
if (responseMode === 'streaming') {
|
||||
return true;
|
||||
@@ -409,7 +414,7 @@ export function validateChatTrigger(
|
||||
// Validate streaming mode
|
||||
if (responseMode === 'streaming') {
|
||||
// CRITICAL: Streaming mode only works with AI Agent
|
||||
if (targetType !== '@n8n/n8n-nodes-langchain.agent') {
|
||||
if (targetType !== 'nodes-langchain.agent') {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -436,7 +441,7 @@ export function validateChatTrigger(
|
||||
if (responseMode === 'lastNode') {
|
||||
// lastNode mode requires a workflow that ends somewhere
|
||||
// Just informational - this is the default and works with any workflow
|
||||
if (targetType === '@n8n/n8n-nodes-langchain.agent') {
|
||||
if (targetType === 'nodes-langchain.agent') {
|
||||
issues.push({
|
||||
severity: 'info',
|
||||
nodeId: node.id,
|
||||
@@ -548,19 +553,19 @@ export function validateAISpecificNodes(
|
||||
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(node.type);
|
||||
|
||||
// Validate AI Agent nodes
|
||||
if (normalizedType === '@n8n/n8n-nodes-langchain.agent') {
|
||||
if (normalizedType === 'nodes-langchain.agent') {
|
||||
const nodeIssues = validateAIAgent(node, reverseConnectionMap, workflow);
|
||||
issues.push(...nodeIssues);
|
||||
}
|
||||
|
||||
// Validate Chat Trigger nodes
|
||||
if (normalizedType === '@n8n/n8n-nodes-langchain.chatTrigger') {
|
||||
if (normalizedType === 'nodes-langchain.chatTrigger') {
|
||||
const nodeIssues = validateChatTrigger(node, workflow, reverseConnectionMap);
|
||||
issues.push(...nodeIssues);
|
||||
}
|
||||
|
||||
// Validate Basic LLM Chain nodes
|
||||
if (normalizedType === '@n8n/n8n-nodes-langchain.chainLlm') {
|
||||
if (normalizedType === 'nodes-langchain.chainLlm') {
|
||||
const nodeIssues = validateBasicLLMChain(node, reverseConnectionMap);
|
||||
issues.push(...nodeIssues);
|
||||
}
|
||||
@@ -586,9 +591,9 @@ export function validateAISpecificNodes(
|
||||
*/
|
||||
export function hasAINodes(workflow: WorkflowJson): boolean {
|
||||
const aiNodeTypes = [
|
||||
'@n8n/n8n-nodes-langchain.agent',
|
||||
'@n8n/n8n-nodes-langchain.chatTrigger',
|
||||
'@n8n/n8n-nodes-langchain.chainLlm',
|
||||
'nodes-langchain.agent',
|
||||
'nodes-langchain.chatTrigger',
|
||||
'nodes-langchain.chainLlm',
|
||||
];
|
||||
|
||||
return workflow.nodes.some(node => {
|
||||
@@ -603,13 +608,13 @@ export function hasAINodes(workflow: WorkflowJson): boolean {
|
||||
export function getAINodeCategory(nodeType: string): string | null {
|
||||
const normalized = NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
||||
|
||||
if (normalized === '@n8n/n8n-nodes-langchain.agent') return 'AI Agent';
|
||||
if (normalized === '@n8n/n8n-nodes-langchain.chatTrigger') return 'Chat Trigger';
|
||||
if (normalized === '@n8n/n8n-nodes-langchain.chainLlm') return 'Basic LLM Chain';
|
||||
if (normalized === 'nodes-langchain.agent') return 'AI Agent';
|
||||
if (normalized === 'nodes-langchain.chatTrigger') return 'Chat Trigger';
|
||||
if (normalized === 'nodes-langchain.chainLlm') return 'Basic LLM Chain';
|
||||
if (isAIToolSubNode(normalized)) return 'AI Tool';
|
||||
|
||||
// Check for AI component nodes
|
||||
if (normalized.startsWith('@n8n/n8n-nodes-langchain.')) {
|
||||
if (normalized.startsWith('nodes-langchain.')) {
|
||||
if (normalized.includes('openAi') || normalized.includes('anthropic') || normalized.includes('googleGemini')) {
|
||||
return 'Language Model';
|
||||
}
|
||||
|
||||
@@ -977,18 +977,18 @@ export function validateWolframAlphaTool(node: WorkflowNode): ValidationIssue[]
|
||||
* Helper: Map node types to validator functions
|
||||
*/
|
||||
export const AI_TOOL_VALIDATORS = {
|
||||
'@n8n/n8n-nodes-langchain.toolHttpRequest': validateHTTPRequestTool,
|
||||
'@n8n/n8n-nodes-langchain.toolCode': validateCodeTool,
|
||||
'@n8n/n8n-nodes-langchain.toolVectorStore': validateVectorStoreTool,
|
||||
'@n8n/n8n-nodes-langchain.toolWorkflow': validateWorkflowTool,
|
||||
'@n8n/n8n-nodes-langchain.agentTool': validateAIAgentTool,
|
||||
'@n8n/n8n-nodes-langchain.mcpClientTool': validateMCPClientTool,
|
||||
'@n8n/n8n-nodes-langchain.toolCalculator': validateCalculatorTool,
|
||||
'@n8n/n8n-nodes-langchain.toolThink': validateThinkTool,
|
||||
'@n8n/n8n-nodes-langchain.toolSerpApi': validateSerpApiTool,
|
||||
'@n8n/n8n-nodes-langchain.toolWikipedia': validateWikipediaTool,
|
||||
'@n8n/n8n-nodes-langchain.toolSearXng': validateSearXngTool,
|
||||
'@n8n/n8n-nodes-langchain.toolWolframAlpha': validateWolframAlphaTool,
|
||||
'nodes-langchain.toolHttpRequest': validateHTTPRequestTool,
|
||||
'nodes-langchain.toolCode': validateCodeTool,
|
||||
'nodes-langchain.toolVectorStore': validateVectorStoreTool,
|
||||
'nodes-langchain.toolWorkflow': validateWorkflowTool,
|
||||
'nodes-langchain.agentTool': validateAIAgentTool,
|
||||
'nodes-langchain.mcpClientTool': validateMCPClientTool,
|
||||
'nodes-langchain.toolCalculator': validateCalculatorTool,
|
||||
'nodes-langchain.toolThink': validateThinkTool,
|
||||
'nodes-langchain.toolSerpApi': validateSerpApiTool,
|
||||
'nodes-langchain.toolWikipedia': validateWikipediaTool,
|
||||
'nodes-langchain.toolSearXng': validateSearXngTool,
|
||||
'nodes-langchain.toolWolframAlpha': validateWolframAlphaTool,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -1012,29 +1012,29 @@ export function validateAIToolSubNode(
|
||||
|
||||
// Route to appropriate validator based on node type
|
||||
switch (normalized) {
|
||||
case '@n8n/n8n-nodes-langchain.toolHttpRequest':
|
||||
case 'nodes-langchain.toolHttpRequest':
|
||||
return validateHTTPRequestTool(node);
|
||||
case '@n8n/n8n-nodes-langchain.toolCode':
|
||||
case 'nodes-langchain.toolCode':
|
||||
return validateCodeTool(node);
|
||||
case '@n8n/n8n-nodes-langchain.toolVectorStore':
|
||||
case 'nodes-langchain.toolVectorStore':
|
||||
return validateVectorStoreTool(node, reverseConnections, workflow);
|
||||
case '@n8n/n8n-nodes-langchain.toolWorkflow':
|
||||
case 'nodes-langchain.toolWorkflow':
|
||||
return validateWorkflowTool(node);
|
||||
case '@n8n/n8n-nodes-langchain.agentTool':
|
||||
case 'nodes-langchain.agentTool':
|
||||
return validateAIAgentTool(node, reverseConnections);
|
||||
case '@n8n/n8n-nodes-langchain.mcpClientTool':
|
||||
case 'nodes-langchain.mcpClientTool':
|
||||
return validateMCPClientTool(node);
|
||||
case '@n8n/n8n-nodes-langchain.toolCalculator':
|
||||
case 'nodes-langchain.toolCalculator':
|
||||
return validateCalculatorTool(node);
|
||||
case '@n8n/n8n-nodes-langchain.toolThink':
|
||||
case 'nodes-langchain.toolThink':
|
||||
return validateThinkTool(node);
|
||||
case '@n8n/n8n-nodes-langchain.toolSerpApi':
|
||||
case 'nodes-langchain.toolSerpApi':
|
||||
return validateSerpApiTool(node);
|
||||
case '@n8n/n8n-nodes-langchain.toolWikipedia':
|
||||
case 'nodes-langchain.toolWikipedia':
|
||||
return validateWikipediaTool(node);
|
||||
case '@n8n/n8n-nodes-langchain.toolSearXng':
|
||||
case 'nodes-langchain.toolSearXng':
|
||||
return validateSearXngTool(node);
|
||||
case '@n8n/n8n-nodes-langchain.toolWolframAlpha':
|
||||
case 'nodes-langchain.toolWolframAlpha':
|
||||
return validateWolframAlphaTool(node);
|
||||
default:
|
||||
return [];
|
||||
|
||||
Reference in New Issue
Block a user