From 0fc2d90b52b19b811a9f92d4c65d55063aa30fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Mon, 20 Apr 2026 10:48:32 +0200 Subject: [PATCH] fix(core): Report success from mcp tool if workflow is created in DB (no-changelog) (#28529) --- .../create-workflow-from-code.tool.test.ts | 5 ++ packages/cli/src/modules/mcp/mcp.service.ts | 1 + .../create-workflow-from-code.tool.ts | 57 ++++++++++++++++++- .../workflow-builder/update-workflow.tool.ts | 6 +- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/modules/mcp/__tests__/create-workflow-from-code.tool.test.ts b/packages/cli/src/modules/mcp/__tests__/create-workflow-from-code.tool.test.ts index e082e5b8e17..2b3af8181a0 100644 --- a/packages/cli/src/modules/mcp/__tests__/create-workflow-from-code.tool.test.ts +++ b/packages/cli/src/modules/mcp/__tests__/create-workflow-from-code.tool.test.ts @@ -9,6 +9,7 @@ import { NodeTypes } from '@/node-types'; import { UrlService } from '@/services/url.service'; import { Telemetry } from '@/telemetry'; import { WorkflowCreationService } from '@/workflows/workflow-creation.service'; +import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; // Mock dynamic imports const mockParseAndValidate = jest.fn(); @@ -100,11 +101,15 @@ describe('create-workflow-from-code MCP tool', () => { const projectRepository = mockInstance(ProjectRepository, { getPersonalProjectForUserOrFail: jest.fn().mockResolvedValue({ id: 'personal-project-1' }), }); + const workflowFinderService = mockInstance(WorkflowFinderService, { + findWorkflowForUser: jest.fn().mockResolvedValue(null), + }); const createTool = () => createCreateWorkflowFromCodeTool( user, workflowCreationService, + workflowFinderService, urlService, telemetry, nodeTypes, diff --git a/packages/cli/src/modules/mcp/mcp.service.ts b/packages/cli/src/modules/mcp/mcp.service.ts index 131b6de38d2..b5c8c0d0bd4 100644 --- a/packages/cli/src/modules/mcp/mcp.service.ts +++ b/packages/cli/src/modules/mcp/mcp.service.ts @@ -320,6 +320,7 @@ export class McpService { const createTool = createCreateWorkflowFromCodeTool( user, this.workflowCreationService, + this.workflowFinderService, this.urlService, this.telemetry, this.nodeTypes, diff --git a/packages/cli/src/modules/mcp/tools/workflow-builder/create-workflow-from-code.tool.ts b/packages/cli/src/modules/mcp/tools/workflow-builder/create-workflow-from-code.tool.ts index 5c7f811e4d2..f62aa343240 100644 --- a/packages/cli/src/modules/mcp/tools/workflow-builder/create-workflow-from-code.tool.ts +++ b/packages/cli/src/modules/mcp/tools/workflow-builder/create-workflow-from-code.tool.ts @@ -1,4 +1,5 @@ import { type User, type ProjectRepository, WorkflowEntity } from '@n8n/db'; +import { layoutWorkflowJSON } from '@n8n/workflow-sdk'; import z from 'zod'; import { MCP_CREATE_WORKFLOW_FROM_CODE_TOOL, CODE_BUILDER_VALIDATE_TOOL } from './constants'; @@ -13,6 +14,7 @@ import type { UrlService } from '@/services/url.service'; import type { Telemetry } from '@/telemetry'; import { resolveNodeWebhookIds } from '@/workflow-helpers'; import type { WorkflowCreationService } from '@/workflows/workflow-creation.service'; +import type { WorkflowFinderService } from '@/workflows/workflow-finder.service'; const inputSchema = { code: z @@ -80,6 +82,7 @@ const outputSchema = { export const createCreateWorkflowFromCodeTool = ( user: User, workflowCreationService: WorkflowCreationService, + workflowFinderService: WorkflowFinderService, urlService: UrlService, telemetry: Telemetry, nodeTypes: NodeTypes, @@ -134,6 +137,8 @@ export const createCreateWorkflowFromCodeTool = ( }; } + let newWorkflow: WorkflowEntity | undefined; + try { const { ParseValidateHandler, stripImportStatements } = await import( '@n8n/ai-workflow-builder' @@ -142,9 +147,10 @@ export const createCreateWorkflowFromCodeTool = ( const handler = new ParseValidateHandler({ generatePinData: false }); const strippedCode = stripImportStatements(code); const result = await handler.parseAndValidate(strippedCode); - const workflowJson = result.workflow; - const newWorkflow = new WorkflowEntity(); + const workflowJson = layoutWorkflowJSON(result.workflow); + + newWorkflow = new WorkflowEntity(); Object.assign(newWorkflow, { name: name ?? workflowJson.name ?? 'Untitled Workflow', ...(description ? { description } : {}), @@ -152,7 +158,7 @@ export const createCreateWorkflowFromCodeTool = ( connections: workflowJson.connections, settings: { ...workflowJson.settings, executionOrder: 'v1', availableInMCP: true }, pinData: workflowJson.pinData, - meta: { ...workflowJson.meta, aiBuilderAssisted: true }, + meta: { ...workflowJson.meta, aiBuilderAssisted: true, builderVariant: 'mcp' }, }); resolveNodeWebhookIds(newWorkflow, nodeTypes); @@ -210,6 +216,51 @@ export const createCreateWorkflowFromCodeTool = ( } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); + // Check whether the workflow was actually persisted despite the error. + // TypeORM sets the entity id during save(), even inside a transaction that + // may later roll back, so newWorkflow.id alone is not a reliable signal. + // A DB lookup confirms the row truly exists before we report success. + if (newWorkflow?.id) { + let persisted: Awaited> | null = + null; + try { + persisted = await workflowFinderService.findWorkflowForUser(newWorkflow.id, user, [ + 'workflow:read', + ]); + } catch { + // Verification lookup failed — fall through and report the original error. + } + + if (persisted) { + const baseUrl = urlService.getInstanceBaseUrl(); + const workflowUrl = `${baseUrl}/workflow/${persisted.id}`; + + telemetryPayload.results = { + success: true, + data: { + workflowId: persisted.id, + nodeCount: persisted.nodes.length, + postSaveError: errorMessage, + }, + }; + telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload); + + const output = { + workflowId: persisted.id, + name: persisted.name, + nodeCount: persisted.nodes.length, + url: workflowUrl, + autoAssignedCredentials: [], + note: `Workflow was created successfully, but a post-save operation failed: ${errorMessage}`, + }; + + return { + content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], + structuredContent: output, + }; + } + } + telemetryPayload.results = { success: false, error: errorMessage, diff --git a/packages/cli/src/modules/mcp/tools/workflow-builder/update-workflow.tool.ts b/packages/cli/src/modules/mcp/tools/workflow-builder/update-workflow.tool.ts index 8fec666c51f..649416bfa7c 100644 --- a/packages/cli/src/modules/mcp/tools/workflow-builder/update-workflow.tool.ts +++ b/packages/cli/src/modules/mcp/tools/workflow-builder/update-workflow.tool.ts @@ -1,4 +1,5 @@ import { type User, type SharedWorkflowRepository, WorkflowEntity } from '@n8n/db'; +import { layoutWorkflowJSON } from '@n8n/workflow-sdk'; import z from 'zod'; import { USER_CALLED_MCP_TOOL_EVENT } from '../../mcp.constants'; @@ -125,7 +126,8 @@ export const createUpdateWorkflowTool = ( const handler = new ParseValidateHandler({ generatePinData: false }); const strippedCode = stripImportStatements(code); const result = await handler.parseAndValidate(strippedCode); - const workflowJson = result.workflow; + + const workflowJson = layoutWorkflowJSON(result.workflow); const workflowUpdateData = new WorkflowEntity(); Object.assign(workflowUpdateData, { @@ -134,7 +136,7 @@ export const createUpdateWorkflowTool = ( nodes: workflowJson.nodes, connections: workflowJson.connections, pinData: workflowJson.pinData, - meta: { ...workflowJson.meta, aiBuilderAssisted: true }, + meta: { ...workflowJson.meta, aiBuilderAssisted: true, builderVariant: 'mcp' }, }); resolveNodeWebhookIds(workflowUpdateData, nodeTypes);