n8n-io/n8n · error · Error

Duplicate deferred tool name "${tool.name}"

Error message

Duplicate deferred tool name "${tool.name}"

What it means

`DeferredToolManager` indexes tools by name in a `Map` for O(1) lookup by `load_tool`. If two tools in the constructor array share the same `name`, the second would overwrite the first, silently hiding a tool. The constructor detects this and throws to fail fast rather than silently lose a tool.

Source

Thrown at packages/@n8n/agents/src/runtime/tools/deferred-tool-manager.ts:96

export class DeferredToolManager {
	private readonly toolsByName = new Map<string, BuiltTool>();

	private readonly loadedToolNames = new Set<string>();

	private readonly topK: number;

	private readonly searchTool: BuiltTool;

	private readonly loadTool: BuiltTool;

	constructor(tools: BuiltTool[], options: DeferredToolManagerOptions = {}) {
		for (const tool of tools) {
			if (tool.name === SEARCH_TOOLS_TOOL_NAME || tool.name === LOAD_TOOL_TOOL_NAME) {
				throw new Error(`Deferred tool name "${tool.name}" is reserved`);
			}
			if (this.toolsByName.has(tool.name)) {
				throw new Error(`Duplicate deferred tool name "${tool.name}"`);
			}
			this.toolsByName.set(tool.name, tool);
		}

		this.topK = options.topK ?? DEFAULT_TOP_K;
		this.searchTool = this.createSearchTool();
		this.loadTool = this.createLoadTool();
	}

	get hasTools(): boolean {
		return this.toolsByName.size > 0;
	}

	get totalToolCount(): number {
		return this.toolsByName.size;
	}

	get loadedToolCount(): number {

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Identify the duplicate name from the error message.
  2. Rename one of the conflicting tools, or deduplicate by keeping only the intended one.
  3. Implement a namespacing/prefixing strategy when merging tools from multiple sources (e.g. prefix with the MCP server name).
  4. Add a pre-construction deduplication step: build a `Map<string, BuiltTool>` and handle collisions before passing to the manager.

Example fix

// before:
const mgr = new DeferredToolManager([
  { name: 'read_file', ... },  // from MCP server A
  { name: 'read_file', ... },  // from MCP server B
]);

// after: namespace by source
const mgr = new DeferredToolManager([
  { name: 'serverA__read_file', ... },
  { name: 'serverB__read_file', ... },
]);
Defensive patterns

Strategy: validation

Validate before calling

function deduplicateTools(tools: BuiltTool[]): BuiltTool[] {
  const seen = new Map<string, BuiltTool>();
  for (const tool of tools) {
    if (seen.has(tool.name)) {
      logger.warn(`Duplicate tool name "${tool.name}", keeping first`);
      continue;
    }
    seen.set(tool.name, tool);
  }
  return [...seen.values()];
}

const mgr = new DeferredToolManager(deduplicateTools(allTools));

Type guard

function hasUniqueNames(tools: BuiltTool[]): boolean {
  const names = tools.map((t) => t.name);
  return new Set(names).size === names.length;
}

Prevention

When it happens

Trigger: Passing `new DeferredToolManager([toolA, toolB])` where `toolA.name === toolB.name`. Common when tools are aggregated from multiple sources (MCP servers, built-in catalog, custom tools) and two sources define a tool with the same name.

Common situations: Two MCP servers expose a tool with the same name (e.g. both have `read_file`). A built-in tool and a custom tool share a name. A tool list was accidentally duplicated during assembly.

Related errors


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