{"record":{"id":"8d78a1a4b60071dc","repo":"n8n-io/n8n","slug":"mcp-tool-name-from-options-source-conflict","errorCode":null,"errorMessage":"MCP tool \"${name}\" from ${options.source} conflicts with \"${claimedBy}\"","messagePattern":"MCP tool \"(.+?)\" from (.+?) conflicts with \"(.+?)\"","errorType":"validation","errorClass":"McpToolNameValidationError","httpStatus":null,"severity":"warning","filePath":"packages/@n8n/instance-ai/src/agent/mcp-tool-name-validation.ts","lineNumber":69,"sourceCode":"\t}\n\treturn claimed;\n}\n\nexport function addSafeMcpTools(\n\ttarget: McpToolRegistry,\n\tsourceTools: McpToolRegistry,\n\toptions: {\n\t\tsource: string;\n\t\tclaimedToolNames: Map<string, string>;\n\t\twarn?: (error: McpToolNameValidationError) => void;\n\t},\n): void {\n\tfor (const [name, tool] of sourceTools) {\n\t\ttry {\n\t\t\tconst normalizedName = validateMcpToolName(name, options.source);\n\t\t\tconst claimedBy = options.claimedToolNames.get(normalizedName);\n\t\t\tif (claimedBy) {\n\t\t\t\tthrow new McpToolNameValidationError(\n\t\t\t\t\t`MCP tool \"${name}\" from ${options.source} conflicts with \"${claimedBy}\"`,\n\t\t\t\t\tname,\n\t\t\t\t\toptions.source,\n\t\t\t\t);\n\t\t\t}\n\t\t\toptions.claimedToolNames.set(normalizedName, name);\n\t\t\ttarget.set(name, tool);\n\t\t} catch (error) {\n\t\t\tif (error instanceof McpToolNameValidationError) {\n\t\t\t\toptions.warn?.(error);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n","sourceCodeStart":51,"sourceCodeEnd":86,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/instance-ai/src/agent/mcp-tool-name-validation.ts#L51-L86","documentation":"Thrown by addSafeMcpTools when a tool's normalized name (NFKC → lowercase → strip non-alphanumerics) already exists in the claimedToolNames map, i.e. another tool from this or an earlier source already claimed that normalized slot. The error reports both the conflicting tool and who claimed the slot. Note: inside addSafeMcpTools this error is caught and routed to the warn callback (the tool is skipped), so it surfaces as a warning unless the caller re-throws.","triggerScenarios":"Two MCP servers expose tools whose names collide after normalization (e.g. 'search.items' and 'searchItems', or 'foo-bar' and 'foo_bar'); the same server is registered twice; a built-in tool name collides with an MCP tool.","commonSituations":"Loading multiple MCP servers with overlapping tool vocabularies; a server using punctuation that normalizes away to match another server's tool; a tool name that lowercases to a built-in.","solutions":["Rename one of the colliding tools on its source server so the normalized forms differ.","If you control the registry load order, load the higher-priority server first so its claim wins and the collision is reported (and skipped) for the lower-priority one.","Inspect the warn callback output to see which tools were skipped due to conflicts."],"exampleFix":"// before — two servers expose tools that normalize to 'searchitems'\nserverA: { name: 'search.items' }\nserverB: { name: 'searchItems' }\n\n// after — rename one so normalized forms differ\nserverA: { name: 'search.items' }   // -> 'searchitems'\nserverB: { name: 'searchItemsV2' } // -> 'searchitemsv2'","handlingStrategy":"validation","validationCode":"import { normalizeMcpToolName, createClaimedToolNames } from './mcp-tool-name-validation';\n\nfunction detectCollisions(sources: { source: string; names: string[] }[]): { source: string; name: string; claimedBy: string }[] {\n  const claimed = createClaimedToolNames([]);\n  const collisions: { source: string; name: string; claimedBy: string }[] = [];\n  for (const s of sources) {\n    for (const name of s.names) {\n      const norm = normalizeMcpToolName(name);\n      const claimedBy = claimed.get(norm);\n      if (claimedBy) collisions.push({ source: s.source, name, claimedBy });\n      else claimed.set(norm, name);\n    }\n  }\n  return collisions;\n}\n\nconst collisions = detectCollisions(allSources);\nif (collisions.length) throw new Error(`collisions: ${JSON.stringify(collisions)}`);","typeGuard":"import { McpToolNameValidationError } from './mcp-tool-name-validation';\n\nfunction isMcpToolConflictError(e: unknown): e is McpToolNameValidationError {\n  return e instanceof McpToolNameValidationError && /conflicts with/.test(e.message);\n}","tryCatchPattern":"import { addSafeMcpTools } from './mcp-tool-name-validation';\n\nconst warnings: string[] = [];\naddSafeMcpTools(target, sourceTools, {\n  source,\n  claimedToolNames,\n  warn: (e) => warnings.push(`${e.source}: ${e.toolName} conflicts; skipped`),\n});\nif (warnings.length) logger.warn(`MCP tool conflicts: \\n${warnings.join('\\n')}`);","preventionTips":["Use addSafeMcpTools with a warn callback so conflicts are logged, not fatal.","Load higher-priority servers first so their claims win.","Namespace tool names per server to avoid cross-server collisions."],"tags":["mcp","validation","naming","conflict","tool-registry"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}