feat(ai-builder): Format zod erros on agent config - better toast msg (#34867)

This commit is contained in:
Anne Aguirre
2026-07-24 16:24:59 +01:00
committed by GitHub
parent 490b7220e1
commit bac8debc0e
5 changed files with 92 additions and 11 deletions
@@ -1,6 +1,7 @@
import {
AgentJsonConfigSchema,
findVectorStoreToolNameCollisions,
formatAgentConfigZodError,
} from '../agent-json-config.schema';
const minimalConfig = {
@@ -542,3 +543,32 @@ describe('AgentJsonConfigSchema — model/credential coupling', () => {
expect(result.success).toBe(true);
});
});
describe('formatAgentConfigZodError', () => {
it('formats an invalid MCP server name as path: message without a Zod JSON dump', () => {
const result = AgentJsonConfigSchema.safeParse({
...minimalConfig,
mcpServers: [
{
name: 'has spaces',
url: 'https://example.com/mcp',
transport: 'streamableHttp',
authentication: 'none',
},
],
});
expect(result.success).toBe(false);
if (result.success) return;
const formatted = formatAgentConfigZodError(result.error);
expect(formatted).toContain('mcpServers.0.name');
expect(formatted).toContain(
'MCP server name can only contain letters, numbers, hyphens, and underscores',
);
expect(formatted).not.toContain('"validation": "regex"');
expect(result.error.issues[0]?.message).toBe(
'MCP server name can only contain letters, numbers, hyphens, and underscores',
);
});
});
@@ -90,7 +90,9 @@ const WebSearchConfigSchema = z.object({
credential: z.string().optional(),
});
const HexColorSchema = z.string().regex(/^#[0-9A-Fa-f]{6}$/);
const HexColorSchema = z
.string()
.regex(/^#[0-9A-Fa-f]{6}$/, 'Color must be a 6-digit hex value (e.g. #FF5500)');
export const DEFAULT_AGENT_PERSONALISATION = {
icon: 'bot',
@@ -196,7 +198,10 @@ const AgentJsonSkillConfigSchema = z.object({
id: z
.string()
.min(1)
.regex(/^[A-Za-z0-9_-]+$/),
.regex(
/^[A-Za-z0-9_-]+$/,
'Skill id can only contain letters, numbers, hyphens, and underscores',
),
});
const AgentJsonTaskConfigSchema = z.object({
@@ -204,7 +209,10 @@ const AgentJsonTaskConfigSchema = z.object({
id: z
.string()
.min(1)
.regex(/^[A-Za-z0-9_-]+$/),
.regex(
/^[A-Za-z0-9_-]+$/,
'Task id can only contain letters, numbers, hyphens, and underscores',
),
enabled: z.boolean(),
});
@@ -228,6 +236,10 @@ export const McpServerConfigSchema = z
.string()
.min(1)
.max(64)
.regex(
/^[a-zA-Z0-9_-]+$/,
'MCP server name can only contain letters, numbers, hyphens, and underscores',
)
.refine((name) => name.trim().length > 0, 'MCP server name cannot be blank')
.describe('Unique display name. The SDK normalizes it when building model-facing tool names'),
description: z.string().max(512).optional().describe('Human-readable server description'),
@@ -323,7 +335,10 @@ const VectorStoreBaseShape = {
.string()
.min(1)
.max(64)
.regex(VECTOR_STORE_NAME_REGEX)
.regex(
VECTOR_STORE_NAME_REGEX,
'Vector store name can only contain letters, numbers, hyphens, and underscores',
)
.describe('Unique connection name, also used as the SDK tool-name suffix: search_<name>'),
credential: CredentialIdSchema,
useWhen: z.string().trim().min(1).max(VECTOR_STORE_USE_WHEN_MAX_LENGTH),
@@ -365,7 +380,13 @@ export const AgentVectorStoreConfigSchema = z.discriminatedUnion('provider', [
const CustomToolJsonConfigSchema = z.object({
type: z.literal('custom'),
id: z.string().min(1).regex(CUSTOM_TOOL_ID_REGEX),
id: z
.string()
.min(1)
.regex(
CUSTOM_TOOL_ID_REGEX,
'Custom tool id can only contain letters, numbers, and underscores',
),
requireApproval: z.boolean().optional(),
});
@@ -574,3 +595,9 @@ export function formatZodErrors(error: ZodError): ConfigValidationError[] {
received: 'received' in issue ? String(issue.received) : undefined,
}));
}
export function formatAgentConfigZodError(error: ZodError): string {
return formatZodErrors(error)
.map((issue) => `${issue.path}: ${issue.message}`)
.join('; ');
}
@@ -162,6 +162,30 @@ describe('AgentConfigService', () => {
}),
).resolves.toMatchObject({ valid: true });
});
it('returns a human-readable Zod error for an invalid MCP server name', async () => {
const { service } = makeService();
const result = await service.validateConfig({
...baseConfig,
mcpServers: [
{
name: 'has spaces',
url: 'https://example.com/mcp',
transport: 'streamableHttp',
authentication: 'none',
},
],
});
expect(result.valid).toBe(false);
if (result.valid) return;
expect(result.error).toContain('mcpServers.0.name');
expect(result.error).toContain(
'MCP server name can only contain letters, numbers, hyphens, and underscores',
);
expect(result.error).not.toContain('"validation": "regex"');
});
});
describe('updateConfig', () => {
@@ -2,6 +2,7 @@ import { reconcileNativeWebSearch } from '@n8n/ai-utilities/agent-config';
import {
AgentJsonConfigSchema,
findVectorStoreToolNameCollisions,
formatAgentConfigZodError,
sanitizeAgentJsonConfig,
type AgentJsonConfig,
type AgentJsonToolConfig,
@@ -66,7 +67,7 @@ export class AgentConfigService {
const parsed = AgentJsonConfigSchema.safeParse(sanitizeAgentJsonConfig(raw));
if (!parsed.success) {
return { valid: false, error: parsed.error.message };
return { valid: false, error: formatAgentConfigZodError(parsed.error) };
}
const config = parsed.data;
@@ -2,7 +2,7 @@ import type { Agent as RuntimeAgent, BuiltAgent, BuiltTool, CredentialProvider }
import type { AgentJsonConfig, AgentSkill } from '@n8n/api-types';
import {
AGENT_WORKFLOW_TRIGGER_TYPE,
formatZodErrors,
formatAgentConfigZodError,
RunnableInlineAgentConfigSchema,
sanitizeAgentJsonConfig,
sanitizeAgentSkillBodies,
@@ -556,10 +556,9 @@ export class AgentWorkflowExecutionService {
...(payload.skills !== undefined ? { skills: sanitizeAgentSkillBodies(payload.skills) } : {}),
});
if (!parsed.success) {
const details = formatZodErrors(parsed.error)
.map((issue) => `${issue.path}: ${issue.message}`)
.join('; ');
throw new UserError(`Invalid inline agent configuration: ${details}`);
throw new UserError(
`Invalid inline agent configuration: ${formatAgentConfigZodError(parsed.error)}`,
);
}
const config = parsed.data.config;