{"record":{"id":"8aafcaca74ec654f","repo":"n8n-io/n8n","slug":"mcp-server-cfg-name-exactly-one-of-url-or","errorCode":null,"errorMessage":"MCP server \"${cfg.name}\": exactly one of \"url\" or \"command\" must be provided","messagePattern":"MCP server \"(.+?)\": exactly one of \"url\" or \"command\" must be provided","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/@n8n/agents/src/sdk/mcp-client.ts","lineNumber":66,"sourceCode":"\tprivate listToolsPromise: Promise<BuiltTool[]> | undefined;\n\n\tprivate closePromise: Promise<void> | undefined;\n\n\t/**\n\t * Per-server connection failures recorded during the last `listTools()`.\n\t * Tools from these servers were skipped; the run continues with the\n\t * remaining servers' tools. Read via `getConnectionFailures()`.\n\t */\n\tprivate connectionFailures: McpConnectionFailedEvent[] = [];\n\n\t/**\n\t * @param configs - Server configurations. Each must have either `url` or `command`.\n\t *   Duplicate names within the list are rejected.\n\t */\n\tconstructor(configs: McpServerConfig[]) {\n\t\tfor (const cfg of configs) {\n\t\t\tif (!cfg.url && !cfg.command) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`MCP server \"${cfg.name}\": exactly one of \"url\" or \"command\" must be provided`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (cfg.url && cfg.command) {\n\t\t\t\tthrow new Error(`MCP server \"${cfg.name}\": provide either \"url\" or \"command\", not both`);\n\t\t\t}\n\t\t}\n\n\t\tconst seen = new Set<string>();\n\t\tfor (const cfg of configs) {\n\t\t\tif (seen.has(cfg.name)) {\n\t\t\t\tthrow new Error(`MCP server name \"${cfg.name}\" is already registered`);\n\t\t\t}\n\t\t\tseen.add(cfg.name);\n\t\t}\n\n\t\tthis.configs = configs;\n\t\tthis.connections = configs.map((cfg) => new McpConnection(cfg));","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/agents/src/sdk/mcp-client.ts#L48-L84","documentation":"Validation in the McpClient constructor over the supplied server configs. It fires when a server entry sets neither `url` (remote HTTP transport) nor `command` (local stdio transport), leaving the client unable to open any connection. Enforces the documented contract that each config must carry exactly one transport.","triggerScenarios":"Passing { name: 'foo' } (or { name: 'foo', transport: 'sse' }) with no url and no command to new McpClient([...]).","commonSituations":"Building McpServerConfig from env vars where both URL and command env entries are empty; typo in config key (e.g. cmd vs command); loading from JSON whose transport field was omitted.","solutions":["Set cfg.url for SSE/StreamableHTTP transports or cfg.command for stdio.","Validate configs with a schema (zod) before passing to McpClient.","Log the offending config name early to pinpoint which entry is incomplete."],"exampleFix":"// before\nnew McpClient([{ name: 'fs' }]);\n// after\nnew McpClient([{ name: 'fs', command: 'npx', args: ['@anthropic/mcp-fs', '/tmp'] }]);","handlingStrategy":"validation","validationCode":"import { z } from 'zod';\n\nconst mcpConfigSchema = z.object({\n  name: z.string().min(1),\n  url: z.string().url().optional(),\n  command: z.string().optional(),\n}).refine((c) => Boolean(c.url) !== Boolean(c.command), {\n  message: 'Provide exactly one of url or command',\n});\nmcpConfigSchema.parse(cfg);","typeGuard":"function hasMcpTransport(c: { url?: string; command?: string }): boolean {\n  return Boolean(c.url) !== Boolean(c.command);\n}","tryCatchPattern":null,"preventionTips":["Validate McpServerConfig arrays with zod before constructing McpClient.","Centralize config construction in a factory that requires a transport discriminator."],"tags":["mcp","config","validation","transport"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","contentChangedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}