Skip to main content
Glama

n8n-MCP

by 88-888
create-workflow.test.tsโ€ข20.3 kB
/** * Integration Tests: handleCreateWorkflow * * Tests workflow creation against a real n8n instance. * Verifies the P0 bug fix (FULL vs SHORT node type formats) * and covers all major workflow creation scenarios. */ import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest'; import { createTestContext, TestContext, createTestWorkflowName } from '../utils/test-context'; import { getTestN8nClient } from '../utils/n8n-client'; import { N8nApiClient } from '../../../../src/services/n8n-api-client'; import { Workflow } from '../../../../src/types/n8n-api'; import { SIMPLE_WEBHOOK_WORKFLOW, SIMPLE_HTTP_WORKFLOW, MULTI_NODE_WORKFLOW, ERROR_HANDLING_WORKFLOW, AI_AGENT_WORKFLOW, EXPRESSION_WORKFLOW, getFixture } from '../utils/fixtures'; import { cleanupOrphanedWorkflows } from '../utils/cleanup-helpers'; import { createMcpContext } from '../utils/mcp-context'; import { InstanceContext } from '../../../../src/types/instance-context'; import { handleCreateWorkflow } from '../../../../src/mcp/handlers-n8n-manager'; describe('Integration: handleCreateWorkflow', () => { let context: TestContext; let client: N8nApiClient; let mcpContext: InstanceContext; beforeEach(() => { context = createTestContext(); client = getTestN8nClient(); mcpContext = createMcpContext(); }); afterEach(async () => { await context.cleanup(); }); // Global cleanup after all tests to catch any orphaned workflows // (e.g., from test retries or failures) // IMPORTANT: Skip cleanup in CI to preserve shared n8n instance workflows afterAll(async () => { if (!process.env.CI) { await cleanupOrphanedWorkflows(); } }); // ====================================================================== // P0: Critical Bug Verification // ====================================================================== describe('P0: Node Type Format Bug Fix', () => { it('should create workflow with webhook node using FULL node type format', async () => { // This test verifies the P0 bug fix where SHORT node type format // (e.g., "webhook") was incorrectly normalized to FULL format // causing workflow creation failures. // // The fix ensures FULL format (e.g., "n8n-nodes-base.webhook") // is preserved and passed to n8n API correctly. const workflowName = createTestWorkflowName('P0 Bug Verification - Webhook Node'); const workflow = { name: workflowName, ...getFixture('simple-webhook') }; // Create workflow using MCP handler const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(true); const result = response.data as Workflow; // Verify workflow created successfully expect(result).toBeDefined(); expect(result.id).toBeTruthy(); if (!result.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(result.id); expect(result.name).toBe(workflowName); expect(result.nodes).toHaveLength(1); // Critical: Verify FULL node type format is preserved expect(result.nodes[0].type).toBe('n8n-nodes-base.webhook'); expect(result.nodes[0].name).toBe('Webhook'); expect(result.nodes[0].parameters).toBeDefined(); }); }); // ====================================================================== // P1: Base Nodes (High Priority) // ====================================================================== describe('P1: Base n8n Nodes', () => { it('should create workflow with HTTP Request node', async () => { const workflowName = createTestWorkflowName('HTTP Request Node'); const workflow = { name: workflowName, ...getFixture('simple-http') }; const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(true); const result = response.data as Workflow; expect(result).toBeDefined(); expect(result.id).toBeTruthy(); if (!result.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(result.id); expect(result.name).toBe(workflowName); expect(result.nodes).toHaveLength(2); // Verify both nodes created with FULL type format const webhookNode = result.nodes.find((n: any) => n.name === 'Webhook'); const httpNode = result.nodes.find((n: any) => n.name === 'HTTP Request'); expect(webhookNode).toBeDefined(); expect(webhookNode!.type).toBe('n8n-nodes-base.webhook'); expect(httpNode).toBeDefined(); expect(httpNode!.type).toBe('n8n-nodes-base.httpRequest'); // Verify connections expect(result.connections).toBeDefined(); expect(result.connections.Webhook).toBeDefined(); }); it('should create workflow with langchain agent node', async () => { const workflowName = createTestWorkflowName('Langchain Agent Node'); const workflow = { name: workflowName, ...getFixture('ai-agent') }; const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(true); const result = response.data as Workflow; expect(result).toBeDefined(); expect(result.id).toBeTruthy(); if (!result.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(result.id); expect(result.name).toBe(workflowName); expect(result.nodes).toHaveLength(2); // Verify langchain node type format const agentNode = result.nodes.find((n: any) => n.name === 'AI Agent'); expect(agentNode).toBeDefined(); expect(agentNode!.type).toBe('@n8n/n8n-nodes-langchain.agent'); }); it('should create complex multi-node workflow', async () => { const workflowName = createTestWorkflowName('Multi-Node Workflow'); const workflow = { name: workflowName, ...getFixture('multi-node') }; const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(true); const result = response.data as Workflow; expect(result).toBeDefined(); expect(result.id).toBeTruthy(); if (!result.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(result.id); expect(result.name).toBe(workflowName); expect(result.nodes).toHaveLength(4); // Verify all node types preserved const nodeTypes = result.nodes.map((n: any) => n.type); expect(nodeTypes).toContain('n8n-nodes-base.webhook'); expect(nodeTypes).toContain('n8n-nodes-base.set'); expect(nodeTypes).toContain('n8n-nodes-base.merge'); // Verify complex connections expect(result.connections.Webhook.main[0]).toHaveLength(2); // Branches to 2 nodes }); }); // ====================================================================== // P2: Advanced Features (Medium Priority) // ====================================================================== describe('P2: Advanced Workflow Features', () => { it('should create workflow with complex connections and branching', async () => { const workflowName = createTestWorkflowName('Complex Connections'); const workflow = { name: workflowName, ...getFixture('multi-node') }; const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(true); const result = response.data as Workflow; expect(result).toBeDefined(); expect(result.id).toBeTruthy(); if (!result.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(result.id); expect(result.connections).toBeDefined(); // Verify branching: Webhook -> Set 1 and Set 2 const webhookConnections = result.connections.Webhook.main[0]; expect(webhookConnections).toHaveLength(2); // Verify merging: Set 1 -> Merge (port 0), Set 2 -> Merge (port 1) const set1Connections = result.connections['Set 1'].main[0]; const set2Connections = result.connections['Set 2'].main[0]; expect(set1Connections[0].node).toBe('Merge'); expect(set1Connections[0].index).toBe(0); expect(set2Connections[0].node).toBe('Merge'); expect(set2Connections[0].index).toBe(1); }); it('should create workflow with custom settings', async () => { const workflowName = createTestWorkflowName('Custom Settings'); const workflow = { name: workflowName, ...getFixture('error-handling'), settings: { executionOrder: 'v1' as const, timezone: 'America/New_York', saveDataErrorExecution: 'all' as const, saveDataSuccessExecution: 'all' as const, saveExecutionProgress: true } }; const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(true); const result = response.data as Workflow; expect(result).toBeDefined(); expect(result.id).toBeTruthy(); if (!result.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(result.id); expect(result.settings).toBeDefined(); expect(result.settings!.executionOrder).toBe('v1'); }); it('should create workflow with n8n expressions', async () => { const workflowName = createTestWorkflowName('n8n Expressions'); const workflow = { name: workflowName, ...getFixture('expression') }; const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(true); const result = response.data as Workflow; expect(result).toBeDefined(); expect(result.id).toBeTruthy(); if (!result.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(result.id); expect(result.nodes).toHaveLength(2); // Verify Set node with expressions const setNode = result.nodes.find((n: any) => n.name === 'Set Variables'); expect(setNode).toBeDefined(); expect(setNode!.parameters.assignments).toBeDefined(); // Verify expressions are preserved const assignmentsData = setNode!.parameters.assignments as { assignments: Array<{ value: string }> }; expect(assignmentsData.assignments).toHaveLength(3); expect(assignmentsData.assignments[0].value).toContain('$now'); expect(assignmentsData.assignments[1].value).toContain('$json'); expect(assignmentsData.assignments[2].value).toContain('$node'); }); it('should create workflow with error handling configuration', async () => { const workflowName = createTestWorkflowName('Error Handling'); const workflow = { name: workflowName, ...getFixture('error-handling') }; const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(true); const result = response.data as Workflow; expect(result).toBeDefined(); expect(result.id).toBeTruthy(); if (!result.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(result.id); expect(result.nodes).toHaveLength(3); // Verify HTTP node with error handling const httpNode = result.nodes.find((n: any) => n.name === 'HTTP Request'); expect(httpNode).toBeDefined(); expect(httpNode!.continueOnFail).toBe(true); expect(httpNode!.onError).toBe('continueErrorOutput'); // Verify error connection expect(result.connections['HTTP Request'].error).toBeDefined(); expect(result.connections['HTTP Request'].error[0][0].node).toBe('Handle Error'); }); }); // ====================================================================== // Error Scenarios (P1 Priority) // ====================================================================== describe('Error Scenarios', () => { it('should reject workflow with invalid node type (MCP validation)', async () => { // MCP handler correctly validates workflows before sending to n8n API. // Invalid node types are caught during MCP validation. // // Note: Raw n8n API would accept this and only fail at execution time, // but MCP handler does proper pre-validation (correct behavior). const workflowName = createTestWorkflowName('Invalid Node Type'); const workflow = { name: workflowName, nodes: [ { id: 'invalid-1', name: 'Invalid Node', type: 'n8n-nodes-base.nonexistentnode', typeVersion: 1, position: [250, 300] as [number, number], parameters: {} } ], connections: {}, settings: { executionOrder: 'v1' as const } }; // MCP handler rejects invalid workflows (correct behavior) const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(false); expect(response.error).toBeDefined(); expect(response.error).toContain('validation'); }); it('should reject workflow with missing required node parameters (MCP validation)', async () => { // MCP handler validates required parameters before sending to n8n API. // // Note: Raw n8n API would accept this and only fail at execution time, // but MCP handler does proper pre-validation (correct behavior). const workflowName = createTestWorkflowName('Missing Parameters'); const workflow = { name: workflowName, nodes: [ { id: 'http-1', name: 'HTTP Request', type: 'n8n-nodes-base.httpRequest', typeVersion: 4.2, position: [250, 300] as [number, number], parameters: { // Missing required 'url' parameter method: 'GET' } } ], connections: {}, settings: { executionOrder: 'v1' as const } }; // MCP handler rejects workflows with validation errors (correct behavior) const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(false); expect(response.error).toBeDefined(); }); it('should reject workflow with duplicate node names (MCP validation)', async () => { // MCP handler validates that node names are unique. // // Note: Raw n8n API might auto-rename duplicates, but MCP handler // enforces unique names upfront (correct behavior). const workflowName = createTestWorkflowName('Duplicate Node Names'); const workflow = { name: workflowName, nodes: [ { id: 'set-1', name: 'Set', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [250, 300] as [number, number], parameters: { assignments: { assignments: [] }, options: {} } }, { id: 'set-2', name: 'Set', // Duplicate name type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [450, 300] as [number, number], parameters: { assignments: { assignments: [] }, options: {} } } ], connections: {}, settings: { executionOrder: 'v1' as const } }; // MCP handler rejects workflows with validation errors (correct behavior) const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(false); expect(response.error).toBeDefined(); }); it('should reject workflow with invalid connection references (MCP validation)', async () => { // MCP handler validates that connection references point to existing nodes. // // Note: Raw n8n API would accept this and only fail at execution time, // but MCP handler does proper connection validation (correct behavior). const workflowName = createTestWorkflowName('Invalid Connections'); const workflow = { name: workflowName, nodes: [ { id: 'webhook-1', name: 'Webhook', type: 'n8n-nodes-base.webhook', typeVersion: 2, position: [250, 300] as [number, number], parameters: { httpMethod: 'GET', path: 'test' } } ], connections: { // Connection references non-existent node Webhook: { main: [[{ node: 'NonExistent', type: 'main', index: 0 }]] } }, settings: { executionOrder: 'v1' as const } }; // MCP handler rejects workflows with invalid connections (correct behavior) const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(false); expect(response.error).toBeDefined(); expect(response.error).toContain('validation'); }); }); // ====================================================================== // Additional Edge Cases // ====================================================================== describe('Edge Cases', () => { it('should reject single-node non-webhook workflow (MCP validation)', async () => { // MCP handler enforces that single-node workflows are only valid for webhooks. // This is a best practice validation. const workflowName = createTestWorkflowName('Minimal Single Node'); const workflow = { name: workflowName, nodes: [ { id: 'manual-1', name: 'Manual Trigger', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [250, 300] as [number, number], parameters: {} } ], connections: {}, settings: { executionOrder: 'v1' as const } }; // MCP handler rejects single-node non-webhook workflows (correct behavior) const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(false); expect(response.error).toBeDefined(); expect(response.error).toContain('validation'); }); it('should reject single-node non-trigger workflow (MCP validation)', async () => { // MCP handler enforces workflow best practices. // Single isolated nodes without connections are rejected. const workflowName = createTestWorkflowName('Empty Connections'); const workflow = { name: workflowName, nodes: [ { id: 'set-1', name: 'Set', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [250, 300] as [number, number], parameters: { assignments: { assignments: [] }, options: {} } } ], connections: {}, // Explicitly empty settings: { executionOrder: 'v1' as const } }; // MCP handler rejects single-node workflows (correct behavior) const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(false); expect(response.error).toBeDefined(); }); it('should reject single-node workflow without settings (MCP validation)', async () => { // MCP handler enforces workflow best practices. // Single-node non-webhook workflows are rejected. const workflowName = createTestWorkflowName('No Settings'); const workflow = { name: workflowName, nodes: [ { id: 'manual-1', name: 'Manual Trigger', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [250, 300] as [number, number], parameters: {} } ], connections: {} // No settings property }; // MCP handler rejects single-node workflows (correct behavior) const response = await handleCreateWorkflow({ ...workflow }, mcpContext); expect(response.success).toBe(false); expect(response.error).toBeDefined(); }); }); });

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