Skip to main content
Glama

n8n-MCP

by 88-888
ai-node-validator.ts21.5 kB
/** * AI Node Validator * * Implements validation logic for AI Agent, Chat Trigger, and Basic LLM Chain nodes * from docs/FINAL_AI_VALIDATION_SPEC.md * * Key Features: * - Reverse connection mapping (AI connections flow TO the consumer) * - AI Agent comprehensive validation (prompt types, fallback models, streaming mode) * - Chat Trigger validation (streaming mode constraints) * - Integration with AI tool validators */ import { NodeTypeNormalizer } from '../utils/node-type-normalizer'; import { WorkflowNode, WorkflowJson, ReverseConnection, ValidationIssue, isAIToolSubNode, validateAIToolSubNode } from './ai-tool-validators'; // Re-export types for test files export type { WorkflowNode, WorkflowJson, ReverseConnection, ValidationIssue } from './ai-tool-validators'; // Validation constants const MIN_SYSTEM_MESSAGE_LENGTH = 20; const MAX_ITERATIONS_WARNING_THRESHOLD = 50; /** * AI Connection Types * From spec lines 551-596 */ export const AI_CONNECTION_TYPES = [ 'ai_languageModel', 'ai_memory', 'ai_tool', 'ai_embedding', 'ai_vectorStore', 'ai_document', 'ai_textSplitter', 'ai_outputParser' ] as const; /** * Build Reverse Connection Map * * CRITICAL: AI connections flow TO the consumer node (reversed from standard n8n pattern) * This utility maps which nodes connect TO each node, essential for AI validation. * * From spec lines 551-596 * * @example * Standard n8n: [Source] --main--> [Target] * workflow.connections["Source"]["main"] = [[{node: "Target", ...}]] * * AI pattern: [Language Model] --ai_languageModel--> [AI Agent] * workflow.connections["Language Model"]["ai_languageModel"] = [[{node: "AI Agent", ...}]] * * Reverse map: reverseMap.get("AI Agent") = [{sourceName: "Language Model", type: "ai_languageModel", ...}] */ export function buildReverseConnectionMap( workflow: WorkflowJson ): Map<string, ReverseConnection[]> { const map = new Map<string, ReverseConnection[]>(); // Iterate through all connections for (const [sourceName, outputs] of Object.entries(workflow.connections)) { // Validate source name is not empty if (!sourceName || typeof sourceName !== 'string' || sourceName.trim() === '') { continue; } if (!outputs || typeof outputs !== 'object') continue; // Iterate through all output types (main, error, ai_tool, ai_languageModel, etc.) for (const [outputType, connections] of Object.entries(outputs)) { if (!Array.isArray(connections)) continue; // Flatten nested arrays and process each connection const connArray = connections.flat().filter(c => c); for (const conn of connArray) { if (!conn || !conn.node) continue; // Validate target node name is not empty if (typeof conn.node !== 'string' || conn.node.trim() === '') { continue; } // Initialize array for target node if not exists if (!map.has(conn.node)) { map.set(conn.node, []); } // Add reverse connection entry map.get(conn.node)!.push({ sourceName: sourceName, sourceType: outputType, type: outputType, index: conn.index ?? 0 }); } } } return map; } /** * Get AI connections TO a specific node */ export function getAIConnections( nodeName: string, reverseConnections: Map<string, ReverseConnection[]>, connectionType?: string ): ReverseConnection[] { const incoming = reverseConnections.get(nodeName) || []; if (connectionType) { return incoming.filter(c => c.type === connectionType); } return incoming.filter(c => AI_CONNECTION_TYPES.includes(c.type as any)); } /** * Validate AI Agent Node * From spec lines 3-549 * * Validates: * - Language model connections (1 or 2 if fallback) * - Output parser connection + hasOutputParser flag * - Prompt type configuration (auto vs define) * - System message recommendations * - Streaming mode constraints (CRITICAL) * - Memory connections (0-1) * - Tool connections * - maxIterations validation */ export function validateAIAgent( node: WorkflowNode, reverseConnections: Map<string, ReverseConnection[]>, workflow: WorkflowJson ): ValidationIssue[] { const issues: ValidationIssue[] = []; const incoming = reverseConnections.get(node.name) || []; // 1. Validate language model connections (REQUIRED: 1 or 2 if fallback) const languageModelConnections = incoming.filter(c => c.type === 'ai_languageModel'); if (languageModelConnections.length === 0) { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" requires an ai_languageModel connection. Connect a language model node (e.g., OpenAI Chat Model, Anthropic Chat Model).`, code: 'MISSING_LANGUAGE_MODEL' }); } else if (languageModelConnections.length > 2) { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" has ${languageModelConnections.length} ai_languageModel connections. Maximum is 2 (for fallback model support).`, code: 'TOO_MANY_LANGUAGE_MODELS' }); } else if (languageModelConnections.length === 2) { // Check if fallback is enabled if (!node.parameters.needsFallback) { issues.push({ severity: 'warning', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" has 2 language models but needsFallback is not enabled. Set needsFallback=true or remove the second model.` }); } } else if (languageModelConnections.length === 1 && node.parameters.needsFallback === true) { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" has needsFallback=true but only 1 language model connected. Connect a second model for fallback or disable needsFallback.`, code: 'FALLBACK_MISSING_SECOND_MODEL' }); } // 2. Validate output parser configuration const outputParserConnections = incoming.filter(c => c.type === 'ai_outputParser'); if (node.parameters.hasOutputParser === true) { if (outputParserConnections.length === 0) { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" has hasOutputParser=true but no ai_outputParser connection. Connect an output parser or set hasOutputParser=false.`, code: 'MISSING_OUTPUT_PARSER' }); } } else if (outputParserConnections.length > 0) { issues.push({ severity: 'warning', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" has an output parser connected but hasOutputParser is not true. Set hasOutputParser=true to enable output parsing.` }); } if (outputParserConnections.length > 1) { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" has ${outputParserConnections.length} output parsers. Only 1 is allowed.`, code: 'MULTIPLE_OUTPUT_PARSERS' }); } // 3. Validate prompt type configuration if (node.parameters.promptType === 'define') { if (!node.parameters.text || node.parameters.text.trim() === '') { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" has promptType="define" but the text field is empty. Provide a custom prompt or switch to promptType="auto".`, code: 'MISSING_PROMPT_TEXT' }); } } // 4. Check system message (RECOMMENDED) if (!node.parameters.systemMessage) { issues.push({ severity: 'info', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" has no systemMessage. Consider adding one to define the agent's role, capabilities, and constraints.` }); } else if (node.parameters.systemMessage.trim().length < MIN_SYSTEM_MESSAGE_LENGTH) { issues.push({ severity: 'info', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" systemMessage is very short (minimum ${MIN_SYSTEM_MESSAGE_LENGTH} characters recommended). Provide more detail about the agent's role and capabilities.` }); } // 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); 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 (${streamSource}) but has outgoing main connections. Remove all main output connections - streaming responses flow back through the Chat Trigger.`, code: 'STREAMING_WITH_MAIN_OUTPUT' }); } } // 6. Validate memory connections (0-1 allowed) const memoryConnections = incoming.filter(c => c.type === 'ai_memory'); if (memoryConnections.length > 1) { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" has ${memoryConnections.length} ai_memory connections. Only 1 memory is allowed.`, code: 'MULTIPLE_MEMORY_CONNECTIONS' }); } // 7. Validate tool connections const toolConnections = incoming.filter(c => c.type === 'ai_tool'); if (toolConnections.length === 0) { issues.push({ severity: 'info', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" has no ai_tool connections. Consider adding tools to enhance the agent's capabilities.` }); } // 8. Validate maxIterations if specified if (node.parameters.maxIterations !== undefined) { if (typeof node.parameters.maxIterations !== 'number') { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" has invalid maxIterations type. Must be a number.`, code: 'INVALID_MAX_ITERATIONS_TYPE' }); } else if (node.parameters.maxIterations < 1) { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" has maxIterations=${node.parameters.maxIterations}. Must be at least 1.`, code: 'MAX_ITERATIONS_TOO_LOW' }); } else if (node.parameters.maxIterations > MAX_ITERATIONS_WARNING_THRESHOLD) { issues.push({ severity: 'warning', nodeId: node.id, nodeName: node.name, message: `AI Agent "${node.name}" has maxIterations=${node.parameters.maxIterations}. Very high iteration counts (>${MAX_ITERATIONS_WARNING_THRESHOLD}) may cause long execution times and high costs.` }); } } return issues; } /** * Check if AI Agent is a streaming target * Helper function to determine if an AI Agent is receiving streaming input from Chat Trigger */ function checkIfStreamingTarget( node: WorkflowNode, workflow: WorkflowJson, reverseConnections: Map<string, ReverseConnection[]> ): boolean { const incoming = reverseConnections.get(node.name) || []; // Check if any incoming main connection is from a Chat Trigger with streaming enabled const mainConnections = incoming.filter(c => c.type === 'main'); for (const conn of mainConnections) { const sourceNode = workflow.nodes.find(n => n.name === conn.sourceName); if (!sourceNode) continue; const normalizedType = NodeTypeNormalizer.normalizeToFullForm(sourceNode.type); if (normalizedType === 'nodes-langchain.chatTrigger') { const responseMode = sourceNode.parameters?.options?.responseMode || 'lastNode'; if (responseMode === 'streaming') { return true; } } } return false; } /** * Validate Chat Trigger Node * From spec lines 753-879 * * Critical validations: * - responseMode="streaming" requires AI Agent target * - AI Agent with streaming MUST NOT have main output connections * - responseMode="lastNode" validation */ export function validateChatTrigger( node: WorkflowNode, workflow: WorkflowJson, reverseConnections: Map<string, ReverseConnection[]> ): ValidationIssue[] { const issues: ValidationIssue[] = []; const responseMode = node.parameters?.options?.responseMode || 'lastNode'; // Get outgoing main connections from Chat Trigger const outgoingMain = workflow.connections[node.name]?.main; if (!outgoingMain || outgoingMain.length === 0 || !outgoingMain[0] || outgoingMain[0].length === 0) { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `Chat Trigger "${node.name}" has no outgoing connections. Connect it to an AI Agent or workflow.`, code: 'MISSING_CONNECTIONS' }); return issues; } const firstConnection = outgoingMain[0][0]; if (!firstConnection) { return issues; } const targetNode = workflow.nodes.find(n => n.name === firstConnection.node); if (!targetNode) { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `Chat Trigger "${node.name}" connects to non-existent node "${firstConnection.node}".`, code: 'INVALID_TARGET_NODE' }); return issues; } const targetType = NodeTypeNormalizer.normalizeToFullForm(targetNode.type); // Validate streaming mode if (responseMode === 'streaming') { // CRITICAL: Streaming mode only works with AI Agent if (targetType !== 'nodes-langchain.agent') { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `Chat Trigger "${node.name}" has responseMode="streaming" but connects to "${targetNode.name}" (${targetType}). Streaming mode only works with AI Agent. Change responseMode to "lastNode" or connect to an AI Agent.`, code: 'STREAMING_WRONG_TARGET' }); } else { // CRITICAL: Check AI Agent has NO main output connections const agentMainOutput = workflow.connections[targetNode.name]?.main; if (agentMainOutput && agentMainOutput.flat().some((c: any) => c)) { issues.push({ severity: 'error', nodeId: targetNode.id, nodeName: targetNode.name, message: `AI Agent "${targetNode.name}" is in streaming mode but has outgoing main connections. In streaming mode, the AI Agent must NOT have main output connections - responses stream back through the Chat Trigger.`, code: 'STREAMING_AGENT_HAS_OUTPUT' }); } } } // Validate lastNode mode if (responseMode === 'lastNode') { // lastNode mode requires a workflow that ends somewhere // Just informational - this is the default and works with any workflow if (targetType === 'nodes-langchain.agent') { issues.push({ severity: 'info', nodeId: node.id, nodeName: node.name, message: `Chat Trigger "${node.name}" uses responseMode="lastNode" with AI Agent. Consider using responseMode="streaming" for better user experience with real-time responses.` }); } } return issues; } /** * Validate Basic LLM Chain Node * From spec - simplified AI chain without agent loop * * Similar to AI Agent but simpler: * - Requires exactly 1 language model * - Can have 0-1 memory * - No tools (not an agent) * - No fallback model support */ export function validateBasicLLMChain( node: WorkflowNode, reverseConnections: Map<string, ReverseConnection[]> ): ValidationIssue[] { const issues: ValidationIssue[] = []; const incoming = reverseConnections.get(node.name) || []; // 1. Validate language model connection (REQUIRED: exactly 1) const languageModelConnections = incoming.filter(c => c.type === 'ai_languageModel'); if (languageModelConnections.length === 0) { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `Basic LLM Chain "${node.name}" requires an ai_languageModel connection. Connect a language model node.`, code: 'MISSING_LANGUAGE_MODEL' }); } else if (languageModelConnections.length > 1) { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `Basic LLM Chain "${node.name}" has ${languageModelConnections.length} ai_languageModel connections. Basic LLM Chain only supports 1 language model (no fallback).`, code: 'MULTIPLE_LANGUAGE_MODELS' }); } // 2. Validate memory connections (0-1 allowed) const memoryConnections = incoming.filter(c => c.type === 'ai_memory'); if (memoryConnections.length > 1) { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `Basic LLM Chain "${node.name}" has ${memoryConnections.length} ai_memory connections. Only 1 memory is allowed.`, code: 'MULTIPLE_MEMORY_CONNECTIONS' }); } // 3. Check for tool connections (not supported) const toolConnections = incoming.filter(c => c.type === 'ai_tool'); if (toolConnections.length > 0) { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `Basic LLM Chain "${node.name}" has ai_tool connections. Basic LLM Chain does not support tools. Use AI Agent if you need tool support.`, code: 'TOOLS_NOT_SUPPORTED' }); } // 4. Validate prompt configuration if (node.parameters.promptType === 'define') { if (!node.parameters.text || node.parameters.text.trim() === '') { issues.push({ severity: 'error', nodeId: node.id, nodeName: node.name, message: `Basic LLM Chain "${node.name}" has promptType="define" but the text field is empty.`, code: 'MISSING_PROMPT_TEXT' }); } } return issues; } /** * Validate all AI-specific nodes in a workflow * * This is the main entry point called by WorkflowValidator */ export function validateAISpecificNodes( workflow: WorkflowJson ): ValidationIssue[] { const issues: ValidationIssue[] = []; // Build reverse connection map (critical for AI validation) const reverseConnectionMap = buildReverseConnectionMap(workflow); for (const node of workflow.nodes) { if (node.disabled) continue; const normalizedType = NodeTypeNormalizer.normalizeToFullForm(node.type); // Validate AI Agent nodes if (normalizedType === 'nodes-langchain.agent') { const nodeIssues = validateAIAgent(node, reverseConnectionMap, workflow); issues.push(...nodeIssues); } // Validate Chat Trigger nodes if (normalizedType === 'nodes-langchain.chatTrigger') { const nodeIssues = validateChatTrigger(node, workflow, reverseConnectionMap); issues.push(...nodeIssues); } // Validate Basic LLM Chain nodes if (normalizedType === 'nodes-langchain.chainLlm') { const nodeIssues = validateBasicLLMChain(node, reverseConnectionMap); issues.push(...nodeIssues); } // Validate AI tool sub-nodes (13 types) if (isAIToolSubNode(normalizedType)) { const nodeIssues = validateAIToolSubNode( node, normalizedType, reverseConnectionMap, workflow ); issues.push(...nodeIssues); } } return issues; } /** * Check if a workflow contains any AI nodes * Useful for skipping AI validation when not needed */ export function hasAINodes(workflow: WorkflowJson): boolean { const aiNodeTypes = [ 'nodes-langchain.agent', 'nodes-langchain.chatTrigger', 'nodes-langchain.chainLlm', ]; return workflow.nodes.some(node => { const normalized = NodeTypeNormalizer.normalizeToFullForm(node.type); return aiNodeTypes.includes(normalized) || isAIToolSubNode(normalized); }); } /** * Helper: Get AI node type category */ export function getAINodeCategory(nodeType: string): string | null { const normalized = NodeTypeNormalizer.normalizeToFullForm(nodeType); 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('nodes-langchain.')) { if (normalized.includes('openAi') || normalized.includes('anthropic') || normalized.includes('googleGemini')) { return 'Language Model'; } if (normalized.includes('memory') || normalized.includes('buffer')) { return 'Memory'; } if (normalized.includes('vectorStore') || normalized.includes('pinecone') || normalized.includes('qdrant')) { return 'Vector Store'; } if (normalized.includes('embedding')) { return 'Embeddings'; } return 'AI Component'; } return null; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/88-888/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server