n8n-io/n8n · error · Error

Duplicate deferred 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.

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.

Appendix: 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)