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 278c516c016..b9bdd27f32c 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 @@ -294,6 +294,9 @@ describe('create-workflow-from-code MCP tool', () => { const response = parseResult(result); expect(response.hint).toContain('sdk_ref'); + expect(response.hint).toContain('Workflow SDK reference'); + expect(response.hint).toContain('validate_workflow_code until it returns valid=true'); + expect(response.hint).toContain('create_workflow_from_code again'); }); test('does not include SDK reference hint for non-parse errors', async () => { diff --git a/packages/cli/src/modules/mcp/__tests__/get-workflow-sdk-reference.tool.test.ts b/packages/cli/src/modules/mcp/__tests__/get-workflow-sdk-reference.tool.test.ts new file mode 100644 index 00000000000..9142c6ab2ec --- /dev/null +++ b/packages/cli/src/modules/mcp/__tests__/get-workflow-sdk-reference.tool.test.ts @@ -0,0 +1,71 @@ +import { mockInstance } from '@n8n/backend-test-utils'; +import { User } from '@n8n/db'; +import { + WORKFLOW_PATTERNS_DETAILED, + WORKFLOW_SDK_PATTERNS, +} from '@n8n/workflow-sdk/prompts/sdk-reference'; + +import { createGetWorkflowSdkReferenceTool } from '../tools/workflow-builder/get-workflow-sdk-reference.tool'; +import { getSdkReferenceContent } from '../tools/workflow-builder/sdk-reference-content'; + +import { Telemetry } from '@/telemetry'; + +jest.mock('@n8n/ai-workflow-builder', () => ({ + SDK_IMPORT_STATEMENT: "import { workflow } from '@n8n/workflow-sdk';", + MCP_GET_SDK_REFERENCE_TOOL: { + toolName: 'get_sdk_reference', + displayTitle: 'Get SDK Reference', + }, +})); + +describe('get-workflow-sdk-reference MCP tool', () => { + const user = Object.assign(new User(), { id: 'user-1' }); + let telemetry: Telemetry; + + beforeEach(() => { + jest.clearAllMocks(); + telemetry = mockInstance(Telemetry, { track: jest.fn() }); + }); + + test('returns canonical workflow SDK patterns', () => { + const content = getSdkReferenceContent('patterns'); + + expect(content).toContain(WORKFLOW_SDK_PATTERNS); + expect(content).toContain(''); + expect(content).toContain('Every IF/Filter `conditions` parameter MUST include'); + }); + + test('returns detailed workflow SDK patterns', () => { + const content = getSdkReferenceContent('patterns_detailed'); + + expect(content).toContain(WORKFLOW_PATTERNS_DETAILED); + expect(content).toContain('output: [{}]'); + }); + + test('includes both workflow pattern sections in the full reference', () => { + const content = getSdkReferenceContent('all'); + + expect(content).toContain('## Workflow Patterns'); + expect(content).toContain(''); + expect(content).toContain('## Workflow Patterns Detailed'); + expect(content).toContain('output: [{}]'); + }); + + test('accepts patterns_detailed as a tool section', async () => { + const tool = createGetWorkflowSdkReferenceTool(user, telemetry); + const sectionSchema = tool.config.inputSchema?.section; + + expect(tool.config.description).toContain('Required reference'); + expect(tool.config.description).toContain('BEFORE writing workflow code'); + expect(tool.config.inputSchema?.section.description).toContain( + 'Omit this for the full reference', + ); + expect(sectionSchema?.safeParse('patterns_detailed').success).toBe(true); + + const result = await tool.handler({ section: 'patterns_detailed' }, {} as never); + + expect(result.structuredContent).toEqual({ + reference: getSdkReferenceContent('patterns_detailed'), + }); + }); +}); diff --git a/packages/cli/src/modules/mcp/__tests__/validate-workflow-code.tool.test.ts b/packages/cli/src/modules/mcp/__tests__/validate-workflow-code.tool.test.ts index 7ff9ba184ac..b8202a7e6e9 100644 --- a/packages/cli/src/modules/mcp/__tests__/validate-workflow-code.tool.test.ts +++ b/packages/cli/src/modules/mcp/__tests__/validate-workflow-code.tool.test.ts @@ -154,6 +154,8 @@ describe('validate-workflow-code MCP tool', () => { const response = parseResult(result); expect(response.valid).toBe(false); expect(response.hint).toContain('sdk_ref'); + expect(response.hint).toContain('Workflow SDK reference'); + expect(response.hint).toContain('validate_workflow_code'); }); test('does not include SDK reference hint for non-parse errors', async () => { diff --git a/packages/cli/src/modules/mcp/__tests__/workflow-validation.utils.test.ts b/packages/cli/src/modules/mcp/__tests__/workflow-validation.utils.test.ts index 5cca4bd6b9d..1ad46711f11 100644 --- a/packages/cli/src/modules/mcp/__tests__/workflow-validation.utils.test.ts +++ b/packages/cli/src/modules/mcp/__tests__/workflow-validation.utils.test.ts @@ -9,6 +9,7 @@ import { getMcpWorkflow, getSdkReferenceHint } from '../tools/workflow-validatio jest.mock('@n8n/ai-workflow-builder', () => ({ MCP_GET_SDK_REFERENCE_TOOL: { toolName: 'get_sdk_reference', displayTitle: 'SDK Ref' }, + CODE_BUILDER_VALIDATE_TOOL: { toolName: 'validate_workflow', displayTitle: 'Validate' }, })); describe('getSdkReferenceHint', () => { @@ -19,6 +20,8 @@ describe('getSdkReferenceHint', () => { const hint = getSdkReferenceHint(error); expect(hint).toContain('get_sdk_reference'); + expect(hint).toContain('Workflow SDK reference'); + expect(hint).toContain('validate_workflow'); }); test('returns hint for SyntaxError', () => { @@ -27,6 +30,18 @@ describe('getSdkReferenceHint', () => { ); expect(hint).toContain('get_sdk_reference'); + expect(hint).toContain('required SDK patterns'); + }); + + test('uses requested follow-up action', () => { + const error = new Error('parse failed'); + error.name = 'WorkflowCodeParseError'; + + const hint = getSdkReferenceHint(error, { + afterReference: 'Then retry validation.', + }); + + expect(hint).toContain('Then retry validation.'); }); test('returns undefined for generic Error', () => { diff --git a/packages/cli/src/modules/mcp/mcp.service.ts b/packages/cli/src/modules/mcp/mcp.service.ts index 30032b3ac3e..f6d3af82876 100644 --- a/packages/cli/src/modules/mcp/mcp.service.ts +++ b/packages/cli/src/modules/mcp/mcp.service.ts @@ -409,7 +409,7 @@ export class McpService { 'n8n://workflow-sdk/reference', { description: - 'n8n Workflow SDK reference — patterns, expressions, and rules for building workflows. Get this FIRST before building workflows to learn the SDK.', + 'Required n8n Workflow SDK reference for building workflows from code. Read this before writing workflow code.', }, async () => ({ contents: [ 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 6984154ddbe..efda8e98582 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 @@ -91,7 +91,7 @@ export const createCreateWorkflowFromCodeTool = ( ): ToolDefinition => ({ name: MCP_CREATE_WORKFLOW_FROM_CODE_TOOL.toolName, config: { - description: `Create a workflow in n8n from validated SDK code. Parses the code into a workflow and saves it. Always validate with ${CODE_BUILDER_VALIDATE_TOOL.toolName} first.`, + description: `Create a workflow in n8n from validated SDK code. This tool expects code that already follows the n8n Workflow SDK patterns and has passed ${CODE_BUILDER_VALIDATE_TOOL.toolName}. If code fails to parse, call get_sdk_reference, rewrite the code using the reference, validate again, then retry creation.`, inputSchema, outputSchema, annotations: { @@ -268,7 +268,9 @@ export const createCreateWorkflowFromCodeTool = ( }; telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload); - const hint = getSdkReferenceHint(error); + const hint = getSdkReferenceHint(error, { + afterReference: `Rewrite the code, call ${CODE_BUILDER_VALIDATE_TOOL.toolName} until it returns valid=true, then call ${MCP_CREATE_WORKFLOW_FROM_CODE_TOOL.toolName} again.`, + }); const output = { error: errorMessage, ...(hint ? { hint } : {}) }; return { diff --git a/packages/cli/src/modules/mcp/tools/workflow-builder/get-suggested-workflow-nodes.tool.ts b/packages/cli/src/modules/mcp/tools/workflow-builder/get-suggested-workflow-nodes.tool.ts index 6834bf5dda5..7bd960d4fa3 100644 --- a/packages/cli/src/modules/mcp/tools/workflow-builder/get-suggested-workflow-nodes.tool.ts +++ b/packages/cli/src/modules/mcp/tools/workflow-builder/get-suggested-workflow-nodes.tool.ts @@ -35,7 +35,7 @@ export const createGetSuggestedWorkflowNodesTool = ( name: CODE_BUILDER_GET_SUGGESTED_NODES_TOOL.toolName, config: { description: - 'Get curated node recommendations for workflow technique categories. Returns recommended nodes with pattern hints and configuration guidance. Use after analyzing what kind of workflow to build.', + 'Required workflow-planning step. Get curated node recommendations for workflow technique categories before searching for nodes or writing code. Returns recommended nodes with pattern hints and configuration guidance.', inputSchema, outputSchema, annotations: { diff --git a/packages/cli/src/modules/mcp/tools/workflow-builder/get-workflow-sdk-reference.tool.ts b/packages/cli/src/modules/mcp/tools/workflow-builder/get-workflow-sdk-reference.tool.ts index 37acb85859f..98c554dc8d2 100644 --- a/packages/cli/src/modules/mcp/tools/workflow-builder/get-workflow-sdk-reference.tool.ts +++ b/packages/cli/src/modules/mcp/tools/workflow-builder/get-workflow-sdk-reference.tool.ts @@ -11,6 +11,7 @@ import { getSdkReferenceContent, type SdkReferenceSection } from './sdk-referenc const VALID_SECTIONS: SdkReferenceSection[] = [ 'patterns', + 'patterns_detailed', 'expressions', 'functions', 'rules', @@ -25,7 +26,7 @@ const inputSchema = { .enum(VALID_SECTIONS as [string, ...string[]]) .optional() .describe( - 'Optional section to retrieve: "patterns", "expressions", "functions", "rules", "import", "guidelines", "design", or "all" (default)', + 'Optional section to retrieve. Omit this for the full reference, or use a section for targeted lookup.', ), } satisfies z.ZodRawShape; @@ -44,7 +45,7 @@ export const createGetWorkflowSdkReferenceTool = ( name: MCP_GET_SDK_REFERENCE_TOOL.toolName, config: { description: - 'Get the n8n Workflow SDK reference documentation including patterns, expression syntax, and rules. Call this FIRST before building workflows to learn the SDK.', + 'Required reference for building n8n Workflow SDK code. Call this BEFORE writing workflow code to learn workflow(), trigger()/node(), .add()/.to(), expr(), and credential patterns.', inputSchema, outputSchema, annotations: { diff --git a/packages/cli/src/modules/mcp/tools/workflow-builder/mcp-instructions.ts b/packages/cli/src/modules/mcp/tools/workflow-builder/mcp-instructions.ts index 7875ca5b9b7..7bab6e452a6 100644 --- a/packages/cli/src/modules/mcp/tools/workflow-builder/mcp-instructions.ts +++ b/packages/cli/src/modules/mcp/tools/workflow-builder/mcp-instructions.ts @@ -24,11 +24,11 @@ export function getMcpInstructions(isBuilderEnabled: boolean): string { To build n8n workflows, follow these steps in order: -1. Read the SDK reference: Call ${MCP_GET_SDK_REFERENCE_TOOL.toolName} (or use the n8n://workflow-sdk/reference resource) to learn the SDK patterns and syntax. +1. Read the SDK reference: You MUST call ${MCP_GET_SDK_REFERENCE_TOOL.toolName} (or use the n8n://workflow-sdk/reference resource) before writing workflow code. Do not guess SDK syntax. -2. Discover nodes: Call ${CODE_BUILDER_SEARCH_NODES_TOOL.toolName} with queries for services you need (e.g., ["gmail", "slack", "schedule trigger"]) and utility nodes (e.g., ["set", "if", "merge", "code"]). Note the discriminators (resource/operation/mode) in the results. +2. Get suggested nodes: You MUST call ${CODE_BUILDER_GET_SUGGESTED_NODES_TOOL.toolName} with all relevant workflow technique categories before searching for nodes. Use the recommendations, pattern hints, and configuration guidance to decide which nodes and patterns to use. -3. (Optional) Get suggestions: Call ${CODE_BUILDER_GET_SUGGESTED_NODES_TOOL.toolName} with workflow technique categories for curated recommendations. +3. Discover nodes: Call ${CODE_BUILDER_SEARCH_NODES_TOOL.toolName} with queries for services you need (e.g., ["gmail", "slack", "schedule trigger"]), utility nodes (e.g., ["set", "if", "merge", "code"]), and suggested nodes you plan to use. Note the discriminators (resource/operation/mode) in the results. 4. Get type definitions: Call ${CODE_BUILDER_GET_NODE_TYPES_TOOL.toolName} with ALL node IDs you plan to use, including discriminators from search results. This returns the exact TypeScript parameter definitions. DO NOT skip this — guessing parameter names creates invalid workflows. diff --git a/packages/cli/src/modules/mcp/tools/workflow-builder/sdk-reference-content.ts b/packages/cli/src/modules/mcp/tools/workflow-builder/sdk-reference-content.ts index bcc29453d73..a5f723a44f4 100644 --- a/packages/cli/src/modules/mcp/tools/workflow-builder/sdk-reference-content.ts +++ b/packages/cli/src/modules/mcp/tools/workflow-builder/sdk-reference-content.ts @@ -1,18 +1,19 @@ /** * SDK reference content for MCP workflow builder tools. * - * Imports the raw (unescaped) prompt constants from the code-builder package + * Imports the raw (unescaped) prompt constants from the workflow-sdk package * and assembles them into structured SDK reference documentation. * Served both as an MCP resource and via the n8n_get_workflow_sdk_reference tool. */ +import { SDK_IMPORT_STATEMENT } from '@n8n/ai-workflow-builder'; import { - SDK_IMPORT_STATEMENT, EXPRESSION_REFERENCE, - WORKFLOW_PATTERNS, + WORKFLOW_SDK_PATTERNS, + WORKFLOW_PATTERNS_DETAILED, ADDITIONAL_FUNCTIONS, WORKFLOW_RULES, -} from '@n8n/ai-workflow-builder'; +} from '@n8n/workflow-sdk/prompts/sdk-reference'; // NOTE: CODING_GUIDELINES and DESIGN_GUIDANCE are MCP-only constants defined // below. They are NOT shared with the code-builder agent (which has its own @@ -40,6 +41,8 @@ const DESIGN_GUIDANCE = `Design guidance: - **Trace item counts**: For each connection A → B, if A returns N items, should B run N times or just once? If B doesn't need A's items (e.g., it fetches from an independent source), either set \`executeOnce: true\` on B or use parallel branches + Merge to combine results. - **Handling convergence after branches**: When a node receives data from multiple paths (after Switch, IF, Merge): use optional chaining \`expr('{{ $json.data?.approved ?? $json.status }}')\`, reference a node that ALWAYS runs \`expr("{{ $('Webhook').item.json.field }}")\`, or normalize data before convergence with Set nodes. - **Prefer dedicated integration nodes** over HTTP Request when search results show one is available. +- **Normalize webhook payloads immediately**: Webhook data often appears under \`body\`, but clients and tests may provide fields directly on \`$json\`. Add a Set node after the webhook that uses optional chaining and defaults, e.g. \`expr('{{ $json.body?.name ?? $json.name ?? "there" }}')\`, \`expr('{{ $json.body?.email ?? $json.email ?? "" }}')\`, and \`expr('{{ $json.body?.message ?? $json.message ?? "" }}')\`. +- **Fan out independent side effects**: For workflows that send email, notify chat, write to storage, and respond to a webhook, branch all side-effect nodes from normalized data instead of chaining them. Set \`onError: 'continueRegularOutput'\` on independent external action nodes when one failed action should not block the others. - **Pay attention to @builderHint annotations** in the type definitions — they provide critical guidance on how to correctly configure node parameters.`; /** @@ -47,6 +50,7 @@ const DESIGN_GUIDANCE = `Design guidance: */ export type SdkReferenceSection = | 'patterns' + | 'patterns_detailed' | 'expressions' | 'functions' | 'rules' @@ -57,13 +61,18 @@ export type SdkReferenceSection = const SDK_IMPORT_SECTION = `## SDK Import Statement\n\n\`\`\`javascript\n${SDK_IMPORT_STATEMENT}\n\`\`\``; +const WORKFLOW_PATTERNS_SECTION = `## Workflow Patterns\n\n${WORKFLOW_SDK_PATTERNS}`; + +const WORKFLOW_PATTERNS_DETAILED_SECTION = `## Workflow Patterns Detailed\n\n${WORKFLOW_PATTERNS_DETAILED}`; + const CODING_GUIDELINES_SECTION = `## Coding Guidelines\n\n${CODING_GUIDELINES}`; const DESIGN_GUIDANCE_SECTION = `## Design Guidance\n\n${DESIGN_GUIDANCE}`; const SECTIONS: Record, string> = { import: SDK_IMPORT_SECTION, - patterns: WORKFLOW_PATTERNS, + patterns: WORKFLOW_PATTERNS_SECTION, + patterns_detailed: WORKFLOW_PATTERNS_DETAILED_SECTION, expressions: EXPRESSION_REFERENCE, functions: ADDITIONAL_FUNCTIONS, rules: WORKFLOW_RULES, @@ -86,6 +95,8 @@ export function getSdkReferenceContent(section?: SdkReferenceSection): string { '', SECTIONS.patterns, '', + SECTIONS.patterns_detailed, + '', SECTIONS.expressions, '', SECTIONS.functions, diff --git a/packages/cli/src/modules/mcp/tools/workflow-builder/validate-workflow-code.tool.ts b/packages/cli/src/modules/mcp/tools/workflow-builder/validate-workflow-code.tool.ts index 28682d4bd10..f6e1eeb6402 100644 --- a/packages/cli/src/modules/mcp/tools/workflow-builder/validate-workflow-code.tool.ts +++ b/packages/cli/src/modules/mcp/tools/workflow-builder/validate-workflow-code.tool.ts @@ -54,7 +54,7 @@ export const createValidateWorkflowCodeTool = ( name: CODE_BUILDER_VALIDATE_TOOL.toolName, config: { description: - 'Validate n8n Workflow SDK code. Parses the code into a workflow and checks for errors. Returns the workflow JSON if valid, or detailed error messages to fix. Always validate before creating a workflow.', + 'Validate n8n Workflow SDK code. Required before creating or updating workflows from code. If you have not already read get_sdk_reference, call that first; guessing SDK syntax commonly creates invalid workflows.', inputSchema, outputSchema, annotations: { diff --git a/packages/cli/src/modules/mcp/tools/workflow-validation.utils.ts b/packages/cli/src/modules/mcp/tools/workflow-validation.utils.ts index a13cfbe4650..e1c3b9ead67 100644 --- a/packages/cli/src/modules/mcp/tools/workflow-validation.utils.ts +++ b/packages/cli/src/modules/mcp/tools/workflow-validation.utils.ts @@ -4,19 +4,33 @@ import type { Scope } from '@n8n/permissions'; import type { WorkflowFinderService } from '@/workflows/workflow-finder.service'; import { WorkflowAccessError } from '../mcp.errors'; -import { MCP_GET_SDK_REFERENCE_TOOL } from './workflow-builder/constants'; +import { + CODE_BUILDER_VALIDATE_TOOL, + MCP_GET_SDK_REFERENCE_TOOL, +} from './workflow-builder/constants'; + +type SdkReferenceHintOptions = { + afterReference?: string; +}; /** * Returns a hint nudging MCP clients to consult the SDK reference, * but only when the error is a workflow code parse error. */ -export function getSdkReferenceHint(error: unknown): string | undefined { +export function getSdkReferenceHint( + error: unknown, + options: SdkReferenceHintOptions = {}, +): string | undefined { const isParseError = error instanceof Error && (error.name === 'WorkflowCodeParseError' || error instanceof SyntaxError); if (!isParseError) return undefined; - return `Make sure your code uses the n8n Workflow SDK syntax. Call ${MCP_GET_SDK_REFERENCE_TOOL.toolName} first to learn the correct patterns before writing workflow code.`; + const afterReference = + options.afterReference ?? + `Rewrite the code using the documented patterns, then call ${CODE_BUILDER_VALIDATE_TOOL.toolName} again before creating or updating a workflow.`; + + return `The code failed to parse as n8n Workflow SDK code. This usually means it does not follow the required SDK patterns. Before retrying, call ${MCP_GET_SDK_REFERENCE_TOOL.toolName} to read the Workflow SDK reference. Use workflow(), trigger()/node(), .add()/.to(), expr(), and newCredential() exactly as documented. ${afterReference}`; } export type FoundWorkflow = NonNullable<