UNPKG

n8n

Version:

n8n Workflow Automation Tool

235 lines • 11.6 kB
"use strict"; 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