From cc9fa172c81ae6fa77bb07b954813abb44f93e0e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 2 Jun 2026 08:35:19 -0400 Subject: [PATCH] feat(core): Replace get_suggested_nodes MCP tool with get_workflow_best_practices (#31048) --- .../src/code-builder/constants.ts | 5 + .../src/code-builder/index.ts | 1 + .../@n8n/ai-workflow-builder.ee/src/index.ts | 1 + .../get-workflow-best-practices.tool.test.ts | 101 ++++++++++++ packages/cli/src/modules/mcp/mcp.service.ts | 14 +- .../mcp/tools/workflow-builder/constants.ts | 2 +- .../get-suggested-workflow-nodes.tool.ts | 78 --------- .../get-workflow-best-practices.tool.ts | 155 ++++++++++++++++++ .../workflow-builder/mcp-instructions.ts | 4 +- .../tests/e2e/mcp/mcp-service.spec.ts | 2 +- 10 files changed, 272 insertions(+), 91 deletions(-) create mode 100644 packages/cli/src/modules/mcp/__tests__/get-workflow-best-practices.tool.test.ts delete mode 100644 packages/cli/src/modules/mcp/tools/workflow-builder/get-suggested-workflow-nodes.tool.ts create mode 100644 packages/cli/src/modules/mcp/tools/workflow-builder/get-workflow-best-practices.tool.ts diff --git a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/constants.ts b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/constants.ts index 0f86feaea9b..a7b912a9098 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/constants.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/constants.ts @@ -133,3 +133,8 @@ export const MCP_UPDATE_WORKFLOW_TOOL: BuilderToolBase = { toolName: 'update_workflow', displayTitle: 'Updating workflow', }; + +export const MCP_GET_WORKFLOW_BEST_PRACTICES_TOOL: BuilderToolBase = { + toolName: 'get_workflow_best_practices', + displayTitle: 'Getting workflow best practices', +}; diff --git a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/index.ts b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/index.ts index 55a93137800..0fa946ac857 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/index.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/index.ts @@ -43,4 +43,5 @@ export { MCP_CREATE_WORKFLOW_FROM_CODE_TOOL, MCP_ARCHIVE_WORKFLOW_TOOL, MCP_UPDATE_WORKFLOW_TOOL, + MCP_GET_WORKFLOW_BEST_PRACTICES_TOOL, } from './constants'; diff --git a/packages/@n8n/ai-workflow-builder.ee/src/index.ts b/packages/@n8n/ai-workflow-builder.ee/src/index.ts index 144e8c53a4e..0cd9da6e9f3 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/index.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/index.ts @@ -34,6 +34,7 @@ export { MCP_CREATE_WORKFLOW_FROM_CODE_TOOL, MCP_ARCHIVE_WORKFLOW_TOOL, MCP_UPDATE_WORKFLOW_TOOL, + MCP_GET_WORKFLOW_BEST_PRACTICES_TOOL, } from './code-builder'; export type { ParseAndValidateResult, ValidationWarning } from './code-builder'; diff --git a/packages/cli/src/modules/mcp/__tests__/get-workflow-best-practices.tool.test.ts b/packages/cli/src/modules/mcp/__tests__/get-workflow-best-practices.tool.test.ts new file mode 100644 index 00000000000..4fa27ba6077 --- /dev/null +++ b/packages/cli/src/modules/mcp/__tests__/get-workflow-best-practices.tool.test.ts @@ -0,0 +1,101 @@ +import { User } from '@n8n/db'; +import { + bestPracticesRegistry, + TechniqueDescription, + WorkflowTechnique, +} from '@n8n/workflow-sdk/prompts/best-practices'; +import { mock } from 'jest-mock-extended'; + +import type { Telemetry } from '@/telemetry'; + +import { USER_CALLED_MCP_TOOL_EVENT } from '../mcp.constants'; +import { createGetWorkflowBestPracticesTool } from '../tools/workflow-builder/get-workflow-best-practices.tool'; + +jest.mock('@n8n/ai-workflow-builder', () => ({ + MCP_GET_WORKFLOW_BEST_PRACTICES_TOOL: { + toolName: 'get_workflow_best_practices', + displayTitle: 'Getting workflow best practices', + }, +})); + +describe('get-workflow-best-practices MCP tool', () => { + const user = Object.assign(new User(), { id: 'user-1' }); + let telemetry: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + telemetry = mock(); + }); + + const createTool = () => createGetWorkflowBestPracticesTool(user, telemetry); + + test('exposes the expected tool name and read-only annotations', () => { + const tool = createTool(); + + expect(tool.name).toBe('get_workflow_best_practices'); + expect(tool.config.annotations?.readOnlyHint).toBe(true); + expect(tool.config.annotations?.destructiveHint).toBe(false); + expect(tool.config.annotations?.idempotentHint).toBe(true); + expect(tool.config.inputSchema?.technique).toBeDefined(); + }); + + test('returns the full technique catalog when technique="list"', async () => { + const tool = createTool(); + const result = await tool.handler({ technique: 'list' }, {} as never); + + expect(result.structuredContent?.technique).toBe('list'); + const available = result.structuredContent?.availableTechniques as Array<{ + technique: string; + description: string; + hasDocumentation: boolean; + }>; + + expect(available).toBeDefined(); + expect(available).toHaveLength(Object.keys(TechniqueDescription).length); + + const chatbotEntry = available.find((t) => t.technique === WorkflowTechnique.CHATBOT); + expect(chatbotEntry?.hasDocumentation).toBe(true); + + const monitoringEntry = available.find((t) => t.technique === WorkflowTechnique.MONITORING); + expect(monitoringEntry?.hasDocumentation).toBe(false); + + expect(telemetry.track).toHaveBeenCalledWith( + USER_CALLED_MCP_TOOL_EVENT, + expect.objectContaining({ + user_id: 'user-1', + tool_name: 'get_workflow_best_practices', + parameters: { technique: 'list' }, + results: expect.objectContaining({ success: true }), + }), + ); + }); + + test('returns documentation for a technique that has a guide', async () => { + const tool = createTool(); + const result = await tool.handler({ technique: WorkflowTechnique.CHATBOT }, {} as never); + + const expectedDoc = bestPracticesRegistry[WorkflowTechnique.CHATBOT]!.getDocumentation(); + + expect(result.structuredContent?.technique).toBe(WorkflowTechnique.CHATBOT); + expect(result.structuredContent?.documentation).toBe(expectedDoc); + expect(result.content).toEqual([{ type: 'text', text: expectedDoc }]); + expect(telemetry.track).toHaveBeenCalledWith( + USER_CALLED_MCP_TOOL_EVENT, + expect.objectContaining({ + results: expect.objectContaining({ + success: true, + data: { technique: WorkflowTechnique.CHATBOT, hasDocumentation: true }, + }), + }), + ); + }); + + test('returns a friendly message for a known technique without documentation', async () => { + const tool = createTool(); + const result = await tool.handler({ technique: WorkflowTechnique.MONITORING }, {} as never); + + expect(result.structuredContent?.technique).toBe(WorkflowTechnique.MONITORING); + expect(result.structuredContent?.documentation).toBeUndefined(); + expect(result.structuredContent?.message).toContain('does not have detailed best-practices'); + }); +}); diff --git a/packages/cli/src/modules/mcp/mcp.service.ts b/packages/cli/src/modules/mcp/mcp.service.ts index 9ade2a08c96..07c5a51edd7 100644 --- a/packages/cli/src/modules/mcp/mcp.service.ts +++ b/packages/cli/src/modules/mcp/mcp.service.ts @@ -45,7 +45,7 @@ import { createUnpublishWorkflowTool } from './tools/unpublish-workflow.tool'; import { createCreateWorkflowFromCodeTool } from './tools/workflow-builder/create-workflow-from-code.tool'; import { createArchiveWorkflowTool } from './tools/workflow-builder/delete-workflow.tool'; import { createUpdateWorkflowTool } from './tools/workflow-builder/update-workflow.tool'; -import { createGetSuggestedWorkflowNodesTool } from './tools/workflow-builder/get-suggested-workflow-nodes.tool'; +import { createGetWorkflowBestPracticesTool } from './tools/workflow-builder/get-workflow-best-practices.tool'; import { createGetWorkflowNodeTypesTool } from './tools/workflow-builder/get-workflow-node-types.tool'; import { createGetWorkflowSdkReferenceTool } from './tools/workflow-builder/get-workflow-sdk-reference.tool'; import { getMcpInstructions } from './tools/workflow-builder/mcp-instructions'; @@ -366,15 +366,11 @@ export class McpService { ); server.registerTool(getNodeTypesTool.name, getNodeTypesTool.config, getNodeTypesTool.handler); - const suggestedNodesTool = createGetSuggestedWorkflowNodesTool( - user, - this.nodeCatalogService, - this.telemetry, - ); + const bestPracticesTool = createGetWorkflowBestPracticesTool(user, this.telemetry); server.registerTool( - suggestedNodesTool.name, - suggestedNodesTool.config, - suggestedNodesTool.handler, + bestPracticesTool.name, + bestPracticesTool.config, + bestPracticesTool.handler, ); const validateTool = createValidateWorkflowCodeTool(user, this.telemetry, this.nodeTypes); diff --git a/packages/cli/src/modules/mcp/tools/workflow-builder/constants.ts b/packages/cli/src/modules/mcp/tools/workflow-builder/constants.ts index dd913d56079..3798344114f 100644 --- a/packages/cli/src/modules/mcp/tools/workflow-builder/constants.ts +++ b/packages/cli/src/modules/mcp/tools/workflow-builder/constants.ts @@ -5,11 +5,11 @@ export { CODE_BUILDER_SEARCH_NODES_TOOL, CODE_BUILDER_GET_NODE_TYPES_TOOL, - CODE_BUILDER_GET_SUGGESTED_NODES_TOOL, CODE_BUILDER_VALIDATE_TOOL, CODE_BUILDER_VALIDATE_NODE_TOOL, MCP_GET_SDK_REFERENCE_TOOL, MCP_CREATE_WORKFLOW_FROM_CODE_TOOL, MCP_ARCHIVE_WORKFLOW_TOOL, MCP_UPDATE_WORKFLOW_TOOL, + MCP_GET_WORKFLOW_BEST_PRACTICES_TOOL, } from '@n8n/ai-workflow-builder'; 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 deleted file mode 100644 index 7bd960d4fa3..00000000000 --- a/packages/cli/src/modules/mcp/tools/workflow-builder/get-suggested-workflow-nodes.tool.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { User } from '@n8n/db'; -import z from 'zod'; - -import { USER_CALLED_MCP_TOOL_EVENT } from '../../mcp.constants'; -import type { ToolDefinition, UserCalledMCPToolEventPayload } from '../../mcp.types'; - -import type { NodeCatalogService } from '@/node-catalog'; -import type { Telemetry } from '@/telemetry'; - -import { CODE_BUILDER_GET_SUGGESTED_NODES_TOOL } from './constants'; - -const inputSchema = { - categories: z - .array(z.string()) - .min(1) - .describe( - 'Workflow technique categories. Available: chatbot, notification, scheduling, data_transformation, data_persistence, data_extraction, document_processing, form_input, content_generation, triage, scraping_and_research', - ), -} satisfies z.ZodRawShape; - -const outputSchema = { - suggestions: z - .string() - .describe('Curated node recommendations with pattern hints and configuration guidance'), -} satisfies z.ZodRawShape; - -/** - * MCP tool that returns curated node recommendations by workflow technique category. - */ -export const createGetSuggestedWorkflowNodesTool = ( - user: User, - nodeCatalogService: NodeCatalogService, - telemetry: Telemetry, -): ToolDefinition => ({ - name: CODE_BUILDER_GET_SUGGESTED_NODES_TOOL.toolName, - config: { - description: - '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: { - title: CODE_BUILDER_GET_SUGGESTED_NODES_TOOL.displayTitle, - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - }, - handler: async ({ categories }: { categories: string[] }) => { - const telemetryPayload: UserCalledMCPToolEventPayload = { - user_id: user.id, - tool_name: CODE_BUILDER_GET_SUGGESTED_NODES_TOOL.toolName, - parameters: { categories }, - }; - - try { - const result = await nodeCatalogService.getSuggestedNodes(categories); - - telemetryPayload.results = { - success: true, - data: { categoryCount: categories.length }, - }; - telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload); - - return { - content: [{ type: 'text', text: result }], - structuredContent: { suggestions: result }, - }; - } catch (error) { - telemetryPayload.results = { - success: false, - error: error instanceof Error ? error.message : String(error), - }; - telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload); - throw error; - } - }, -}); diff --git a/packages/cli/src/modules/mcp/tools/workflow-builder/get-workflow-best-practices.tool.ts b/packages/cli/src/modules/mcp/tools/workflow-builder/get-workflow-best-practices.tool.ts new file mode 100644 index 00000000000..b00ea54c9e1 --- /dev/null +++ b/packages/cli/src/modules/mcp/tools/workflow-builder/get-workflow-best-practices.tool.ts @@ -0,0 +1,155 @@ +import type { User } from '@n8n/db'; +import { + bestPracticesRegistry, + TechniqueDescription, + WorkflowTechnique, + type WorkflowTechniqueType, +} from '@n8n/workflow-sdk/prompts/best-practices'; +import z from 'zod'; + +import type { Telemetry } from '@/telemetry'; + +import { MCP_GET_WORKFLOW_BEST_PRACTICES_TOOL } from './constants'; +import { USER_CALLED_MCP_TOOL_EVENT } from '../../mcp.constants'; +import type { ToolDefinition, UserCalledMCPToolEventPayload } from '../../mcp.types'; + +const LIST_SENTINEL = 'list'; + +const inputSchema = { + technique: z + .union([z.nativeEnum(WorkflowTechnique), z.literal(LIST_SENTINEL)]) + .describe( + `Workflow technique key (e.g. "chatbot", "scheduling", "triage") to fetch best-practices guidance for. Pass "${LIST_SENTINEL}" to discover all available techniques.`, + ), +} satisfies z.ZodRawShape; + +const availableTechniqueSchema = z.object({ + technique: z.string(), + description: z.string(), + hasDocumentation: z.boolean(), +}); + +const outputSchema = { + technique: z + .string() + .describe('The requested technique key, or "list" when listing all available techniques.'), + message: z.string().describe('Human-readable summary of the response.'), + documentation: z + .string() + .optional() + .describe( + 'Best-practices documentation for the requested technique, when one with documentation was requested.', + ), + availableTechniques: z + .array(availableTechniqueSchema) + .optional() + .describe('All available techniques, returned when "list" was requested.'), +} satisfies z.ZodRawShape; + +function buildListResponse() { + const availableTechniques = Object.entries(TechniqueDescription).map(([key, description]) => ({ + technique: key, + description, + hasDocumentation: bestPracticesRegistry[key as WorkflowTechniqueType] !== undefined, + })); + + const documentedCount = availableTechniques.filter((t) => t.hasDocumentation).length; + const message = `Found ${availableTechniques.length} workflow techniques. ${documentedCount} have detailed best-practices documentation. Call this tool again with a specific technique key to fetch its guidance.`; + + const text = [ + message, + '', + ...availableTechniques.map( + (t) => + `- ${t.technique}${t.hasDocumentation ? '' : ' (no detailed documentation yet)'} — ${t.description}`, + ), + ].join('\n'); + + return { + text, + hasDocumentation: false, + structured: { + technique: LIST_SENTINEL, + message, + availableTechniques, + }, + }; +} + +function buildTechniqueResponse(technique: WorkflowTechniqueType) { + const doc = bestPracticesRegistry[technique]; + + if (doc) { + const documentation = doc.getDocumentation(); + return { + text: documentation, + hasDocumentation: true, + structured: { + technique, + message: `Best-practices documentation for "${technique}" retrieved.`, + documentation, + }, + }; + } + + const description = TechniqueDescription[technique]; + const message = `Technique "${technique}" (${description}) does not have detailed best-practices documentation yet — proceed with general n8n knowledge.`; + return { text: message, hasDocumentation: false, structured: { technique, message } }; +} + +/** + * MCP tool that returns workflow design best-practices for a given workflow technique. + * Sources guidance directly from `bestPracticesRegistry` in `@n8n/workflow-sdk`. + */ +export const createGetWorkflowBestPracticesTool = ( + user: User, + telemetry: Telemetry, +): ToolDefinition => ({ + name: MCP_GET_WORKFLOW_BEST_PRACTICES_TOOL.toolName, + config: { + description: + 'Required workflow-planning step. Get best-practices guidance (recommended nodes, patterns, and common pitfalls) for a specific workflow technique before searching for nodes or writing code. Call once per relevant technique. Use technique="list" first if unsure which techniques apply.', + inputSchema, + outputSchema, + annotations: { + title: MCP_GET_WORKFLOW_BEST_PRACTICES_TOOL.displayTitle, + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, + handler: async ({ technique }: { technique: WorkflowTechniqueType | typeof LIST_SENTINEL }) => { + const telemetryPayload: UserCalledMCPToolEventPayload = { + user_id: user.id, + tool_name: MCP_GET_WORKFLOW_BEST_PRACTICES_TOOL.toolName, + parameters: { technique }, + }; + + try { + const response = + technique === LIST_SENTINEL ? buildListResponse() : buildTechniqueResponse(technique); + + telemetryPayload.results = { + success: true, + data: { + technique, + hasDocumentation: response.hasDocumentation, + }, + }; + telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload); + + return { + content: [{ type: 'text', text: response.text }], + structuredContent: response.structured, + }; + } catch (error) { + telemetryPayload.results = { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload); + throw error; + } + }, +}); 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 b1ff96870a8..12606c9a655 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 @@ -12,7 +12,7 @@ import { MCP_ARCHIVE_WORKFLOW_TOOL, CODE_BUILDER_GET_NODE_TYPES_TOOL, MCP_GET_SDK_REFERENCE_TOOL, - CODE_BUILDER_GET_SUGGESTED_NODES_TOOL, + MCP_GET_WORKFLOW_BEST_PRACTICES_TOOL, CODE_BUILDER_SEARCH_NODES_TOOL, CODE_BUILDER_VALIDATE_TOOL, CODE_BUILDER_VALIDATE_NODE_TOOL, @@ -27,7 +27,7 @@ To build n8n workflows, follow these steps in order: 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. 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. +2. Get workflow best practices: You MUST call ${MCP_GET_WORKFLOW_BEST_PRACTICES_TOOL.toolName} for each workflow technique relevant to the user's request (e.g. "chatbot", "scheduling", "triage"). Call once per technique. Use the returned design guidance, recommended nodes, and common pitfalls to decide which nodes and patterns to use. If you are unsure which techniques apply, call this tool with technique="list" first to see all available techniques. 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. diff --git a/packages/testing/playwright/tests/e2e/mcp/mcp-service.spec.ts b/packages/testing/playwright/tests/e2e/mcp/mcp-service.spec.ts index 3abd0e85c70..0245fc3805c 100644 --- a/packages/testing/playwright/tests/e2e/mcp/mcp-service.spec.ts +++ b/packages/testing/playwright/tests/e2e/mcp/mcp-service.spec.ts @@ -19,7 +19,7 @@ import { test, expect } from '../../../fixtures/base'; * Builder tools (enabled via N8N_MCP_BUILDER_ENABLED): * - search_nodes: Search for n8n nodes by service name/trigger type * - get_node_types: Get TypeScript type definitions for nodes - * - get_suggested_nodes: Get curated node recommendations by category + * - get_workflow_best_practices: Get best-practices guidance for a workflow technique * - validate_workflow: Validate n8n Workflow SDK code * - create_workflow_from_code: Create a workflow from validated SDK code * - archive_workflow: Archive a workflow by ID