feat: implement comprehensive workflow validation (v2.5.0)

Major Features:
- Add ExpressionValidator for n8n expression syntax validation
- Add WorkflowValidator for complete workflow structure validation
- Add three new MCP tools: validate_workflow, validate_workflow_connections, validate_workflow_expressions

Validation Capabilities:
-  Detects workflow cycles (infinite loops)
-  Validates n8n expressions with syntax checking
-  Checks node references in expressions
-  Identifies orphaned nodes and missing connections
-  Supports multiple node type formats (n8n-nodes-base, @n8n/n8n-nodes-langchain)
-  Provides actionable error messages and suggestions

Testing & Analysis:
- Add test scripts for workflow validation
- Add template validation testing
- Add validation summary analysis tool
- Fixed expression validation false positives
- Handle node type normalization correctly

Results from testing 50 real n8n templates:
- 70.9% of errors are from informal sticky notes
- Expression validation catches real syntax issues
- Cycle detection prevents runtime infinite loops
- Successfully validates both core and LangChain nodes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-06-24 14:45:36 +02:00
parent 42a24278db
commit 533b1acc20
13 changed files with 2140 additions and 2 deletions

View File

@@ -19,6 +19,7 @@ import { EnhancedConfigValidator, ValidationMode, ValidationProfile } from '../s
import { PropertyDependencies } from '../services/property-dependencies';
import { SimpleCache } from '../utils/simple-cache';
import { TemplateService } from '../templates/template-service';
import { WorkflowValidator } from '../services/workflow-validator';
interface NodeRow {
node_type: string;
@@ -202,6 +203,12 @@ export class N8NDocumentationMCPServer {
return this.searchTemplates(args.query, args.limit);
case 'get_templates_for_task':
return this.getTemplatesForTask(args.task);
case 'validate_workflow':
return this.validateWorkflow(args.workflow, args.options);
case 'validate_workflow_connections':
return this.validateWorkflowConnections(args.workflow);
case 'validate_workflow_expressions':
return this.validateWorkflowExpressions(args.workflow);
default:
throw new Error(`Unknown tool: ${name}`);
}
@@ -1149,6 +1156,203 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
return descriptions[task] || 'Workflow templates for this task';
}
private async validateWorkflow(workflow: any, options?: any): Promise<any> {
await this.ensureInitialized();
if (!this.repository) throw new Error('Repository not initialized');
// Create workflow validator instance
const validator = new WorkflowValidator(
this.repository,
EnhancedConfigValidator
);
try {
const result = await validator.validateWorkflow(workflow, options);
// Format the response for better readability
const response: any = {
valid: result.valid,
summary: {
totalNodes: result.statistics.totalNodes,
enabledNodes: result.statistics.enabledNodes,
triggerNodes: result.statistics.triggerNodes,
validConnections: result.statistics.validConnections,
invalidConnections: result.statistics.invalidConnections,
expressionsValidated: result.statistics.expressionsValidated,
errorCount: result.errors.length,
warningCount: result.warnings.length
}
};
if (result.errors.length > 0) {
response.errors = result.errors.map(e => ({
node: e.nodeName || 'workflow',
message: e.message,
details: e.details
}));
}
if (result.warnings.length > 0) {
response.warnings = result.warnings.map(w => ({
node: w.nodeName || 'workflow',
message: w.message,
details: w.details
}));
}
if (result.suggestions.length > 0) {
response.suggestions = result.suggestions;
}
return response;
} catch (error) {
logger.error('Error validating workflow:', error);
return {
valid: false,
error: error instanceof Error ? error.message : 'Unknown error validating workflow',
tip: 'Ensure the workflow JSON includes nodes array and connections object'
};
}
}
private async validateWorkflowConnections(workflow: any): Promise<any> {
await this.ensureInitialized();
if (!this.repository) throw new Error('Repository not initialized');
// Create workflow validator instance
const validator = new WorkflowValidator(
this.repository,
EnhancedConfigValidator
);
try {
// Validate only connections
const result = await validator.validateWorkflow(workflow, {
validateNodes: false,
validateConnections: true,
validateExpressions: false
});
const response: any = {
valid: result.errors.length === 0,
statistics: {
totalNodes: result.statistics.totalNodes,
triggerNodes: result.statistics.triggerNodes,
validConnections: result.statistics.validConnections,
invalidConnections: result.statistics.invalidConnections
}
};
// Filter to only connection-related issues
const connectionErrors = result.errors.filter(e =>
e.message.includes('connection') ||
e.message.includes('cycle') ||
e.message.includes('orphaned')
);
const connectionWarnings = result.warnings.filter(w =>
w.message.includes('connection') ||
w.message.includes('orphaned') ||
w.message.includes('trigger')
);
if (connectionErrors.length > 0) {
response.errors = connectionErrors.map(e => ({
node: e.nodeName || 'workflow',
message: e.message
}));
}
if (connectionWarnings.length > 0) {
response.warnings = connectionWarnings.map(w => ({
node: w.nodeName || 'workflow',
message: w.message
}));
}
return response;
} catch (error) {
logger.error('Error validating workflow connections:', error);
return {
valid: false,
error: error instanceof Error ? error.message : 'Unknown error validating connections'
};
}
}
private async validateWorkflowExpressions(workflow: any): Promise<any> {
await this.ensureInitialized();
if (!this.repository) throw new Error('Repository not initialized');
// Create workflow validator instance
const validator = new WorkflowValidator(
this.repository,
EnhancedConfigValidator
);
try {
// Validate only expressions
const result = await validator.validateWorkflow(workflow, {
validateNodes: false,
validateConnections: false,
validateExpressions: true
});
const response: any = {
valid: result.errors.length === 0,
statistics: {
totalNodes: result.statistics.totalNodes,
expressionsValidated: result.statistics.expressionsValidated
}
};
// Filter to only expression-related issues
const expressionErrors = result.errors.filter(e =>
e.message.includes('Expression') ||
e.message.includes('$') ||
e.message.includes('{{')
);
const expressionWarnings = result.warnings.filter(w =>
w.message.includes('Expression') ||
w.message.includes('$') ||
w.message.includes('{{')
);
if (expressionErrors.length > 0) {
response.errors = expressionErrors.map(e => ({
node: e.nodeName || 'workflow',
message: e.message
}));
}
if (expressionWarnings.length > 0) {
response.warnings = expressionWarnings.map(w => ({
node: w.nodeName || 'workflow',
message: w.message
}));
}
// Add tips for common expression issues
if (expressionErrors.length > 0 || expressionWarnings.length > 0) {
response.tips = [
'Use {{ }} to wrap expressions',
'Reference data with $json.propertyName',
'Reference other nodes with $node["Node Name"].json',
'Use $input.item for input data in loops'
];
}
return response;
} catch (error) {
logger.error('Error validating workflow expressions:', error);
return {
valid: false,
error: error instanceof Error ? error.message : 'Unknown error validating expressions'
};
}
}
async run(): Promise<void> {
// Ensure database is initialized before starting server
await this.ensureInitialized();

View File

@@ -318,6 +318,75 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
required: ['task'],
},
},
{
name: 'validate_workflow',
description: `Validate an entire n8n workflow before deployment. Checks: workflow structure, node connections, expressions, best practices, and more. Returns comprehensive validation report with errors, warnings, and suggestions. Essential for AI agents building complete workflows. Prevents common workflow errors before they happen.`,
inputSchema: {
type: 'object',
properties: {
workflow: {
type: 'object',
description: 'The complete workflow JSON to validate. Must include nodes array and connections object.',
},
options: {
type: 'object',
properties: {
validateNodes: {
type: 'boolean',
description: 'Validate individual node configurations. Default true.',
default: true,
},
validateConnections: {
type: 'boolean',
description: 'Validate node connections and flow. Default true.',
default: true,
},
validateExpressions: {
type: 'boolean',
description: 'Validate n8n expressions syntax and references. Default true.',
default: true,
},
profile: {
type: 'string',
enum: ['minimal', 'runtime', 'ai-friendly', 'strict'],
description: 'Validation profile for node validation. Default "runtime".',
default: 'runtime',
},
},
description: 'Optional validation settings',
},
},
required: ['workflow'],
},
},
{
name: 'validate_workflow_connections',
description: `Validate only the connections in a workflow. Checks: all connections point to existing nodes, no cycles (infinite loops), no orphaned nodes, proper trigger node setup. Faster than full validation when you only need to check workflow structure.`,
inputSchema: {
type: 'object',
properties: {
workflow: {
type: 'object',
description: 'The workflow JSON with nodes array and connections object.',
},
},
required: ['workflow'],
},
},
{
name: 'validate_workflow_expressions',
description: `Validate all n8n expressions in a workflow. Checks: expression syntax ({{ }}), variable references ($json, $node, $input), node references exist, context availability. Returns specific errors with locations. Use this to catch expression errors before runtime.`,
inputSchema: {
type: 'object',
properties: {
workflow: {
type: 'object',
description: 'The workflow JSON to check for expression errors.',
},
},
required: ['workflow'],
},
},
];
/**