diff --git a/CLAUDE.md b/CLAUDE.md index 741cde2..a678520 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,20 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co n8n-mcp is a comprehensive documentation and knowledge server that provides AI assistants with complete access to n8n node information through the Model Context Protocol (MCP). It serves as a bridge between n8n's workflow automation platform and AI models, enabling them to understand and work with n8n nodes effectively. -## ✅ Latest Updates (v2.6.1) +## ✅ Latest Updates (v2.6.2) + +### Update (v2.6.2) - Enhanced Workflow Creation Validation: +- ✅ **NEW: Node type validation** - Verifies node types actually exist in n8n +- ✅ **FIXED: nodes-base prefix detection** - Now catches `nodes-base.webhook` BEFORE database lookup +- ✅ **NEW: Smart suggestions** - Detects `nodes-base.webhook` and suggests `n8n-nodes-base.webhook` +- ✅ **NEW: Common mistake detection** - Catches missing package prefixes (e.g., `webhook` → `n8n-nodes-base.webhook`) +- ✅ **NEW: Minimum viable workflow validation** - Prevents single-node workflows (except webhooks) +- ✅ **NEW: Empty connection detection** - Catches multi-node workflows with no connections +- ✅ **Enhanced error messages** - Clear guidance on proper workflow structure +- ✅ **Connection examples** - Shows correct format: `connections: { "Node Name": { "main": [[{ "node": "Target", "type": "main", "index": 0 }]] } }` +- ✅ **Helper functions** - `getWorkflowStructureExample()` and `getWorkflowFixSuggestions()` +- ✅ **Prevents broken workflows** - Like single webhook nodes with empty connections that show as question marks +- ✅ **Reinforces best practices** - Use node NAMES (not IDs) in connections ### Update (v2.6.1) - Enhanced typeVersion Validation: - ✅ **NEW: typeVersion validation** - Workflow validator now enforces typeVersion on all versioned nodes diff --git a/package.json b/package.json index 0336371..e132f72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.6.1", + "version": "2.6.2", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", "scripts": { diff --git a/scripts/test-empty-connection-validation.ts b/scripts/test-empty-connection-validation.ts new file mode 100644 index 0000000..e89e1c4 --- /dev/null +++ b/scripts/test-empty-connection-validation.ts @@ -0,0 +1,157 @@ +#!/usr/bin/env tsx + +/** + * Test script for empty connection validation + * Tests the improvements to prevent broken workflows like the one in the logs + */ + +import { WorkflowValidator } from '../src/services/workflow-validator'; +import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator'; +import { NodeRepository } from '../src/database/node-repository'; +import { createDatabaseAdapter } from '../src/database/database-adapter'; +import { validateWorkflowStructure, getWorkflowFixSuggestions, getWorkflowStructureExample } from '../src/services/n8n-validation'; +import { Logger } from '../src/utils/logger'; + +const logger = new Logger({ prefix: '[TestEmptyConnectionValidation]' }); + +async function testValidation() { + const adapter = await createDatabaseAdapter('./data/nodes.db'); + const repository = new NodeRepository(adapter); + const validator = new WorkflowValidator(repository, EnhancedConfigValidator); + + logger.info('Testing empty connection validation...\n'); + + // Test 1: The broken workflow from the logs + const brokenWorkflow = { + "nodes": [ + { + "parameters": {}, + "id": "webhook_node", + "name": "Webhook", + "type": "nodes-base.webhook", + "typeVersion": 2, + "position": [260, 300] as [number, number] + } + ], + "connections": {}, + "pinData": {}, + "meta": { + "instanceId": "74e11c77e266f2c77f6408eb6c88e3fec63c9a5d8c4a3a2ea4c135c542012d6b" + } + }; + + logger.info('Test 1: Broken single-node workflow with empty connections'); + const result1 = await validator.validateWorkflow(brokenWorkflow as any); + + logger.info('Validation result:'); + logger.info(`Valid: ${result1.valid}`); + logger.info(`Errors: ${result1.errors.length}`); + result1.errors.forEach(err => { + if (typeof err === 'string') { + logger.error(` - ${err}`); + } else if (err && typeof err === 'object' && 'message' in err) { + logger.error(` - ${err.message}`); + } else { + logger.error(` - ${JSON.stringify(err)}`); + } + }); + logger.info(`Warnings: ${result1.warnings.length}`); + result1.warnings.forEach(warn => logger.warn(` - ${warn.message || JSON.stringify(warn)}`)); + logger.info(`Suggestions: ${result1.suggestions.length}`); + result1.suggestions.forEach(sug => logger.info(` - ${sug}`)); + + // Test 2: Multi-node workflow with no connections + const multiNodeNoConnections = { + "name": "Test Workflow", + "nodes": [ + { + "id": "manual-1", + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [250, 300] as [number, number], + "parameters": {} + }, + { + "id": "set-1", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [450, 300] as [number, number], + "parameters": {} + } + ], + "connections": {} + }; + + logger.info('\nTest 2: Multi-node workflow with empty connections'); + const result2 = await validator.validateWorkflow(multiNodeNoConnections as any); + + logger.info('Validation result:'); + logger.info(`Valid: ${result2.valid}`); + logger.info(`Errors: ${result2.errors.length}`); + result2.errors.forEach(err => logger.error(` - ${err.message || JSON.stringify(err)}`)); + logger.info(`Suggestions: ${result2.suggestions.length}`); + result2.suggestions.forEach(sug => logger.info(` - ${sug}`)); + + // Test 3: Using n8n-validation functions + logger.info('\nTest 3: Testing n8n-validation.ts functions'); + + const errors = validateWorkflowStructure(brokenWorkflow as any); + logger.info('Validation errors:'); + errors.forEach(err => logger.error(` - ${err}`)); + + const suggestions = getWorkflowFixSuggestions(errors); + logger.info('Fix suggestions:'); + suggestions.forEach(sug => logger.info(` - ${sug}`)); + + logger.info('\nExample of proper workflow structure:'); + logger.info(getWorkflowStructureExample()); + + // Test 4: Workflow using IDs instead of names in connections + const workflowWithIdConnections = { + "name": "Test Workflow", + "nodes": [ + { + "id": "manual-1", + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [250, 300] as [number, number], + "parameters": {} + }, + { + "id": "set-1", + "name": "Set Data", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [450, 300] as [number, number], + "parameters": {} + } + ], + "connections": { + "manual-1": { // Using ID instead of name! + "main": [[{ + "node": "set-1", // Using ID instead of name! + "type": "main", + "index": 0 + }]] + } + } + }; + + logger.info('\nTest 4: Workflow using IDs instead of names in connections'); + const result4 = await validator.validateWorkflow(workflowWithIdConnections as any); + + logger.info('Validation result:'); + logger.info(`Valid: ${result4.valid}`); + logger.info(`Errors: ${result4.errors.length}`); + result4.errors.forEach(err => logger.error(` - ${err.message || JSON.stringify(err)}`)); + + adapter.close(); +} + +testValidation().catch(err => { + logger.error('Test failed:', err); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/test-node-type-validation.ts b/scripts/test-node-type-validation.ts new file mode 100644 index 0000000..4b52679 --- /dev/null +++ b/scripts/test-node-type-validation.ts @@ -0,0 +1,187 @@ +#!/usr/bin/env tsx + +/** + * Test script for node type validation + * Tests the improvements to catch invalid node types like "nodes-base.webhook" + */ + +import { WorkflowValidator } from '../src/services/workflow-validator'; +import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator'; +import { NodeRepository } from '../src/database/node-repository'; +import { createDatabaseAdapter } from '../src/database/database-adapter'; +import { validateWorkflowStructure } from '../src/services/n8n-validation'; +import { Logger } from '../src/utils/logger'; + +const logger = new Logger({ prefix: '[TestNodeTypeValidation]' }); + +async function testValidation() { + const adapter = await createDatabaseAdapter('./data/nodes.db'); + const repository = new NodeRepository(adapter); + const validator = new WorkflowValidator(repository, EnhancedConfigValidator); + + logger.info('Testing node type validation...\n'); + + // Test 1: The exact broken workflow from Claude Desktop + const brokenWorkflowFromLogs = { + "nodes": [ + { + "parameters": {}, + "id": "webhook_node", + "name": "Webhook", + "type": "nodes-base.webhook", // WRONG! Missing n8n- prefix + "typeVersion": 2, + "position": [260, 300] as [number, number] + } + ], + "connections": {}, + "pinData": {}, + "meta": { + "instanceId": "74e11c77e266f2c77f6408eb6c88e3fec63c9a5d8c4a3a2ea4c135c542012d6b" + } + }; + + logger.info('Test 1: Invalid node type "nodes-base.webhook" (missing n8n- prefix)'); + const result1 = await validator.validateWorkflow(brokenWorkflowFromLogs as any); + + logger.info('Validation result:'); + logger.info(`Valid: ${result1.valid}`); + logger.info(`Errors: ${result1.errors.length}`); + result1.errors.forEach(err => { + if (typeof err === 'string') { + logger.error(` - ${err}`); + } else if (err && typeof err === 'object' && 'message' in err) { + logger.error(` - ${err.message}`); + } + }); + + // Check if the specific error about nodes-base.webhook was caught + const hasNodeBaseError = result1.errors.some(err => + err && typeof err === 'object' && 'message' in err && + err.message.includes('nodes-base.webhook') && + err.message.includes('n8n-nodes-base.webhook') + ); + logger.info(`Caught nodes-base.webhook error: ${hasNodeBaseError ? 'YES ✅' : 'NO ❌'}`); + + // Test 2: Node type without any prefix + const noPrefixWorkflow = { + "name": "Test Workflow", + "nodes": [ + { + "id": "webhook-1", + "name": "My Webhook", + "type": "webhook", // WRONG! No package prefix + "typeVersion": 2, + "position": [250, 300] as [number, number], + "parameters": {} + }, + { + "id": "set-1", + "name": "Set Data", + "type": "set", // WRONG! No package prefix + "typeVersion": 3.4, + "position": [450, 300] as [number, number], + "parameters": {} + } + ], + "connections": { + "My Webhook": { + "main": [[{ + "node": "Set Data", + "type": "main", + "index": 0 + }]] + } + } + }; + + logger.info('\nTest 2: Node types without package prefix ("webhook", "set")'); + const result2 = await validator.validateWorkflow(noPrefixWorkflow as any); + + logger.info('Validation result:'); + logger.info(`Valid: ${result2.valid}`); + logger.info(`Errors: ${result2.errors.length}`); + result2.errors.forEach(err => { + if (typeof err === 'string') { + logger.error(` - ${err}`); + } else if (err && typeof err === 'object' && 'message' in err) { + logger.error(` - ${err.message}`); + } + }); + + // Test 3: Completely invalid node type + const invalidNodeWorkflow = { + "name": "Test Workflow", + "nodes": [ + { + "id": "fake-1", + "name": "Fake Node", + "type": "n8n-nodes-base.fakeNodeThatDoesNotExist", + "typeVersion": 1, + "position": [250, 300] as [number, number], + "parameters": {} + } + ], + "connections": {} + }; + + logger.info('\nTest 3: Completely invalid node type'); + const result3 = await validator.validateWorkflow(invalidNodeWorkflow as any); + + logger.info('Validation result:'); + logger.info(`Valid: ${result3.valid}`); + logger.info(`Errors: ${result3.errors.length}`); + result3.errors.forEach(err => { + if (typeof err === 'string') { + logger.error(` - ${err}`); + } else if (err && typeof err === 'object' && 'message' in err) { + logger.error(` - ${err.message}`); + } + }); + + // Test 4: Using n8n-validation.ts function + logger.info('\nTest 4: Testing n8n-validation.ts with invalid node types'); + + const errors = validateWorkflowStructure(brokenWorkflowFromLogs as any); + logger.info('Validation errors:'); + errors.forEach(err => logger.error(` - ${err}`)); + + // Test 5: Valid workflow (should pass) + const validWorkflow = { + "name": "Valid Webhook Workflow", + "nodes": [ + { + "id": "webhook-1", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", // CORRECT! + "typeVersion": 2, + "position": [250, 300] as [number, number], + "parameters": { + "path": "my-webhook", + "responseMode": "onReceived", + "responseData": "allEntries" + } + } + ], + "connections": {} + }; + + logger.info('\nTest 5: Valid workflow with correct node type'); + const result5 = await validator.validateWorkflow(validWorkflow as any); + + logger.info('Validation result:'); + logger.info(`Valid: ${result5.valid}`); + logger.info(`Errors: ${result5.errors.length}`); + logger.info(`Warnings: ${result5.warnings.length}`); + result5.warnings.forEach(warn => { + if (warn && typeof warn === 'object' && 'message' in warn) { + logger.warn(` - ${warn.message}`); + } + }); + + adapter.close(); +} + +testValidation().catch(err => { + logger.error('Test failed:', err); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/test-nodes-base-prefix.ts b/scripts/test-nodes-base-prefix.ts new file mode 100644 index 0000000..0bc8104 --- /dev/null +++ b/scripts/test-nodes-base-prefix.ts @@ -0,0 +1,100 @@ +#!/usr/bin/env tsx + +/** + * Specific test for nodes-base. prefix validation + */ + +import { WorkflowValidator } from '../src/services/workflow-validator'; +import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator'; +import { NodeRepository } from '../src/database/node-repository'; +import { createDatabaseAdapter } from '../src/database/database-adapter'; +import { Logger } from '../src/utils/logger'; + +const logger = new Logger({ prefix: '[TestNodesBasePrefix]' }); + +async function testValidation() { + const adapter = await createDatabaseAdapter('./data/nodes.db'); + const repository = new NodeRepository(adapter); + const validator = new WorkflowValidator(repository, EnhancedConfigValidator); + + logger.info('Testing nodes-base. prefix validation...\n'); + + // Test various nodes-base. prefixed types + const testCases = [ + { type: 'nodes-base.webhook', expected: 'n8n-nodes-base.webhook' }, + { type: 'nodes-base.httpRequest', expected: 'n8n-nodes-base.httpRequest' }, + { type: 'nodes-base.set', expected: 'n8n-nodes-base.set' }, + { type: 'nodes-base.code', expected: 'n8n-nodes-base.code' }, + { type: 'nodes-base.slack', expected: 'n8n-nodes-base.slack' }, + ]; + + for (const testCase of testCases) { + const workflow = { + name: `Test ${testCase.type}`, + nodes: [{ + id: 'test-node', + name: 'Test Node', + type: testCase.type, + typeVersion: 1, + position: [100, 100] as [number, number], + parameters: {} + }], + connections: {} + }; + + logger.info(`Testing: "${testCase.type}"`); + const result = await validator.validateWorkflow(workflow as any); + + const nodeTypeError = result.errors.find(err => + err && typeof err === 'object' && 'message' in err && + err.message.includes(testCase.type) && + err.message.includes(testCase.expected) + ); + + if (nodeTypeError) { + logger.info(`✅ Caught and suggested: "${testCase.expected}"`); + } else { + logger.error(`❌ Failed to catch invalid type: "${testCase.type}"`); + result.errors.forEach(err => { + if (err && typeof err === 'object' && 'message' in err) { + logger.error(` Error: ${err.message}`); + } + }); + } + } + + // Test that n8n-nodes-base. prefix still works + const validWorkflow = { + name: 'Valid Workflow', + nodes: [{ + id: 'webhook', + name: 'Webhook', + type: 'n8n-nodes-base.webhook', + typeVersion: 2, + position: [100, 100] as [number, number], + parameters: {} + }], + connections: {} + }; + + logger.info('\nTesting valid n8n-nodes-base.webhook:'); + const validResult = await validator.validateWorkflow(validWorkflow as any); + + const hasNodeTypeError = validResult.errors.some(err => + err && typeof err === 'object' && 'message' in err && + err.message.includes('node type') + ); + + if (!hasNodeTypeError) { + logger.info('✅ Correctly accepted n8n-nodes-base.webhook'); + } else { + logger.error('❌ Incorrectly rejected valid n8n-nodes-base.webhook'); + } + + adapter.close(); +} + +testValidation().catch(err => { + logger.error('Test failed:', err); + process.exit(1); +}); \ No newline at end of file diff --git a/src/mcp/server-update.ts b/src/mcp/server-update.ts index af3cb95..9d82272 100644 --- a/src/mcp/server-update.ts +++ b/src/mcp/server-update.ts @@ -1412,6 +1412,16 @@ Full documentation is being prepared. For now, use get_node_essentials for confi "Tags": "Read-only during creation/update" }, workflow_examples: { + "⚠️ CRITICAL: Connection Rules": { + warning: "ALWAYS use node NAMES in connections, NEVER use node IDs!", + explanation: "Using IDs will make nodes appear disconnected in n8n UI", + wrong: { + connections: {"1": {main: [[{node: "2", type: "main", index: 0}]]}} // ❌ WRONG - uses IDs + }, + correct: { + connections: {"Start": {main: [[{node: "Set", type: "main", index: 0}]]}} // ✅ CORRECT - uses names + } + }, "Create Simple Workflow": { tools: ["n8n_create_workflow"], example: { @@ -1420,7 +1430,7 @@ Full documentation is being prepared. For now, use get_node_essentials for confi {id: "1", name: "Start", type: "n8n-nodes-base.start", position: [250, 300]}, {id: "2", name: "Set", type: "n8n-nodes-base.set", position: [450, 300]} ], - connections: {"1": {main: [[{node: "2", type: "main", index: 0}]]}} + connections: {"Start": {main: [[{node: "Set", type: "main", index: 0}]]}} // ✅ Uses node names! } }, "Execute via Webhook": { @@ -1433,6 +1443,7 @@ Full documentation is being prepared. For now, use get_node_essentials for confi } }, best_practices: [ + "⚠️ ALWAYS use node NAMES in connections, NEVER node IDs", "Always use n8n_health_check first to verify connectivity", "Fetch full workflow before updating (n8n_get_workflow)", "Validate workflows before creating (validate_workflow)", diff --git a/src/services/n8n-validation.ts b/src/services/n8n-validation.ts index cc78109..5c473a3 100644 --- a/src/services/n8n-validation.ts +++ b/src/services/n8n-validation.ts @@ -134,11 +134,38 @@ export function validateWorkflowStructure(workflow: Partial): string[] errors.push('Workflow connections are required'); } + // Check for minimum viable workflow + if (workflow.nodes && workflow.nodes.length === 1) { + const singleNode = workflow.nodes[0]; + const isWebhookOnly = singleNode.type === 'n8n-nodes-base.webhook' || + singleNode.type === 'n8n-nodes-base.webhookTrigger'; + + if (!isWebhookOnly) { + errors.push('Single-node workflows are only valid for webhooks. Add at least one more node and connect them. Example: Manual Trigger → Set node'); + } + } + + // Check for empty connections in multi-node workflows + if (workflow.nodes && workflow.nodes.length > 1 && workflow.connections) { + const connectionCount = Object.keys(workflow.connections).length; + + if (connectionCount === 0) { + errors.push('Multi-node workflow has empty connections. Connect nodes like this: connections: { "Node1 Name": { "main": [[{ "node": "Node2 Name", "type": "main", "index": 0 }]] } }'); + } + } + // Validate nodes if (workflow.nodes) { workflow.nodes.forEach((node, index) => { try { validateWorkflowNode(node); + + // Additional check for common node type mistakes + if (node.type.startsWith('nodes-base.')) { + errors.push(`Invalid node type "${node.type}" at index ${index}. Use "n8n-nodes-base.${node.type.substring(11)}" instead.`); + } else if (!node.type.includes('.')) { + errors.push(`Invalid node type "${node.type}" at index ${index}. Node types must include package prefix (e.g., "n8n-nodes-base.webhook").`); + } } catch (error) { errors.push(`Invalid node at index ${index}: ${error instanceof Error ? error.message : 'Unknown error'}`); } @@ -154,19 +181,35 @@ export function validateWorkflowStructure(workflow: Partial): string[] } } - // Validate that all connection references exist + // Validate that all connection references exist and use node NAMES (not IDs) if (workflow.nodes && workflow.connections) { + const nodeNames = new Set(workflow.nodes.map(node => node.name)); const nodeIds = new Set(workflow.nodes.map(node => node.id)); + const nodeIdToName = new Map(workflow.nodes.map(node => [node.id, node.name])); - Object.entries(workflow.connections).forEach(([sourceId, connection]) => { - if (!nodeIds.has(sourceId)) { - errors.push(`Connection references non-existent source node: ${sourceId}`); + Object.entries(workflow.connections).forEach(([sourceName, connection]) => { + // Check if source exists by name (correct) + if (!nodeNames.has(sourceName)) { + // Check if they're using an ID instead of name + if (nodeIds.has(sourceName)) { + const correctName = nodeIdToName.get(sourceName); + errors.push(`Connection uses node ID '${sourceName}' but must use node name '${correctName}'. Change connections.${sourceName} to connections['${correctName}']`); + } else { + errors.push(`Connection references non-existent node: ${sourceName}`); + } } connection.main.forEach((outputs, outputIndex) => { outputs.forEach((target, targetIndex) => { - if (!nodeIds.has(target.node)) { - errors.push(`Connection references non-existent target node: ${target.node} (from ${sourceId}[${outputIndex}][${targetIndex}])`); + // Check if target exists by name (correct) + if (!nodeNames.has(target.node)) { + // Check if they're using an ID instead of name + if (nodeIds.has(target.node)) { + const correctName = nodeIdToName.get(target.node); + errors.push(`Connection target uses node ID '${target.node}' but must use node name '${correctName}' (from ${sourceName}[${outputIndex}][${targetIndex}])`); + } else { + errors.push(`Connection references non-existent target node: ${target.node} (from ${sourceName}[${outputIndex}][${targetIndex}])`); + } } }); }); @@ -204,4 +247,74 @@ export function getWebhookUrl(workflow: Workflow): string | null { // Note: We can't construct the full URL without knowing the n8n instance URL // The caller will need to prepend the base URL return path; +} + +// Helper function to generate proper workflow structure examples +export function getWorkflowStructureExample(): string { + return ` +Minimal Workflow Example: +{ + "name": "My Workflow", + "nodes": [ + { + "id": "manual-trigger-1", + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [250, 300], + "parameters": {} + }, + { + "id": "set-1", + "name": "Set Data", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [450, 300], + "parameters": { + "mode": "manual", + "assignments": { + "assignments": [{ + "id": "1", + "name": "message", + "value": "Hello World", + "type": "string" + }] + } + } + } + ], + "connections": { + "Manual Trigger": { + "main": [[{ + "node": "Set Data", + "type": "main", + "index": 0 + }]] + } + } +} + +IMPORTANT: In connections, use the node NAME (e.g., "Manual Trigger"), NOT the node ID or type!`; +} + +// Helper function to fix common workflow issues +export function getWorkflowFixSuggestions(errors: string[]): string[] { + const suggestions: string[] = []; + + if (errors.some(e => e.includes('empty connections'))) { + suggestions.push('Add connections between your nodes. Each node (except endpoints) should connect to another node.'); + suggestions.push('Connection format: connections: { "Source Node Name": { "main": [[{ "node": "Target Node Name", "type": "main", "index": 0 }]] } }'); + } + + if (errors.some(e => e.includes('Single-node workflows'))) { + suggestions.push('Add at least one more node to process data. Common patterns: Trigger → Process → Output'); + suggestions.push('Examples: Manual Trigger → Set, Webhook → HTTP Request, Schedule Trigger → Database Query'); + } + + if (errors.some(e => e.includes('node ID') && e.includes('instead of node name'))) { + suggestions.push('Replace node IDs with node names in connections. The name is what appears in the node header.'); + suggestions.push('Wrong: connections: { "set-1": {...} }, Right: connections: { "Set Data": {...} }'); + } + + return suggestions; } \ No newline at end of file diff --git a/src/services/workflow-validator.ts b/src/services/workflow-validator.ts index e6786a8..c9f13d0 100644 --- a/src/services/workflow-validator.ts +++ b/src/services/workflow-validator.ts @@ -173,6 +173,39 @@ export class WorkflowValidator { return; } + // Check for minimum viable workflow + if (workflow.nodes.length === 1) { + const singleNode = workflow.nodes[0]; + const normalizedType = singleNode.type.replace('n8n-nodes-base.', 'nodes-base.'); + const isWebhook = normalizedType === 'nodes-base.webhook' || + normalizedType === 'nodes-base.webhookTrigger'; + + if (!isWebhook) { + 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) { + result.warnings.push({ + type: 'warning', + message: 'Webhook node has no connections. Consider adding nodes to process the webhook data.' + }); + } + } + + // Check for empty connections in multi-node workflows + if (workflow.nodes.length > 1) { + const hasEnabledNodes = workflow.nodes.some(n => !n.disabled); + const hasConnections = Object.keys(workflow.connections).length > 0; + + if (hasEnabledNodes && !hasConnections) { + result.errors.push({ + type: 'error', + message: 'Multi-node workflow has no connections. Nodes must be connected to create a workflow. Use connections: { "Source Node Name": { "main": [[{ "node": "Target Node Name", "type": "main", "index": 0 }]] } }' + }); + } + } + // Check for duplicate node names const nodeNames = new Set(); const nodeIds = new Set(); @@ -230,6 +263,19 @@ export class WorkflowValidator { if (node.disabled) continue; try { + // FIRST: Check for common invalid patterns before database lookup + if (node.type.startsWith('nodes-base.')) { + // This is ALWAYS invalid in workflows - must use n8n-nodes-base prefix + const correctType = node.type.replace('nodes-base.', 'n8n-nodes-base.'); + result.errors.push({ + type: 'error', + nodeId: node.id, + nodeName: node.name, + message: `Invalid node type: "${node.type}". Use "${correctType}" instead. Node types in workflows must use the full package name.` + }); + continue; + } + // Get node definition - try multiple formats let nodeInfo = this.nodeRepository.getNode(node.type); @@ -250,11 +296,44 @@ export class WorkflowValidator { } if (!nodeInfo) { + // Check for common mistakes + let suggestion = ''; + + // Missing package prefix + if (node.type.startsWith('nodes-base.')) { + const withPrefix = node.type.replace('nodes-base.', 'n8n-nodes-base.'); + const exists = this.nodeRepository.getNode(withPrefix) || + this.nodeRepository.getNode(withPrefix.replace('n8n-nodes-base.', 'nodes-base.')); + if (exists) { + suggestion = ` Did you mean "n8n-nodes-base.${node.type.substring(11)}"?`; + } + } + // Check if it's just the node name without package + else if (!node.type.includes('.')) { + // Try common node names + const commonNodes = [ + 'webhook', 'httpRequest', 'set', 'code', 'manualTrigger', + 'scheduleTrigger', 'emailSend', 'slack', 'discord' + ]; + + if (commonNodes.includes(node.type)) { + suggestion = ` Did you mean "n8n-nodes-base.${node.type}"?`; + } + } + + // If no specific suggestion, try to find similar nodes + if (!suggestion) { + const similarNodes = this.findSimilarNodeTypes(node.type); + if (similarNodes.length > 0) { + suggestion = ` Did you mean: ${similarNodes.map(n => `"${n}"`).join(', ')}?`; + } + } + result.errors.push({ type: 'error', nodeId: node.id, nodeName: node.name, - message: `Unknown node type: ${node.type}` + message: `Unknown node type: "${node.type}".${suggestion} Node types must include the package prefix (e.g., "n8n-nodes-base.webhook", not "webhook" or "nodes-base.webhook").` }); continue; } @@ -346,16 +425,28 @@ export class WorkflowValidator { result: WorkflowValidationResult ): void { const nodeMap = new Map(workflow.nodes.map(n => [n.name, n])); + const nodeIdMap = new Map(workflow.nodes.map(n => [n.id, n])); // Check all connections for (const [sourceName, outputs] of Object.entries(workflow.connections)) { const sourceNode = nodeMap.get(sourceName); if (!sourceNode) { - result.errors.push({ - type: 'error', - message: `Connection from non-existent node: "${sourceName}"` - }); + // Check if this is an ID being used instead of a name + const nodeById = nodeIdMap.get(sourceName); + if (nodeById) { + result.errors.push({ + type: 'error', + nodeId: nodeById.id, + nodeName: nodeById.name, + message: `Connection uses node ID '${sourceName}' instead of node name '${nodeById.name}'. In n8n, connections must use node names, not IDs.` + }); + } else { + result.errors.push({ + type: 'error', + message: `Connection from non-existent node: "${sourceName}"` + }); + } result.statistics.invalidConnections++; continue; } @@ -366,6 +457,7 @@ export class WorkflowValidator { sourceName, outputs.main, nodeMap, + nodeIdMap, result, 'main' ); @@ -377,6 +469,7 @@ export class WorkflowValidator { sourceName, outputs.error, nodeMap, + nodeIdMap, result, 'error' ); @@ -388,6 +481,7 @@ export class WorkflowValidator { sourceName, outputs.ai_tool, nodeMap, + nodeIdMap, result, 'ai_tool' ); @@ -456,6 +550,7 @@ export class WorkflowValidator { sourceName: string, outputs: Array>, nodeMap: Map, + nodeIdMap: Map, result: WorkflowValidationResult, outputType: 'main' | 'error' | 'ai_tool' ): void { @@ -466,10 +561,21 @@ export class WorkflowValidator { const targetNode = nodeMap.get(connection.node); if (!targetNode) { - result.errors.push({ - type: 'error', - message: `Connection to non-existent node: "${connection.node}" from "${sourceName}"` - }); + // Check if this is an ID being used instead of a name + const nodeById = nodeIdMap.get(connection.node); + if (nodeById) { + result.errors.push({ + type: 'error', + nodeId: nodeById.id, + nodeName: nodeById.name, + message: `Connection target uses node ID '${connection.node}' instead of node name '${nodeById.name}' (from ${sourceName}). In n8n, connections must use node names, not IDs.` + }); + } else { + result.errors.push({ + type: 'error', + message: `Connection to non-existent node: "${connection.node}" from "${sourceName}"` + }); + } result.statistics.invalidConnections++; } else if (targetNode.disabled) { result.warnings.push({ @@ -772,6 +878,66 @@ export class WorkflowValidator { return maxChain; } + /** + * Find similar node types for suggestions + */ + private findSimilarNodeTypes(invalidType: string): string[] { + // Since we don't have a method to list all nodes, we'll use a predefined list + // of common node types that users might be looking for + const suggestions: string[] = []; + const nodeName = invalidType.includes('.') ? invalidType.split('.').pop()! : invalidType; + + const commonNodeMappings: Record = { + 'webhook': ['nodes-base.webhook'], + 'httpRequest': ['nodes-base.httpRequest'], + 'http': ['nodes-base.httpRequest'], + 'set': ['nodes-base.set'], + 'code': ['nodes-base.code'], + 'manualTrigger': ['nodes-base.manualTrigger'], + 'manual': ['nodes-base.manualTrigger'], + 'scheduleTrigger': ['nodes-base.scheduleTrigger'], + 'schedule': ['nodes-base.scheduleTrigger'], + 'cron': ['nodes-base.scheduleTrigger'], + 'emailSend': ['nodes-base.emailSend'], + 'email': ['nodes-base.emailSend'], + 'slack': ['nodes-base.slack'], + 'discord': ['nodes-base.discord'], + 'postgres': ['nodes-base.postgres'], + 'mysql': ['nodes-base.mySql'], + 'mongodb': ['nodes-base.mongoDb'], + 'redis': ['nodes-base.redis'], + 'if': ['nodes-base.if'], + 'switch': ['nodes-base.switch'], + 'merge': ['nodes-base.merge'], + 'splitInBatches': ['nodes-base.splitInBatches'], + 'loop': ['nodes-base.splitInBatches'], + 'googleSheets': ['nodes-base.googleSheets'], + 'sheets': ['nodes-base.googleSheets'], + 'airtable': ['nodes-base.airtable'], + 'github': ['nodes-base.github'], + 'git': ['nodes-base.github'], + }; + + // Check for exact match + const lowerNodeName = nodeName.toLowerCase(); + if (commonNodeMappings[lowerNodeName]) { + suggestions.push(...commonNodeMappings[lowerNodeName]); + } + + // Check for partial matches + Object.entries(commonNodeMappings).forEach(([key, values]) => { + if (key.includes(lowerNodeName) || lowerNodeName.includes(key)) { + values.forEach(v => { + if (!suggestions.includes(v)) { + suggestions.push(v); + } + }); + } + }); + + return suggestions.slice(0, 3); // Return top 3 suggestions + } + /** * Generate suggestions based on validation results */ @@ -786,6 +952,24 @@ export class WorkflowValidator { ); } + // Suggest proper connection structure for workflows with connection errors + const hasConnectionErrors = result.errors.some(e => + e.message && ( + e.message.includes('connection') || + e.message.includes('Connection') || + e.message.includes('Multi-node workflow has no connections') + ) + ); + + if (hasConnectionErrors) { + result.suggestions.push( + 'Example connection structure: connections: { "Manual Trigger": { "main": [[{ "node": "Set", "type": "main", "index": 0 }]] } }' + ); + result.suggestions.push( + 'Remember: Use node NAMES (not IDs) in connections. The name is what you see in the UI, not the node type.' + ); + } + // Suggest error handling if (!Object.values(workflow.connections).some(o => o.error)) { result.suggestions.push( @@ -812,5 +996,12 @@ export class WorkflowValidator { 'Consider using a Code node for complex data transformations instead of multiple expressions' ); } + + // Suggest minimum workflow structure + if (workflow.nodes.length === 1 && Object.keys(workflow.connections).length === 0) { + result.suggestions.push( + 'A minimal workflow needs: 1) A trigger node (e.g., Manual Trigger), 2) An action node (e.g., Set, HTTP Request), 3) A connection between them' + ); + } } } \ No newline at end of file