Merge pull request #223 from czlonkowski/feat/improve-update-partial-workflow

feat: Remove unnecessary 5-operation limit from n8n_update_partial_workflow
This commit is contained in:
Romuald Członkowski
2025-09-24 16:07:01 +02:00
committed by GitHub
12 changed files with 522 additions and 30 deletions

View File

@@ -2,7 +2,6 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub stars](https://img.shields.io/github/stars/czlonkowski/n8n-mcp?style=social)](https://github.com/czlonkowski/n8n-mcp)
[![Version](https://img.shields.io/badge/version-2.13.0-blue.svg)](https://github.com/czlonkowski/n8n-mcp)
[![npm version](https://img.shields.io/npm/v/n8n-mcp.svg)](https://www.npmjs.com/package/n8n-mcp)
[![codecov](https://codecov.io/gh/czlonkowski/n8n-mcp/graph/badge.svg?token=YOUR_TOKEN)](https://codecov.io/gh/czlonkowski/n8n-mcp)
[![Tests](https://img.shields.io/badge/tests-1728%20passing-brightgreen.svg)](https://github.com/czlonkowski/n8n-mcp/actions)

View File

@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.13.1] - 2025-01-24
### Changed
- **Removed 5-operation limit from n8n_update_partial_workflow**: The workflow diff engine now supports unlimited operations per request
- Previously limited to 5 operations for "transactional integrity"
- Analysis revealed the limit was unnecessary - the clone-validate-apply pattern already ensures atomicity
- All operations are validated before any are applied, maintaining data integrity
- Enables complex workflow refactoring in single API calls
- Updated documentation and examples to demonstrate large batch operations (26+ operations)
## [2.13.0] - 2025-01-24
### Added

View File

@@ -296,6 +296,193 @@ The `n8n_update_partial_workflow` tool allows you to make targeted changes to wo
}
```
### Example 5: Large Batch Workflow Refactoring
Demonstrates handling many operations in a single request - no longer limited to 5 operations!
```json
{
"id": "workflow-batch",
"operations": [
// Add 10 processing nodes
{
"type": "addNode",
"node": {
"name": "Filter Active Users",
"type": "n8n-nodes-base.filter",
"position": [400, 200],
"parameters": { "conditions": { "boolean": [{ "value1": "={{$json.active}}", "value2": true }] } }
}
},
{
"type": "addNode",
"node": {
"name": "Transform User Data",
"type": "n8n-nodes-base.set",
"position": [600, 200],
"parameters": { "values": { "string": [{ "name": "formatted_name", "value": "={{$json.firstName}} {{$json.lastName}}" }] } }
}
},
{
"type": "addNode",
"node": {
"name": "Validate Email",
"type": "n8n-nodes-base.if",
"position": [800, 200],
"parameters": { "conditions": { "string": [{ "value1": "={{$json.email}}", "operation": "contains", "value2": "@" }] } }
}
},
{
"type": "addNode",
"node": {
"name": "Enrich with API",
"type": "n8n-nodes-base.httpRequest",
"position": [1000, 150],
"parameters": { "url": "https://api.example.com/enrich", "method": "POST" }
}
},
{
"type": "addNode",
"node": {
"name": "Log Invalid Emails",
"type": "n8n-nodes-base.code",
"position": [1000, 350],
"parameters": { "jsCode": "console.log('Invalid email:', $json.email);\nreturn $json;" }
}
},
{
"type": "addNode",
"node": {
"name": "Merge Results",
"type": "n8n-nodes-base.merge",
"position": [1200, 250]
}
},
{
"type": "addNode",
"node": {
"name": "Deduplicate",
"type": "n8n-nodes-base.removeDuplicates",
"position": [1400, 250],
"parameters": { "propertyName": "id" }
}
},
{
"type": "addNode",
"node": {
"name": "Sort by Date",
"type": "n8n-nodes-base.sort",
"position": [1600, 250],
"parameters": { "sortFieldsUi": { "sortField": [{ "fieldName": "created_at", "order": "descending" }] } }
}
},
{
"type": "addNode",
"node": {
"name": "Batch for DB",
"type": "n8n-nodes-base.splitInBatches",
"position": [1800, 250],
"parameters": { "batchSize": 100 }
}
},
{
"type": "addNode",
"node": {
"name": "Save to Database",
"type": "n8n-nodes-base.postgres",
"position": [2000, 250],
"parameters": { "operation": "insert", "table": "processed_users" }
}
},
// Connect all the nodes
{
"type": "addConnection",
"source": "Get Users",
"target": "Filter Active Users"
},
{
"type": "addConnection",
"source": "Filter Active Users",
"target": "Transform User Data"
},
{
"type": "addConnection",
"source": "Transform User Data",
"target": "Validate Email"
},
{
"type": "addConnection",
"source": "Validate Email",
"sourceOutput": "true",
"target": "Enrich with API"
},
{
"type": "addConnection",
"source": "Validate Email",
"sourceOutput": "false",
"target": "Log Invalid Emails"
},
{
"type": "addConnection",
"source": "Enrich with API",
"target": "Merge Results"
},
{
"type": "addConnection",
"source": "Log Invalid Emails",
"target": "Merge Results",
"targetInput": "input2"
},
{
"type": "addConnection",
"source": "Merge Results",
"target": "Deduplicate"
},
{
"type": "addConnection",
"source": "Deduplicate",
"target": "Sort by Date"
},
{
"type": "addConnection",
"source": "Sort by Date",
"target": "Batch for DB"
},
{
"type": "addConnection",
"source": "Batch for DB",
"target": "Save to Database"
},
// Update workflow metadata
{
"type": "updateName",
"name": "User Processing Pipeline v2"
},
{
"type": "updateSettings",
"settings": {
"executionOrder": "v1",
"timezone": "UTC",
"saveDataSuccessExecution": "all"
}
},
{
"type": "addTag",
"tag": "production"
},
{
"type": "addTag",
"tag": "user-processing"
},
{
"type": "addTag",
"tag": "v2"
}
]
}
```
This example shows 26 operations in a single request, creating a complete data processing pipeline with proper error handling, validation, and batch processing.
## Best Practices
1. **Use Descriptive Names**: Always provide clear node names and descriptions for operations

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp",
"version": "2.13.0",
"version": "2.13.1",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js",
"bin": {

View File

@@ -76,6 +76,6 @@ export const validateWorkflowDoc: ToolDocumentation = {
'Validation cannot catch all runtime errors (e.g., API failures)',
'Profile setting only affects node validation, not connection/expression checks'
],
relatedTools: ['validate_workflow_connections', 'validate_workflow_expressions', 'validate_node_operation', 'n8n_create_workflow', 'n8n_update_partial_workflow']
relatedTools: ['validate_workflow_connections', 'validate_workflow_expressions', 'validate_node_operation', 'n8n_create_workflow', 'n8n_update_partial_workflow', 'n8n_autofix_workflow']
}
};

View File

@@ -4,18 +4,18 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
name: 'n8n_update_partial_workflow',
category: 'workflow_management',
essentials: {
description: 'Update workflow incrementally with diff operations. Max 5 ops. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag.',
description: 'Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag.',
keyParameters: ['id', 'operations'],
example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "updateNode", ...}]})',
performance: 'Fast (50-200ms)',
tips: [
'Use for targeted changes',
'Supports up to 5 operations',
'Supports multiple operations in one call',
'Validate with validateOnly first'
]
},
full: {
description: `Updates workflows using surgical diff operations instead of full replacement. Supports 13 operation types for precise modifications. Operations are validated and applied atomically - all succeed or none are applied. Maximum 5 operations per call for safety.
description: `Updates workflows using surgical diff operations instead of full replacement. Supports 13 operation types for precise modifications. Operations are validated and applied atomically - all succeed or none are applied.
## Available Operations:
@@ -42,7 +42,7 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
operations: {
type: 'array',
required: true,
description: 'Array of diff operations. Each must have "type" field and operation-specific properties. Max 5 operations. Nodes can be referenced by ID or name.'
description: 'Array of diff operations. Each must have "type" field and operation-specific properties. Nodes can be referenced by ID or name.'
},
validateOnly: { type: 'boolean', description: 'If true, only validate operations without applying them' }
},
@@ -64,12 +64,10 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
bestPractices: [
'Use validateOnly to test operations',
'Group related changes in one call',
'Keep operations under 5 for clarity',
'Check operation order for dependencies'
],
pitfalls: [
'**REQUIRES N8N_API_URL and N8N_API_KEY environment variables** - will not work without n8n API access',
'Maximum 5 operations per call - split larger updates',
'Operations validated together - all must be valid',
'Order matters for dependent operations (e.g., must add node before connecting to it)',
'Node references accept ID or name, but name must be unique',

View File

@@ -66,6 +66,6 @@ Requires N8N_API_URL and N8N_API_KEY environment variables to be configured.`,
'Profile affects validation time - strict is slower but more thorough',
'Expression validation may flag working but non-standard syntax'
],
relatedTools: ['validate_workflow', 'n8n_get_workflow', 'validate_workflow_expressions', 'n8n_health_check']
relatedTools: ['validate_workflow', 'n8n_get_workflow', 'validate_workflow_expressions', 'n8n_health_check', 'n8n_autofix_workflow']
}
};

View File

@@ -160,7 +160,7 @@ export const n8nManagementTools: ToolDefinition[] = [
},
{
name: 'n8n_update_partial_workflow',
description: `Update workflow incrementally with diff operations. Max 5 ops. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag. See tools_documentation("n8n_update_partial_workflow", "full") for details.`,
description: `Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag. See tools_documentation("n8n_update_partial_workflow", "full") for details.`,
inputSchema: {
type: 'object',
additionalProperties: true, // Allow any extra properties Claude Desktop might add

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env npx tsx
/**
* Test script to verify n8n_autofix_workflow documentation is properly integrated
*/
import { toolsDocumentation } from '../mcp/tool-docs';
import { getToolDocumentation } from '../mcp/tools-documentation';
import { Logger } from '../utils/logger';
const logger = new Logger({ prefix: '[AutofixDoc Test]' });
async function testAutofixDocumentation() {
logger.info('Testing n8n_autofix_workflow documentation...\n');
// Test 1: Check if documentation exists in the registry
logger.info('Test 1: Checking documentation registry');
const hasDoc = 'n8n_autofix_workflow' in toolsDocumentation;
if (hasDoc) {
logger.info('✅ Documentation found in registry');
} else {
logger.error('❌ Documentation NOT found in registry');
logger.info('Available tools:', Object.keys(toolsDocumentation).filter(k => k.includes('autofix')));
}
// Test 2: Check documentation structure
if (hasDoc) {
logger.info('\nTest 2: Checking documentation structure');
const doc = toolsDocumentation['n8n_autofix_workflow'];
const hasEssentials = doc.essentials &&
doc.essentials.description &&
doc.essentials.keyParameters &&
doc.essentials.example;
const hasFull = doc.full &&
doc.full.description &&
doc.full.parameters &&
doc.full.examples;
if (hasEssentials) {
logger.info('✅ Essentials documentation complete');
logger.info(` Description: ${doc.essentials.description.substring(0, 80)}...`);
logger.info(` Key params: ${doc.essentials.keyParameters.join(', ')}`);
} else {
logger.error('❌ Essentials documentation incomplete');
}
if (hasFull) {
logger.info('✅ Full documentation complete');
logger.info(` Parameters: ${Object.keys(doc.full.parameters).join(', ')}`);
logger.info(` Examples: ${doc.full.examples.length} provided`);
} else {
logger.error('❌ Full documentation incomplete');
}
}
// Test 3: Test getToolDocumentation function
logger.info('\nTest 3: Testing getToolDocumentation function');
try {
const essentialsDoc = getToolDocumentation('n8n_autofix_workflow', 'essentials');
if (essentialsDoc.includes("Tool 'n8n_autofix_workflow' not found")) {
logger.error('❌ Essentials documentation retrieval failed');
} else {
logger.info('✅ Essentials documentation retrieved');
const lines = essentialsDoc.split('\n').slice(0, 3);
lines.forEach(line => logger.info(` ${line}`));
}
} catch (error) {
logger.error('❌ Error retrieving essentials documentation:', error);
}
try {
const fullDoc = getToolDocumentation('n8n_autofix_workflow', 'full');
if (fullDoc.includes("Tool 'n8n_autofix_workflow' not found")) {
logger.error('❌ Full documentation retrieval failed');
} else {
logger.info('✅ Full documentation retrieved');
const lines = fullDoc.split('\n').slice(0, 3);
lines.forEach(line => logger.info(` ${line}`));
}
} catch (error) {
logger.error('❌ Error retrieving full documentation:', error);
}
// Test 4: Check if tool is listed in workflow management tools
logger.info('\nTest 4: Checking workflow management tools listing');
const workflowTools = Object.keys(toolsDocumentation).filter(k => k.startsWith('n8n_'));
const hasAutofix = workflowTools.includes('n8n_autofix_workflow');
if (hasAutofix) {
logger.info('✅ n8n_autofix_workflow is listed in workflow management tools');
logger.info(` Total workflow tools: ${workflowTools.length}`);
// Show related tools
const relatedTools = workflowTools.filter(t =>
t.includes('validate') || t.includes('update') || t.includes('fix')
);
logger.info(` Related tools: ${relatedTools.join(', ')}`);
} else {
logger.error('❌ n8n_autofix_workflow NOT listed in workflow management tools');
}
// Summary
logger.info('\n' + '='.repeat(60));
logger.info('Summary:');
if (hasDoc && hasAutofix) {
logger.info('✨ Documentation integration successful!');
logger.info('The n8n_autofix_workflow tool documentation is properly integrated.');
logger.info('\nTo use in MCP:');
logger.info(' - Essentials: tools_documentation({topic: "n8n_autofix_workflow"})');
logger.info(' - Full: tools_documentation({topic: "n8n_autofix_workflow", depth: "full"})');
} else {
logger.error('⚠️ Documentation integration incomplete');
logger.info('Please check the implementation and rebuild the project.');
}
}
testAutofixDocumentation().catch(console.error);

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env node
/**
* Test script for webhook path autofixer functionality
*/
import { NodeRepository } from '../database/node-repository';
import { createDatabaseAdapter } from '../database/database-adapter';
import { WorkflowAutoFixer } from '../services/workflow-auto-fixer';
import { WorkflowValidator } from '../services/workflow-validator';
import { EnhancedConfigValidator } from '../services/enhanced-config-validator';
import { Workflow } from '../types/n8n-api';
import { Logger } from '../utils/logger';
import { join } from 'path';
const logger = new Logger({ prefix: '[TestWebhookAutofix]' });
// Test workflow with webhook missing path
const testWorkflow: Workflow = {
id: 'test_webhook_fix',
name: 'Test Webhook Autofix',
active: false,
nodes: [
{
id: '1',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
typeVersion: 2.1,
position: [250, 300],
parameters: {}, // Empty parameters - missing path
},
{
id: '2',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [450, 300],
parameters: {
url: 'https://api.example.com/data',
method: 'GET'
}
}
],
connections: {
'Webhook': {
main: [[{
node: 'HTTP Request',
type: 'main',
index: 0
}]]
}
},
settings: {
executionOrder: 'v1'
},
staticData: undefined
};
async function testWebhookAutofix() {
logger.info('Testing webhook path autofixer...');
// Initialize database and repository
const dbPath = join(process.cwd(), 'data', 'nodes.db');
const adapter = await createDatabaseAdapter(dbPath);
const repository = new NodeRepository(adapter);
// Create validators
const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
const autoFixer = new WorkflowAutoFixer(repository);
// Step 1: Validate workflow to identify issues
logger.info('Step 1: Validating workflow to identify issues...');
const validationResult = await validator.validateWorkflow(testWorkflow);
console.log('\n📋 Validation Summary:');
console.log(`- Valid: ${validationResult.valid}`);
console.log(`- Errors: ${validationResult.errors.length}`);
console.log(`- Warnings: ${validationResult.warnings.length}`);
if (validationResult.errors.length > 0) {
console.log('\n❌ Errors found:');
validationResult.errors.forEach(error => {
console.log(` - [${error.nodeName || error.nodeId}] ${error.message}`);
});
}
// Step 2: Generate fixes (preview mode)
logger.info('\nStep 2: Generating fixes in preview mode...');
const fixResult = autoFixer.generateFixes(
testWorkflow,
validationResult,
[], // No expression format issues to pass
{
applyFixes: false, // Preview mode
fixTypes: ['webhook-missing-path'] // Only test webhook fixes
}
);
console.log('\n🔧 Fix Results:');
console.log(`- Summary: ${fixResult.summary}`);
console.log(`- Total fixes: ${fixResult.stats.total}`);
console.log(`- Webhook path fixes: ${fixResult.stats.byType['webhook-missing-path']}`);
if (fixResult.fixes.length > 0) {
console.log('\n📝 Detailed Fixes:');
fixResult.fixes.forEach(fix => {
console.log(` - Node: ${fix.node}`);
console.log(` Field: ${fix.field}`);
console.log(` Type: ${fix.type}`);
console.log(` Before: ${fix.before || 'undefined'}`);
console.log(` After: ${fix.after}`);
console.log(` Confidence: ${fix.confidence}`);
console.log(` Description: ${fix.description}`);
});
}
if (fixResult.operations.length > 0) {
console.log('\n🔄 Operations to Apply:');
fixResult.operations.forEach(op => {
if (op.type === 'updateNode') {
console.log(` - Update Node: ${op.nodeId}`);
console.log(` Updates: ${JSON.stringify(op.updates, null, 2)}`);
}
});
}
// Step 3: Verify UUID format
if (fixResult.fixes.length > 0) {
const webhookFix = fixResult.fixes.find(f => f.type === 'webhook-missing-path');
if (webhookFix) {
const uuid = webhookFix.after as string;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const isValidUUID = uuidRegex.test(uuid);
console.log('\n✅ UUID Validation:');
console.log(` - Generated UUID: ${uuid}`);
console.log(` - Valid format: ${isValidUUID ? 'Yes' : 'No'}`);
}
}
logger.info('\n✨ Webhook autofix test completed successfully!');
}
// Run test
testWebhookAutofix().catch(error => {
logger.error('Test failed:', error);
process.exit(1);
});

View File

@@ -41,17 +41,6 @@ export class WorkflowDiffEngine {
request: WorkflowDiffRequest
): Promise<WorkflowDiffResult> {
try {
// Limit operations to keep complexity manageable
if (request.operations.length > 5) {
return {
success: false,
errors: [{
operation: -1,
message: 'Too many operations. Maximum 5 operations allowed per request to ensure transactional integrity.'
}]
};
}
// Clone workflow to avoid modifying original
const workflowCopy = JSON.parse(JSON.stringify(workflow));

View File

@@ -1,8 +1,9 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { WorkflowDiffEngine } from '@/services/workflow-diff-engine';
import { createWorkflow, WorkflowBuilder } from '@tests/utils/builders/workflow.builder';
import {
import {
WorkflowDiffRequest,
WorkflowDiffOperation,
AddNodeOperation,
RemoveNodeOperation,
UpdateNodeOperation,
@@ -60,9 +61,10 @@ describe('WorkflowDiffEngine', () => {
baseWorkflow.connections = newConnections;
});
describe('Operation Limits', () => {
it('should reject more than 5 operations', async () => {
const operations = Array(6).fill(null).map((_: any, i: number) => ({
describe('Large Operation Batches', () => {
it('should handle many operations successfully', async () => {
// Test with 50 operations
const operations = Array(50).fill(null).map((_: any, i: number) => ({
type: 'updateName',
name: `Name ${i}`
} as UpdateNameOperation));
@@ -73,10 +75,47 @@ describe('WorkflowDiffEngine', () => {
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors![0].message).toContain('Too many operations');
expect(result.success).toBe(true);
expect(result.operationsApplied).toBe(50);
expect(result.workflow!.name).toBe('Name 49'); // Last operation wins
});
it('should handle 100+ mixed operations', async () => {
const operations: WorkflowDiffOperation[] = [
// Add 30 nodes
...Array(30).fill(null).map((_: any, i: number) => ({
type: 'addNode',
node: {
name: `Node${i}`,
type: 'n8n-nodes-base.code',
position: [i * 100, 300],
parameters: {}
}
} as AddNodeOperation)),
// Update names 30 times
...Array(30).fill(null).map((_: any, i: number) => ({
type: 'updateName',
name: `Workflow Version ${i}`
} as UpdateNameOperation)),
// Add 40 tags
...Array(40).fill(null).map((_: any, i: number) => ({
type: 'addTag',
tag: `tag${i}`
} as AddTagOperation))
];
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.operationsApplied).toBe(100);
expect(result.workflow!.nodes.length).toBeGreaterThan(30);
expect(result.workflow!.name).toBe('Workflow Version 29');
});
});