feat(core): Replace get_suggested_nodes MCP tool with get_workflow_best_practices (#31048)

This commit is contained in:
Ricardo Espinoza 2026-06-02 08:35:19 -04:00 committed by GitHub
parent dd5d539398
commit cc9fa172c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 272 additions and 91 deletions

View File

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

View File

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

View File

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

View File

@ -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<Telemetry>;
beforeEach(() => {
jest.clearAllMocks();
telemetry = mock<Telemetry>();
});
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');
});
});

View File

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

View File

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

View File

@ -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<typeof inputSchema> => ({
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;
}
},
});

View File

@ -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<typeof inputSchema> => ({
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;
}
},
});

View File

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

View File

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