fix(core): Improve documentation usage in mcp tools (#30210)

This commit is contained in:
Milorad FIlipović 2026-05-11 14:52:56 +02:00 committed by GitHub
parent b64a84159d
commit e8827cd6e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 137 additions and 18 deletions

View File

@ -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 () => {

View File

@ -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('<zero_item_safety>');
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('<zero_item_safety>');
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'),
});
});
});

View File

@ -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 () => {

View File

@ -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', () => {

View File

@ -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: [

View File

@ -91,7 +91,7 @@ export const createCreateWorkflowFromCodeTool = (
): ToolDefinition<typeof inputSchema> => ({
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 {

View File

@ -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: {

View File

@ -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: {

View File

@ -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.

View File

@ -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<Exclude<SdkReferenceSection, 'all'>, 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,

View File

@ -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: {

View File

@ -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<