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:
@@ -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();
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user