feat(v2.7.0): add n8n_update_partial_workflow with transactional updates
BREAKING CHANGES: - Renamed n8n_update_workflow to n8n_update_full_workflow for clarity NEW FEATURES: - Added n8n_update_partial_workflow tool for diff-based workflow editing - Implemented WorkflowDiffEngine with 13 operation types - Added transactional updates with two-pass processing - Maximum 5 operations per request for reliability - Operations can be in any order - engine handles dependencies - Added validateOnly mode for testing changes before applying - 80-90% token savings by only sending changes IMPROVEMENTS: - Enhanced tool description with comprehensive parameter documentation - Added clear examples for simple and complex use cases - Improved error messages and validation - Added extensive test coverage for all operations DOCUMENTATION: - Added workflow-diff-examples.md with usage patterns - Added transactional-updates-example.md showing before/after - Updated README.md with v2.7.0 features - Updated CLAUDE.md with latest changes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
145
src/mcp/handlers-workflow-diff.ts
Normal file
145
src/mcp/handlers-workflow-diff.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* MCP Handler for Partial Workflow Updates
|
||||
* Handles diff-based workflow modifications
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { McpToolResponse } from '../types/n8n-api';
|
||||
import { WorkflowDiffRequest, WorkflowDiffOperation } from '../types/workflow-diff';
|
||||
import { WorkflowDiffEngine } from '../services/workflow-diff-engine';
|
||||
import { getN8nApiClient } from './handlers-n8n-manager';
|
||||
import { N8nApiError, getUserFriendlyErrorMessage } from '../utils/n8n-errors';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Zod schema for the diff request
|
||||
const workflowDiffSchema = z.object({
|
||||
id: z.string(),
|
||||
operations: z.array(z.object({
|
||||
type: z.string(),
|
||||
description: z.string().optional(),
|
||||
// Node operations
|
||||
node: z.any().optional(),
|
||||
nodeId: z.string().optional(),
|
||||
nodeName: z.string().optional(),
|
||||
changes: z.any().optional(),
|
||||
position: z.tuple([z.number(), z.number()]).optional(),
|
||||
// Connection operations
|
||||
source: z.string().optional(),
|
||||
target: z.string().optional(),
|
||||
sourceOutput: z.string().optional(),
|
||||
targetInput: z.string().optional(),
|
||||
sourceIndex: z.number().optional(),
|
||||
targetIndex: z.number().optional(),
|
||||
// Metadata operations
|
||||
settings: z.any().optional(),
|
||||
name: z.string().optional(),
|
||||
tag: z.string().optional(),
|
||||
})),
|
||||
validateOnly: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export async function handleUpdatePartialWorkflow(args: unknown): Promise<McpToolResponse> {
|
||||
try {
|
||||
// Debug: Log what Claude Desktop sends
|
||||
logger.info('[DEBUG] Full args from Claude Desktop:', JSON.stringify(args, null, 2));
|
||||
logger.info('[DEBUG] Args type:', typeof args);
|
||||
if (args && typeof args === 'object') {
|
||||
logger.info('[DEBUG] Args keys:', Object.keys(args as any));
|
||||
}
|
||||
|
||||
// Validate input
|
||||
const input = workflowDiffSchema.parse(args);
|
||||
|
||||
// Get API client
|
||||
const client = getN8nApiClient();
|
||||
if (!client) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'n8n API not configured. Please set N8N_API_URL and N8N_API_KEY environment variables.'
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch current workflow
|
||||
let workflow;
|
||||
try {
|
||||
workflow = await client.getWorkflow(input.id);
|
||||
} catch (error) {
|
||||
if (error instanceof N8nApiError) {
|
||||
return {
|
||||
success: false,
|
||||
error: getUserFriendlyErrorMessage(error),
|
||||
code: error.code
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Apply diff operations
|
||||
const diffEngine = new WorkflowDiffEngine();
|
||||
const diffResult = await diffEngine.applyDiff(workflow, input as WorkflowDiffRequest);
|
||||
|
||||
if (!diffResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to apply diff operations',
|
||||
details: {
|
||||
errors: diffResult.errors,
|
||||
operationsApplied: diffResult.operationsApplied
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// If validateOnly, return validation result
|
||||
if (input.validateOnly) {
|
||||
return {
|
||||
success: true,
|
||||
message: diffResult.message,
|
||||
data: {
|
||||
valid: true,
|
||||
operationsToApply: input.operations.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Update workflow via API
|
||||
try {
|
||||
const updatedWorkflow = await client.updateWorkflow(input.id, diffResult.workflow!);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedWorkflow,
|
||||
message: `Workflow "${updatedWorkflow.name}" updated successfully. Applied ${diffResult.operationsApplied} operations.`,
|
||||
details: {
|
||||
operationsApplied: diffResult.operationsApplied,
|
||||
workflowId: updatedWorkflow.id,
|
||||
workflowName: updatedWorkflow.name
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof N8nApiError) {
|
||||
return {
|
||||
success: false,
|
||||
error: getUserFriendlyErrorMessage(error),
|
||||
code: error.code,
|
||||
details: error.details as Record<string, unknown> | undefined
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid input',
|
||||
details: { errors: error.errors }
|
||||
};
|
||||
}
|
||||
|
||||
logger.error('Failed to update partial workflow', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { TemplateService } from '../templates/template-service';
|
||||
import { WorkflowValidator } from '../services/workflow-validator';
|
||||
import { isN8nApiConfigured } from '../config/n8n-api';
|
||||
import * as n8nHandlers from './handlers-n8n-manager';
|
||||
import { handleUpdatePartialWorkflow } from './handlers-workflow-diff';
|
||||
|
||||
interface NodeRow {
|
||||
node_type: string;
|
||||
@@ -236,8 +237,10 @@ export class N8NDocumentationMCPServer {
|
||||
return n8nHandlers.handleGetWorkflowStructure(args);
|
||||
case 'n8n_get_workflow_minimal':
|
||||
return n8nHandlers.handleGetWorkflowMinimal(args);
|
||||
case 'n8n_update_workflow':
|
||||
case 'n8n_update_full_workflow':
|
||||
return n8nHandlers.handleUpdateWorkflow(args);
|
||||
case 'n8n_update_partial_workflow':
|
||||
return handleUpdatePartialWorkflow(args);
|
||||
case 'n8n_delete_workflow':
|
||||
return n8nHandlers.handleDeleteWorkflow(args);
|
||||
case 'n8n_list_workflows':
|
||||
|
||||
@@ -125,8 +125,8 @@ export const n8nManagementTools: ToolDefinition[] = [
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'n8n_update_workflow',
|
||||
description: `Update an existing workflow. Requires the full nodes array when modifying nodes/connections. Cannot activate workflows via API - use UI instead.`,
|
||||
name: 'n8n_update_full_workflow',
|
||||
description: `Update an existing workflow with complete replacement. Requires the full nodes array and connections object when modifying workflow structure. Use n8n_update_partial_workflow for incremental changes. Cannot activate workflows via API - use UI instead.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -154,6 +154,135 @@ export const n8nManagementTools: ToolDefinition[] = [
|
||||
required: ['id']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'n8n_update_partial_workflow',
|
||||
description: `Update a workflow using diff operations for precise, incremental changes. More efficient than n8n_update_full_workflow for small modifications. Supports adding/removing/updating nodes and connections without sending the entire workflow.
|
||||
|
||||
PARAMETERS:
|
||||
• id (required) - Workflow ID to update
|
||||
• operations (required) - Array of operations to apply (max 5)
|
||||
• validateOnly (optional) - Test operations without applying (default: false)
|
||||
|
||||
TRANSACTIONAL UPDATES (v2.7.0+):
|
||||
• Maximum 5 operations per request for reliability
|
||||
• Two-pass processing: nodes first, then connections/metadata
|
||||
• Add nodes and connect them in the same request
|
||||
• Operations can be in any order - engine handles dependencies
|
||||
|
||||
IMPORTANT NOTES:
|
||||
• Operations are atomic - all succeed or all fail
|
||||
• Use validateOnly: true to test before applying
|
||||
• Node references use NAME, not ID (except in node definition)
|
||||
• updateNode with nested paths: use dot notation like "parameters.values[0]"
|
||||
• All nodes require: id, name, type, typeVersion, position, parameters
|
||||
|
||||
OPERATION TYPES:
|
||||
|
||||
addNode - Add a new node
|
||||
Required: node object with id, name, type, typeVersion, position, parameters
|
||||
Example: {
|
||||
type: "addNode",
|
||||
node: {
|
||||
id: "unique_id",
|
||||
name: "HTTP Request",
|
||||
type: "n8n-nodes-base.httpRequest",
|
||||
typeVersion: 4.2,
|
||||
position: [400, 300],
|
||||
parameters: { url: "https://api.example.com", method: "GET" }
|
||||
}
|
||||
}
|
||||
|
||||
removeNode - Remove node by name
|
||||
Required: nodeName or nodeId
|
||||
Example: {type: "removeNode", nodeName: "Old Node"}
|
||||
|
||||
updateNode - Update node properties
|
||||
Required: nodeName, changes
|
||||
Example: {type: "updateNode", nodeName: "Webhook", changes: {"parameters.path": "/new-path"}}
|
||||
|
||||
moveNode - Change node position
|
||||
Required: nodeName, position
|
||||
Example: {type: "moveNode", nodeName: "Set", position: [600, 400]}
|
||||
|
||||
enableNode/disableNode - Toggle node status
|
||||
Required: nodeName
|
||||
Example: {type: "disableNode", nodeName: "Debug"}
|
||||
|
||||
addConnection - Connect nodes
|
||||
Required: source, target
|
||||
Optional: sourceOutput (default: "main"), targetInput (default: "main"),
|
||||
sourceIndex (default: 0), targetIndex (default: 0)
|
||||
Example: {
|
||||
type: "addConnection",
|
||||
source: "Webhook",
|
||||
target: "Set",
|
||||
sourceOutput: "main", // for nodes with multiple outputs
|
||||
targetInput: "main" // for nodes with multiple inputs
|
||||
}
|
||||
|
||||
removeConnection - Disconnect nodes
|
||||
Required: source, target
|
||||
Optional: sourceOutput, targetInput
|
||||
Example: {type: "removeConnection", source: "Set", target: "HTTP Request"}
|
||||
|
||||
updateSettings - Change workflow settings
|
||||
Required: settings object
|
||||
Example: {type: "updateSettings", settings: {executionOrder: "v1", timezone: "Europe/Berlin"}}
|
||||
|
||||
updateName - Rename workflow
|
||||
Required: name
|
||||
Example: {type: "updateName", name: "New Workflow Name"}
|
||||
|
||||
addTag/removeTag - Manage tags
|
||||
Required: tag
|
||||
Example: {type: "addTag", tag: "production"}
|
||||
|
||||
EXAMPLES:
|
||||
|
||||
Simple update:
|
||||
operations: [
|
||||
{type: "updateName", name: "My Updated Workflow"},
|
||||
{type: "disableNode", nodeName: "Debug Node"}
|
||||
]
|
||||
|
||||
Complex example - Add nodes and connect (any order works):
|
||||
operations: [
|
||||
{type: "addConnection", source: "Webhook", target: "Format Date"},
|
||||
{type: "addNode", node: {id: "abc123", name: "Format Date", type: "n8n-nodes-base.dateTime", typeVersion: 2, position: [400, 300], parameters: {}}},
|
||||
{type: "addConnection", source: "Format Date", target: "Logger"},
|
||||
{type: "addNode", node: {id: "def456", name: "Logger", type: "n8n-nodes-base.n8n", typeVersion: 1, position: [600, 300], parameters: {}}}
|
||||
]
|
||||
|
||||
Validation example:
|
||||
{
|
||||
id: "workflow-id",
|
||||
operations: [{type: "addNode", node: {...}}],
|
||||
validateOnly: true // Test without applying
|
||||
}`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: true, // Allow any extra properties Claude Desktop might add
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Workflow ID to update'
|
||||
},
|
||||
operations: {
|
||||
type: 'array',
|
||||
description: 'Array of diff operations to apply. Each operation must have a "type" field and relevant properties for that operation type.',
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: true
|
||||
}
|
||||
},
|
||||
validateOnly: {
|
||||
type: 'boolean',
|
||||
description: 'If true, only validate operations without applying them'
|
||||
}
|
||||
},
|
||||
required: ['id', 'operations']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'n8n_delete_workflow',
|
||||
description: `Permanently delete a workflow. This action cannot be undone.`,
|
||||
|
||||
Reference in New Issue
Block a user