diff --git a/src/mcp/handlers-n8n-manager.ts b/src/mcp/handlers-n8n-manager.ts index 8e0cf2f..d62152f 100644 --- a/src/mcp/handlers-n8n-manager.ts +++ b/src/mcp/handlers-n8n-manager.ts @@ -750,14 +750,16 @@ export async function handleValidateWorkflow( if (validationResult.errors.length > 0) { response.errors = validationResult.errors.map(e => ({ node: e.nodeName || 'workflow', + nodeName: e.nodeName, // Also set nodeName for compatibility message: e.message, details: e.details })); } - + if (validationResult.warnings.length > 0) { response.warnings = validationResult.warnings.map(w => ({ node: w.nodeName || 'workflow', + nodeName: w.nodeName, // Also set nodeName for compatibility message: w.message, details: w.details })); diff --git a/src/services/universal-expression-validator.ts b/src/services/universal-expression-validator.ts index 6805d1e..fdc0a0b 100644 --- a/src/services/universal-expression-validator.ts +++ b/src/services/universal-expression-validator.ts @@ -21,8 +21,18 @@ export class UniversalExpressionValidator { private static readonly EXPRESSION_PREFIX = '='; /** - * Universal Rule 1: Any field containing {{ }} MUST have = prefix - * This is an absolute rule in n8n - no exceptions + * Universal Rule 1: Any field containing {{ }} MUST have = prefix to be evaluated + * This applies to BOTH pure expressions and mixed content + * + * Examples: + * - "{{ $json.value }}" -> literal text (NOT evaluated) + * - "={{ $json.value }}" -> evaluated expression + * - "Hello {{ $json.name }}!" -> literal text (NOT evaluated) + * - "=Hello {{ $json.name }}!" -> evaluated (expression in mixed content) + * - "=https://api.com/{{ $json.id }}/data" -> evaluated (real example from n8n) + * + * EXCEPTION: Some langchain node fields auto-evaluate without = prefix + * (validated separately by AI-specific validators) */ static validateExpressionPrefix(value: any): UniversalValidationResult { // Only validate strings @@ -53,6 +63,10 @@ export class UniversalExpressionValidator { const hasPrefix = value.startsWith(this.EXPRESSION_PREFIX); const isMixedContent = this.hasMixedContent(value); + // For langchain nodes, we don't validate expression prefixes + // They have AI-specific validators that handle their expression rules + // This is checked at the node level, not here + if (!hasPrefix) { return { isValid: false, diff --git a/src/services/workflow-validator.ts b/src/services/workflow-validator.ts index fc78036..5242877 100644 --- a/src/services/workflow-validator.ts +++ b/src/services/workflow-validator.ts @@ -272,13 +272,15 @@ export class WorkflowValidator { const normalizedType = NodeTypeNormalizer.normalizeToFullForm(singleNode.type); const isWebhook = normalizedType === 'nodes-base.webhook' || normalizedType === 'nodes-base.webhookTrigger'; - - if (!isWebhook) { + const isLangchainNode = normalizedType.startsWith('nodes-langchain.'); + + // Langchain nodes can be validated standalone for AI tool purposes + if (!isWebhook && !isLangchainNode) { result.errors.push({ type: 'error', message: 'Single-node workflows are only valid for webhook endpoints. Add at least one more connected node to create a functional workflow.' }); - } else if (Object.keys(workflow.connections).length === 0) { + } else if (isWebhook && Object.keys(workflow.connections).length === 0) { result.warnings.push({ type: 'warning', message: 'Webhook node has no connections. Consider adding nodes to process the webhook data.' @@ -961,6 +963,13 @@ export class WorkflowValidator { for (const node of workflow.nodes) { if (node.disabled || this.isStickyNote(node)) continue; + // Skip expression validation for langchain nodes + // They have AI-specific validators and different expression rules + const normalizedType = NodeTypeNormalizer.normalizeToFullForm(node.type); + if (normalizedType.startsWith('nodes-langchain.')) { + continue; + } + // Create expression context const context = { availableNodes: nodeNames.filter(n => n !== node.name),