mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-04 02:37:46 +02:00
feat(core): Add validate_node_config MCP tool for per-node validation (#31047)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fb51c309e8
commit
166eb85509
|
|
@ -93,6 +93,11 @@ export const CODE_BUILDER_VALIDATE_TOOL: BuilderToolBase = {
|
|||
displayTitle: 'Validating workflow',
|
||||
};
|
||||
|
||||
export const CODE_BUILDER_VALIDATE_NODE_TOOL: BuilderToolBase = {
|
||||
toolName: 'validate_node_config',
|
||||
displayTitle: 'Validating node config',
|
||||
};
|
||||
|
||||
export const CODE_BUILDER_SEARCH_NODES_TOOL: BuilderToolBase = {
|
||||
toolName: 'search_nodes',
|
||||
displayTitle: 'Searching nodes',
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,307 @@
|
|||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import { User } from '@n8n/db';
|
||||
|
||||
import { createValidateNodeTool } from '../tools/workflow-builder/validate-node.tool';
|
||||
|
||||
import { Telemetry } from '@/telemetry';
|
||||
|
||||
const mockValidateNodeConfig = jest.fn();
|
||||
|
||||
jest.mock('@n8n/workflow-sdk', () => ({
|
||||
validateNodeConfig: (...args: unknown[]) => mockValidateNodeConfig(...args),
|
||||
}));
|
||||
|
||||
jest.mock('@n8n/ai-workflow-builder', () => ({
|
||||
CODE_BUILDER_VALIDATE_NODE_TOOL: {
|
||||
toolName: 'validate_node_config',
|
||||
displayTitle: 'Validating node config',
|
||||
},
|
||||
}));
|
||||
|
||||
/** Parse the first text content item from a tool result */
|
||||
const parseResult = (result: { content: Array<{ type: string; text?: string }> }) =>
|
||||
JSON.parse((result.content[0] as { type: 'text'; text: string }).text) as Record<string, unknown>;
|
||||
|
||||
describe('validate-node MCP tool', () => {
|
||||
const user = Object.assign(new User(), { id: 'user-1' });
|
||||
let telemetry: Telemetry;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
telemetry = mockInstance(Telemetry, { track: jest.fn() });
|
||||
});
|
||||
|
||||
const createTool = () => createValidateNodeTool(user, telemetry);
|
||||
|
||||
describe('smoke tests', () => {
|
||||
test('creates tool with correct name and read-only annotations', () => {
|
||||
const tool = createTool();
|
||||
|
||||
expect(tool.name).toBe('validate_node_config');
|
||||
expect(tool.config.annotations).toEqual(
|
||||
expect.objectContaining({
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: false,
|
||||
}),
|
||||
);
|
||||
expect(typeof tool.handler).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handler tests', () => {
|
||||
test('returns valid=true for a single valid node', async () => {
|
||||
mockValidateNodeConfig.mockReturnValue({ valid: true, errors: [] });
|
||||
|
||||
const tool = createTool();
|
||||
const result = await tool.handler(
|
||||
{
|
||||
nodes: [
|
||||
{
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3,
|
||||
parameters: { mode: 'manual', assignments: { assignments: [] } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{} as never,
|
||||
);
|
||||
|
||||
const response = parseResult(result);
|
||||
expect(response.valid).toBe(true);
|
||||
expect(response.results).toEqual([
|
||||
{
|
||||
index: 0,
|
||||
type: 'n8n-nodes-base.set',
|
||||
valid: true,
|
||||
},
|
||||
]);
|
||||
expect(result.isError).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns structured errors for invalid node', async () => {
|
||||
mockValidateNodeConfig.mockReturnValue({
|
||||
valid: false,
|
||||
errors: [{ path: 'mode', message: 'Invalid value: expected "manual" or "raw"' }],
|
||||
});
|
||||
|
||||
const tool = createTool();
|
||||
const result = await tool.handler(
|
||||
{
|
||||
nodes: [
|
||||
{
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3,
|
||||
parameters: { mode: 'bogus' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{} as never,
|
||||
);
|
||||
|
||||
const response = parseResult(result);
|
||||
expect(response.valid).toBe(false);
|
||||
expect(response.results).toEqual([
|
||||
{
|
||||
index: 0,
|
||||
type: 'n8n-nodes-base.set',
|
||||
valid: false,
|
||||
errors: [{ path: 'mode', message: 'Invalid value: expected "manual" or "raw"' }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('mixes valid and invalid nodes — top-level valid is false', async () => {
|
||||
mockValidateNodeConfig.mockReturnValueOnce({ valid: true, errors: [] }).mockReturnValueOnce({
|
||||
valid: false,
|
||||
errors: [{ path: 'url', message: 'URL is required' }],
|
||||
});
|
||||
|
||||
const tool = createTool();
|
||||
const result = await tool.handler(
|
||||
{
|
||||
nodes: [
|
||||
{ name: 'Set', type: 'n8n-nodes-base.set', typeVersion: 3, parameters: {} },
|
||||
{ name: 'HTTP', type: 'n8n-nodes-base.httpRequest', typeVersion: 4, parameters: {} },
|
||||
],
|
||||
},
|
||||
{} as never,
|
||||
);
|
||||
|
||||
const response = parseResult(result);
|
||||
expect(response.valid).toBe(false);
|
||||
const results = response.results as Array<Record<string, unknown>>;
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0]).toEqual({
|
||||
index: 0,
|
||||
name: 'Set',
|
||||
type: 'n8n-nodes-base.set',
|
||||
valid: true,
|
||||
});
|
||||
expect(results[1]).toEqual({
|
||||
index: 1,
|
||||
name: 'HTTP',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
valid: false,
|
||||
errors: [{ path: 'url', message: 'URL is required' }],
|
||||
});
|
||||
});
|
||||
|
||||
test('forwards typeVersion and parameters as received from Zod-parsed input', async () => {
|
||||
// At runtime the MCP framework Zod-parses input before calling the handler,
|
||||
// so `.default(1)` for typeVersion and `.default({})` for parameters are
|
||||
// already applied. This test verifies the handler forwards them unchanged.
|
||||
mockValidateNodeConfig.mockReturnValue({ valid: true, errors: [] });
|
||||
|
||||
const tool = createTool();
|
||||
await tool.handler(
|
||||
{
|
||||
nodes: [{ type: 'n8n-nodes-base.noOp', typeVersion: 1, parameters: {} }],
|
||||
},
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(mockValidateNodeConfig).toHaveBeenCalledWith(
|
||||
'n8n-nodes-base.noOp',
|
||||
1,
|
||||
{ parameters: {}, subnodes: undefined },
|
||||
{ isToolNode: undefined },
|
||||
);
|
||||
});
|
||||
|
||||
test('forwards isToolNode to validateNodeConfig options', async () => {
|
||||
mockValidateNodeConfig.mockReturnValue({ valid: true, errors: [] });
|
||||
|
||||
const tool = createTool();
|
||||
await tool.handler(
|
||||
{
|
||||
nodes: [
|
||||
{
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4,
|
||||
parameters: { url: 'https://example.com' },
|
||||
isToolNode: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(mockValidateNodeConfig).toHaveBeenCalledWith(
|
||||
'n8n-nodes-base.httpRequest',
|
||||
4,
|
||||
{ parameters: { url: 'https://example.com' }, subnodes: undefined },
|
||||
{ isToolNode: true },
|
||||
);
|
||||
});
|
||||
|
||||
test('forwards subnodes inside the config object', async () => {
|
||||
mockValidateNodeConfig.mockReturnValue({ valid: true, errors: [] });
|
||||
const subnodes = {
|
||||
model: { type: '@n8n/n8n-nodes-langchain.lmChatOpenAi', version: 1 },
|
||||
};
|
||||
|
||||
const tool = createTool();
|
||||
await tool.handler(
|
||||
{
|
||||
nodes: [
|
||||
{
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
parameters: { agent: 'conversationalAgent' },
|
||||
subnodes,
|
||||
},
|
||||
],
|
||||
},
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(mockValidateNodeConfig).toHaveBeenCalledWith(
|
||||
'@n8n/n8n-nodes-langchain.agent',
|
||||
1,
|
||||
{ parameters: { agent: 'conversationalAgent' }, subnodes },
|
||||
{ isToolNode: undefined },
|
||||
);
|
||||
});
|
||||
|
||||
test('passes through graceful-fallback result for unknown node types', async () => {
|
||||
// validateNodeConfig returns valid:true when no schema is registered
|
||||
mockValidateNodeConfig.mockReturnValue({ valid: true, errors: [] });
|
||||
|
||||
const tool = createTool();
|
||||
const result = await tool.handler(
|
||||
{
|
||||
nodes: [
|
||||
{ type: 'community-node-without-schema', typeVersion: 1, parameters: { foo: 'bar' } },
|
||||
],
|
||||
},
|
||||
{} as never,
|
||||
);
|
||||
|
||||
const response = parseResult(result);
|
||||
expect(response.valid).toBe(true);
|
||||
expect((response.results as Array<{ valid: boolean }>)[0].valid).toBe(true);
|
||||
});
|
||||
|
||||
test('tracks telemetry on success with nodeCount, invalidCount, and errorCount', async () => {
|
||||
mockValidateNodeConfig.mockReturnValueOnce({ valid: true, errors: [] }).mockReturnValueOnce({
|
||||
valid: false,
|
||||
errors: [
|
||||
{ path: 'a', message: 'bad a' },
|
||||
{ path: 'b', message: 'bad b' },
|
||||
],
|
||||
});
|
||||
|
||||
const tool = createTool();
|
||||
await tool.handler(
|
||||
{
|
||||
nodes: [
|
||||
{ type: 'n8n-nodes-base.set', typeVersion: 3, parameters: {} },
|
||||
{ type: 'n8n-nodes-base.httpRequest', typeVersion: 4, parameters: {} },
|
||||
],
|
||||
},
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(telemetry.track).toHaveBeenCalledWith(
|
||||
'User called mcp tool',
|
||||
expect.objectContaining({
|
||||
user_id: 'user-1',
|
||||
tool_name: 'validate_node_config',
|
||||
parameters: expect.objectContaining({ nodeCount: 2 }),
|
||||
results: expect.objectContaining({
|
||||
success: true,
|
||||
data: expect.objectContaining({ invalidCount: 1, errorCount: 2 }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('tracks telemetry on failure when validateNodeConfig throws', async () => {
|
||||
mockValidateNodeConfig.mockImplementation(() => {
|
||||
throw new Error('schema load failed');
|
||||
});
|
||||
|
||||
const tool = createTool();
|
||||
const result = await tool.handler(
|
||||
{
|
||||
nodes: [{ type: 'n8n-nodes-base.set', typeVersion: 3, parameters: {} }],
|
||||
},
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(telemetry.track).toHaveBeenCalledWith(
|
||||
'User called mcp tool',
|
||||
expect.objectContaining({
|
||||
tool_name: 'validate_node_config',
|
||||
results: expect.objectContaining({
|
||||
success: false,
|
||||
error: 'schema load failed',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -51,6 +51,7 @@ import { createGetWorkflowSdkReferenceTool } from './tools/workflow-builder/get-
|
|||
import { getMcpInstructions } from './tools/workflow-builder/mcp-instructions';
|
||||
import { createSearchWorkflowNodesTool } from './tools/workflow-builder/search-workflow-nodes.tool';
|
||||
import { getSdkReferenceContent } from './tools/workflow-builder/sdk-reference-content';
|
||||
import { createValidateNodeTool } from './tools/workflow-builder/validate-node.tool';
|
||||
import { createValidateWorkflowCodeTool } from './tools/workflow-builder/validate-workflow-code.tool';
|
||||
import { NodeCatalogService } from '@/node-catalog';
|
||||
|
||||
|
|
@ -379,6 +380,9 @@ export class McpService {
|
|||
const validateTool = createValidateWorkflowCodeTool(user, this.telemetry, this.nodeTypes);
|
||||
server.registerTool(validateTool.name, validateTool.config, validateTool.handler);
|
||||
|
||||
const validateNodeTool = createValidateNodeTool(user, this.telemetry);
|
||||
server.registerTool(validateNodeTool.name, validateNodeTool.config, validateNodeTool.handler);
|
||||
|
||||
const createTool = createCreateWorkflowFromCodeTool(
|
||||
user,
|
||||
this.workflowCreationService,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
CODE_BUILDER_GET_SUGGESTED_NODES_TOOL,
|
||||
CODE_BUILDER_SEARCH_NODES_TOOL,
|
||||
CODE_BUILDER_VALIDATE_TOOL,
|
||||
CODE_BUILDER_VALIDATE_NODE_TOOL,
|
||||
} from './constants';
|
||||
|
||||
export function getMcpInstructions(isBuilderEnabled: boolean): string {
|
||||
|
|
@ -34,13 +35,15 @@ To build n8n workflows, follow these steps in order:
|
|||
|
||||
5. Write the workflow code using the SDK patterns from the reference and the exact parameter names from the type definitions. Follow the coding guidelines and design guidance sections of the SDK reference (retrieve them with ${MCP_GET_SDK_REFERENCE_TOOL.toolName} using sections "guidelines" and "design").
|
||||
|
||||
6. Validate: Call ${CODE_BUILDER_VALIDATE_TOOL.toolName} with your full code. Fix any errors and re-validate until valid.
|
||||
6. Spot-check as you go: after configuring each node, call ${CODE_BUILDER_VALIDATE_NODE_TOOL.toolName} on it before wiring it into the rest of the workflow. Catches param, type, and discriminator errors per-node with a clean signal, before they're buried inside a full-graph ${CODE_BUILDER_VALIDATE_TOOL.toolName} run. Can check several candidate configs in one call so you wire only the one that passes.
|
||||
|
||||
7. Create: Call ${MCP_CREATE_WORKFLOW_FROM_CODE_TOOL.toolName} with the validated code to save the workflow to n8n. Include a short \`description\` (1-2 sentences) summarizing what the workflow does — this helps users find and understand their workflows.
|
||||
7. Validate: Call ${CODE_BUILDER_VALIDATE_TOOL.toolName} with your full code. Fix any errors and re-validate until valid.
|
||||
|
||||
8. Update: Call ${MCP_UPDATE_WORKFLOW_TOOL.toolName} with the workflow ID and a list of operations (addNode, removeNode, updateNodeParameters, setNodeParameter, renameNode, addConnection, removeConnection, setNodeCredential, setNodePosition, setNodeDisabled, setNodeSettings, setWorkflowMetadata). The whole batch is atomic: if any op fails the workflow is unchanged. To modify an existing node's configuration, use updateNodeParameters or setNodeParameter — do NOT use removeNode followed by addNode for the same node, as this disconnects any attached sub-nodes (LLM models, memory, tools) and they will not be re-attached automatically. Use setNodeSettings to change a node's execution behavior (onError, retryOnFail, maxTries, waitBetweenTries, alwaysOutputData, executeOnce); for sub-nodes (LLM model, memory, tools) this is the only way to set onError, because the canvas UI does not expose that setting for them.
|
||||
8. Create: Call ${MCP_CREATE_WORKFLOW_FROM_CODE_TOOL.toolName} with the validated code to save the workflow to n8n. Include a short \`description\` (1-2 sentences) summarizing what the workflow does — this helps users find and understand their workflows.
|
||||
|
||||
9. Archive: Call ${MCP_ARCHIVE_WORKFLOW_TOOL.toolName} with the workflow ID.`;
|
||||
9. Update: Call ${MCP_UPDATE_WORKFLOW_TOOL.toolName} with the workflow ID and a list of operations (addNode, removeNode, updateNodeParameters, setNodeParameter, renameNode, addConnection, removeConnection, setNodeCredential, setNodePosition, setNodeDisabled, setNodeSettings, setWorkflowMetadata). The whole batch is atomic: if any op fails the workflow is unchanged. To modify an existing node's configuration, use updateNodeParameters or setNodeParameter — do NOT use removeNode followed by addNode for the same node, as this disconnects any attached sub-nodes (LLM models, memory, tools) and they will not be re-attached automatically. Use setNodeSettings to change a node's execution behavior (onError, retryOnFail, maxTries, waitBetweenTries, alwaysOutputData, executeOnce); for sub-nodes (LLM model, memory, tools) this is the only way to set onError, because the canvas UI does not expose that setting for them.
|
||||
|
||||
10. Archive: Call ${MCP_ARCHIVE_WORKFLOW_TOOL.toolName} with the workflow ID.`;
|
||||
|
||||
return isBuilderEnabled ? `${INTRO}\n\n${BUILDER_INSTRUCTIONS}` : INTRO;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
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 { CODE_BUILDER_VALIDATE_NODE_TOOL } from './constants';
|
||||
|
||||
import type { Telemetry } from '@/telemetry';
|
||||
|
||||
const nodeInputSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional node name. Echoed back in the result so callers can correlate.'),
|
||||
type: z
|
||||
.string()
|
||||
.describe('Full node type, e.g. "n8n-nodes-base.set" or "@n8n/n8n-nodes-langchain.agent".'),
|
||||
typeVersion: z.number().positive().default(1).describe('Node type version. Defaults to 1.'),
|
||||
parameters: z
|
||||
.record(z.unknown())
|
||||
.default({})
|
||||
.describe('Node parameters object — same shape as workflow JSON.'),
|
||||
subnodes: z
|
||||
.unknown()
|
||||
.optional()
|
||||
.describe(
|
||||
'Optional subnode config for AI parent nodes (e.g. langchain agent): `{ model, memory, tools: [...] }` of `{ type, version }` refs.',
|
||||
),
|
||||
isToolNode: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
'Set to true when validating a node that is wired as an AI tool subnode (ai_tool connection). Adjusts which displayOptions branch is evaluated.',
|
||||
),
|
||||
});
|
||||
|
||||
const inputSchema = {
|
||||
nodes: z
|
||||
.array(nodeInputSchema)
|
||||
.min(1)
|
||||
.max(50)
|
||||
.describe('One or more node configurations to validate independently.'),
|
||||
} satisfies z.ZodRawShape;
|
||||
|
||||
const outputSchema = {
|
||||
valid: z.boolean().describe('True when every node is valid.'),
|
||||
results: z
|
||||
.array(
|
||||
z.object({
|
||||
index: z.number().describe('Position of this node in the input array.'),
|
||||
name: z.string().optional().describe('Echo of the input name, if provided.'),
|
||||
type: z.string().describe('Echo of the input node type.'),
|
||||
valid: z.boolean().describe('Whether this node config is valid.'),
|
||||
errors: z
|
||||
.array(
|
||||
z.object({
|
||||
path: z.string().describe('Parameter path of the error.'),
|
||||
message: z.string().describe('Human-readable error message.'),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.describe('Validation errors for this node (omitted when valid).'),
|
||||
}),
|
||||
)
|
||||
.describe('Per-node validation results, in input order.'),
|
||||
error: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Top-level error message when validation could not run at all (e.g. internal failure loading the schema). Omitted on success.',
|
||||
),
|
||||
} satisfies z.ZodRawShape;
|
||||
|
||||
/**
|
||||
* MCP tool that validates individual node configurations against their
|
||||
* generated Zod schema. Schema-level only — does not check workflow-level
|
||||
* concerns (connections, triggers, disconnected nodes, credential existence).
|
||||
* For those, use `validate_workflow` on full workflow code.
|
||||
*/
|
||||
export const createValidateNodeTool = (
|
||||
user: User,
|
||||
telemetry: Telemetry,
|
||||
): ToolDefinition<typeof inputSchema> => ({
|
||||
name: CODE_BUILDER_VALIDATE_NODE_TOOL.toolName,
|
||||
config: {
|
||||
description:
|
||||
"Validate a node's config the moment you write it — before assembling create_workflow_from_code or calling update_workflow. Read-only and needs no existing workflow, so use it freely while composing. Unlike the write tools (which validate only as they mutate), this returns isolated per-node, per-parameter errors with no graph noise, and can check several candidate configs in one call so you wire only the one that passes. For langchain tool subnodes (nodes wired via ai_tool), set isToolNode: true so the schema evaluates the correct displayOptions branch. Schema-level only — for connections, required inputs, triggers, and credentials use validate_workflow.",
|
||||
inputSchema,
|
||||
outputSchema,
|
||||
annotations: {
|
||||
title: CODE_BUILDER_VALIDATE_NODE_TOOL.displayTitle,
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: false,
|
||||
},
|
||||
},
|
||||
handler: async ({ nodes }) => {
|
||||
const telemetryPayload: UserCalledMCPToolEventPayload = {
|
||||
user_id: user.id,
|
||||
tool_name: CODE_BUILDER_VALIDATE_NODE_TOOL.toolName,
|
||||
parameters: { nodeCount: nodes.length },
|
||||
};
|
||||
|
||||
try {
|
||||
const { validateNodeConfig } = await import('@n8n/workflow-sdk');
|
||||
|
||||
const results = nodes.map((node, index) => {
|
||||
const result = validateNodeConfig(
|
||||
node.type,
|
||||
node.typeVersion,
|
||||
{ parameters: node.parameters, subnodes: node.subnodes },
|
||||
{ isToolNode: node.isToolNode },
|
||||
);
|
||||
|
||||
return {
|
||||
index,
|
||||
...(node.name !== undefined ? { name: node.name } : {}),
|
||||
type: node.type,
|
||||
valid: result.valid,
|
||||
...(result.valid ? {} : { errors: result.errors }),
|
||||
};
|
||||
});
|
||||
|
||||
const valid = results.every((r) => r.valid);
|
||||
const invalidCount = results.filter((r) => !r.valid).length;
|
||||
const errorCount = results.reduce((sum, r) => sum + (r.errors?.length ?? 0), 0);
|
||||
|
||||
telemetryPayload.results = {
|
||||
success: true,
|
||||
data: { invalidCount, errorCount },
|
||||
};
|
||||
telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload);
|
||||
|
||||
const response = { valid, results };
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(response, null, 2) }],
|
||||
structuredContent: response,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
telemetryPayload.results = {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload);
|
||||
|
||||
const output = {
|
||||
valid: false,
|
||||
results: [],
|
||||
error: errorMessage,
|
||||
};
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
||||
structuredContent: output,
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user