n8n-io/n8n · error · Error

MCP server " ": exactly one of "url" or "command" must be…

Error message

MCP server "${cfg.name}": exactly one of "url" or "command" must be provided

What it means

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.

Solutions

  1. Set cfg.url for SSE/StreamableHTTP transports or cfg.command for stdio.
  2. Validate configs with a schema (zod) before passing to McpClient.
  3. Log the offending config name early to pinpoint which entry is incomplete.

Example fix

// before
new McpClient([{ name: 'fs' }]);
// after
new McpClient([{ name: 'fs', command: 'npx', args: ['@anthropic/mcp-fs', '/tmp'] }]);
Defensive patterns

Strategy: validation

Validate before calling

import { z } from 'zod';

const mcpConfigSchema = z.object({
  name: z.string().min(1),
  url: z.string().url().optional(),
  command: z.string().optional(),
}).refine((c) => Boolean(c.url) !== Boolean(c.command), {
  message: 'Provide exactly one of url or command',
});
mcpConfigSchema.parse(cfg);

Type guard

function hasMcpTransport(c: { url?: string; command?: string }): boolean {
  return Boolean(c.url) !== Boolean(c.command);
}

Prevention

When it happens

Trigger: Passing { name: 'foo' } (or { name: 'foo', transport: 'sse' }) with no url and no command to new McpClient([...]).

Common situations: 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.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/8aafcaca74ec654f. Report an issue: GitHub.

Appendix: source

Thrown at packages/@n8n/agents/src/sdk/mcp-client.ts:66

	private listToolsPromise: Promise<BuiltTool[]> | undefined;

	private closePromise: Promise<void> | undefined;

	/**
	 * Per-server connection failures recorded during the last `listTools()`.
	 * Tools from these servers were skipped; the run continues with the
	 * remaining servers' tools. Read via `getConnectionFailures()`.
	 */
	private connectionFailures: McpConnectionFailedEvent[] = [];

	/**
	 * @param configs - Server configurations. Each must have either `url` or `command`.
	 *   Duplicate names within the list are rejected.
	 */
	constructor(configs: McpServerConfig[]) {
		for (const cfg of configs) {
			if (!cfg.url && !cfg.command) {
				throw new Error(
					`MCP server "${cfg.name}": exactly one of "url" or "command" must be provided`,
				);
			}
			if (cfg.url && cfg.command) {
				throw new Error(`MCP server "${cfg.name}": provide either "url" or "command", not both`);
			}
		}

		const seen = new Set<string>();
		for (const cfg of configs) {
			if (seen.has(cfg.name)) {
				throw new Error(`MCP server name "${cfg.name}" is already registered`);
			}
			seen.add(cfg.name);
		}

		this.configs = configs;
		this.connections = configs.map((cfg) => new McpConnection(cfg));

View on GitHub (pinned to 5ac6606e81)