n8n
Version:
n8n Workflow Automation Tool
235 lines • 11.6 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createUpdateWorkflowTool = void 0;
const db_1 = require("@n8n/db");
const zod_1 = __importDefault(require("zod"));
const mcp_constants_1 = require("../../mcp.constants");
const connection_structure_check_1 = require("./connection-structure-check");
const constants_1 = require("./constants");
const credential_validation_1 = require("./credential-validation");
const credentials_auto_assign_1 = require("./credentials-auto-assign");
const data_table_validation_1 = require("./data-table-validation");
const workflow_operations_1 = require("./workflow-operations");
const workflow_helpers_1 = require("../../../../workflow-helpers");
const workflow_validation_utils_1 = require("../workflow-validation.utils");
const MAX_OPERATIONS_PER_CALL = 100;
function collectTouchedNodes(operations) {
const touched = new Map();
const recordTouch = (name, opIndex) => {
if (!touched.has(name))
touched.set(name, opIndex);
};
for (let i = 0; i < operations.length; i++) {
const op = operations[i];
if (op.type === 'addNode') {
recordTouch(op.node.name, i);
}
else if (op.type === 'updateNodeParameters' || op.type === 'setNodeParameter') {
recordTouch(op.nodeName, i);
}
else if (op.type === 'renameNode') {
const idx = touched.get(op.oldName);
if (idx !== undefined) {
touched.delete(op.oldName);
touched.set(op.newName, idx);
}
}
else if (op.type === 'removeNode') {
touched.delete(op.nodeName);
}
}
return touched;
}
const inputSchema = {
workflowId: zod_1.default.string().describe('The ID of the workflow to update.'),
operations: zod_1.default
.array(workflow_operations_1.partialUpdateOperationSchema)
.min(1)
.max(MAX_OPERATIONS_PER_CALL)
.describe(`Ordered list of operations to apply (max ${MAX_OPERATIONS_PER_CALL}). Operations are applied atomically: if any operation fails (e.g. node not found, duplicate name), the whole batch is rejected and no changes are saved.`),
};
const outputSchema = {
workflowId: zod_1.default.string(),
name: zod_1.default.string(),
nodeCount: zod_1.default.number(),
url: zod_1.default.string(),
appliedOperations: zod_1.default.number().describe('Number of operations applied.'),
autoAssignedCredentials: zod_1.default
.array(zod_1.default.object({
nodeName: zod_1.default.string(),
credentialName: zod_1.default.string(),
credentialType: zod_1.default.string(),
}))
.describe('Credentials auto-assigned to nodes that were added in this update.'),
validationWarnings: zod_1.default
.array(zod_1.default.object({
code: zod_1.default.string(),
message: zod_1.default.string(),
nodeName: zod_1.default.string().optional(),
}))
.describe('Graph and JSON validation warnings on the resulting workflow. Use these to self-correct on the next call.'),
note: zod_1.default.string().optional(),
};
const createUpdateWorkflowTool = (user, workflowFinderService, workflowService, urlService, telemetry, nodeTypes, credentialsService, sharedWorkflowRepository, collaborationService, dataTableOps) => ({
name: constants_1.MCP_UPDATE_WORKFLOW_TOOL.toolName,
config: {
description: 'Apply a small list of operations to an existing workflow (see the operations input schema for the supported op types). The whole batch is atomic: if any op fails the workflow is left unchanged.',
inputSchema,
outputSchema,
annotations: {
title: constants_1.MCP_UPDATE_WORKFLOW_TOOL.displayTitle,
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: false,
},
},
handler: async ({ workflowId, operations, }) => {
const telemetryPayload = {
user_id: user.id,
tool_name: constants_1.MCP_UPDATE_WORKFLOW_TOOL.toolName,
parameters: {
workflowId,
opCount: operations.length,
opTypes: operations.map((op) => op.type),
},
};
try {
const existingWorkflow = await (0, workflow_validation_utils_1.getMcpWorkflow)(workflowId, user, ['workflow:update'], workflowFinderService);
await collaborationService.ensureWorkflowEditable(existingWorkflow.id);
const result = (0, workflow_operations_1.applyOperations)((0, workflow_operations_1.toWorkflowSlice)(existingWorkflow), operations);
if (!result.success) {
throw new Error(result.error);
}
const credentialCheck = await (0, credential_validation_1.validateCredentialReferences)(operations, existingWorkflow, user, credentialsService, nodeTypes);
if (!credentialCheck.ok) {
throw new Error(credentialCheck.error);
}
const invalidToolSourceResponse = (0, connection_structure_check_1.buildInvalidAiToolSourceErrorResponse)({ nodes: result.workflow.nodes, connections: result.workflow.connections }, nodeTypes, (errorMessage) => ({ error: errorMessage }), telemetryPayload, telemetry);
if (invalidToolSourceResponse)
return invalidToolSourceResponse;
const { projectId: workflowProjectId } = await sharedWorkflowRepository.findOneOrFail({
where: { workflowId, role: 'workflow:owner' },
select: ['projectId'],
});
const dataTableCheck = await (0, data_table_validation_1.validateDataTableReferencesForUpdate)(result.workflow.nodes, collectTouchedNodes(operations), workflowProjectId, dataTableOps);
if (!dataTableCheck.ok) {
throw new Error(dataTableCheck.error);
}
const workflowUpdateData = new db_1.WorkflowEntity();
Object.assign(workflowUpdateData, {
name: result.workflow.name,
...(result.workflow.description !== undefined
? { description: result.workflow.description }
: {}),
nodes: result.workflow.nodes,
connections: result.workflow.connections,
meta: {
...(existingWorkflow.meta ?? {}),
aiBuilderAssisted: true,
builderVariant: 'mcp',
},
});
(0, workflow_helpers_1.resolveNodeWebhookIds)(workflowUpdateData, nodeTypes);
let credentialAssignments = [];
let skippedHttpNodes = [];
if (result.addedNodeNames.length > 0) {
const addedNodeSet = new Set(result.addedNodeNames);
const addedNodes = workflowUpdateData.nodes.filter((n) => addedNodeSet.has(n.name));
const autoAssign = await (0, credentials_auto_assign_1.autoPopulateNodeCredentials)({ ...workflowUpdateData, nodes: addedNodes }, user, nodeTypes, credentialsService, workflowProjectId);
credentialAssignments = autoAssign.assignments;
skippedHttpNodes = autoAssign.skippedHttpNodes;
}
const { ParseValidateHandler } = await Promise.resolve().then(() => __importStar(require('@n8n/ai-workflow-builder')));
const validator = new ParseValidateHandler({ generatePinData: false });
const validationWarnings = validator.validateJSON({
name: workflowUpdateData.name,
nodes: workflowUpdateData.nodes,
connections: workflowUpdateData.connections,
});
const updatedWorkflow = await workflowService.update(user, workflowUpdateData, workflowId, {
aiBuilderAssisted: true,
source: 'n8n-mcp',
});
void collaborationService.broadcastWorkflowUpdate(workflowId, user.id).catch(() => { });
const baseUrl = urlService.getInstanceBaseUrl();
const workflowUrl = `${baseUrl}/workflow/${updatedWorkflow.id}`;
telemetryPayload.results = {
success: true,
data: {
workflowId: updatedWorkflow.id,
nodeCount: updatedWorkflow.nodes.length,
},
};
telemetry.track(mcp_constants_1.USER_CALLED_MCP_TOOL_EVENT, telemetryPayload);
const output = {
workflowId: updatedWorkflow.id,
name: updatedWorkflow.name,
nodeCount: updatedWorkflow.nodes.length,
url: workflowUrl,
appliedOperations: operations.length,
autoAssignedCredentials: credentialAssignments,
validationWarnings,
note: skippedHttpNodes.length
? `HTTP Request nodes (${skippedHttpNodes.join(', ')}) were skipped during credential auto-assignment. Their credentials must be configured manually.`
: undefined,
};
return {
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
structuredContent: output,
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
telemetryPayload.results = {
success: false,
error: errorMessage,
};
telemetry.track(mcp_constants_1.USER_CALLED_MCP_TOOL_EVENT, telemetryPayload);
const output = { error: errorMessage };
return {
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
structuredContent: output,
isError: true,
};
}
},
});
exports.createUpdateWorkflowTool = createUpdateWorkflowTool;
//# sourceMappingURL=update-workflow.tool.js.map