{"record":{"id":"ab644acd3b1f2b37","repo":"zylon-ai/private-gpt","slug":"tool-name-conflict","errorCode":"TOOL_NAME_CONFLICT","errorMessage":"Tool name conflict: name='{tool.name}', layer_a='{existing}', layer_b='{layer.source}'","messagePattern":"Tool name conflict: name='(.+?)', layer_a='(.+?)', layer_b='(.+?)'","errorType":"exception","errorClass":"ToolNameConflictError","httpStatus":500,"severity":"error","filePath":"private_gpt/components/context/services/context_stack_builder.py","lineNumber":98,"sourceCode":"    def build(self) -> ContextStack:\n        \"\"\"Build an immutable stack after conflict checks.\"\"\"\n        self.validate_tool_name_uniqueness()\n        return ContextStack(layers=list(self.layers))\n\n    def validate_tool_name_uniqueness(self) -> None:\n        \"\"\"Validate global tool uniqueness across tool-definition layers.\"\"\"\n        seen: dict[str, str] = {}\n        for layer in self.layers:\n            if layer.type is not LayerType.TOOL_DEFINITIONS:\n                continue\n            if not isinstance(layer, ToolDefinitionsLayer):\n                continue\n            for tool in layer.tools:\n                if tool.name is None:\n                    continue\n                existing = seen.get(tool.name)\n                if existing is not None:\n                    raise ToolNameConflictError(\n                        \"Tool name conflict: \"\n                        f\"name='{tool.name}', \"\n                        f\"layer_a='{existing}', layer_b='{layer.source}'\"\n                    )\n                seen[tool.name] = layer.source\n","sourceCodeStart":80,"sourceCodeEnd":104,"githubUrl":"https://github.com/zylon-ai/private-gpt/blob/4a030776a31a901ad80b1bf4d7faa2c1a367efbb/private_gpt/components/context/services/context_stack_builder.py#L80-L104","documentation":"Thrown by ContextStackBuilder while validating global tool-name uniqueness. It walks every layer whose type is LayerType.TOOL_DEFINITIONS (an instance of ToolDefinitionsLayer) and records the first layer.source for each non-None tool.name; a second layer defining the same name raises ToolNameConflictError with both layer sources. Tools with name=None are skipped, and only the first owner is reported per name.","triggerScenarios":"Building a context stack that contains two ToolDefinitionsLayers whose tool lists both contain a tool with the same name (e.g. MCP servers exposing identically named tools, or the same tool set attached at two context levels). Triggered during stack construction/validation, not at tool invocation time.","commonSituations":"Connecting two MCP servers that both expose 'search' or 'read_file'; loading the same tool catalog twice (global layer + request-level layer); merging contexts from different providers that use generic tool names; upgrading a dependency that renames a built-in tool to collide with your custom tool.","solutions":["Find the two layers named in the message (layer_a / layer_b) and remove or rename the duplicated tool in one of them","If both copies are the same tool set, drop the redundant ToolDefinitionsLayer instead of passing it twice","Rename one MCP server's tool via its own tool-name prefix/alias configuration","Rebuild the stack incrementally (add one tool layer at a time) to identify which layer introduces the conflict"],"exampleFix":"# before\nstack = builder.build([\n    tool_layer_from_server_a,  # defines 'search'\n    tool_layer_from_server_b,  # also defines 'search' -> conflict\n])\n\n# after\nstack = builder.build([\n    tool_layer_from_server_a,\n    rename_tools(tool_layer_from_server_b, prefix='b_'),\n])","handlingStrategy":"validation","validationCode":"def find_tool_conflicts(layers) -> dict[str, tuple[str, str]]:\n    seen, conflicts = {}, {}\n    for layer in layers:\n        if getattr(layer, 'type', None).__class__ and layer.type is not None and str(layer.type).endswith('TOOL_DEFINITIONS'):\n            for tool in getattr(layer, 'tools', []):\n                if tool.name and tool.name in seen and seen[tool.name] != layer.source:\n                    conflicts[tool.name] = (seen[tool.name], layer.source)\n                elif tool.name:\n                    seen[tool.name] = layer.source\n    return conflicts\n\nconflicts = find_tool_conflicts(builder.layers)\nassert not conflicts, conflicts","typeGuard":null,"tryCatchPattern":"try:\n    stack = builder.build(layers)\nexcept ToolNameConflictError as e:\n    # message names both conflicting layer sources; drop/rename and rebuild\n    raise HTTPException(400, str(e))","preventionTips":["Namespace/prefix tool names per MCP server or per layer so cross-layer collisions cannot happen","Deduplicate tool-definition layers before building the stack (same source loaded once)","Run uniqueness validation in CI over all configured tool sources"],"tags":["tools","mcp","context-stack","validation","configuration"],"backgroundTag":null,"analyzedSha":"4a030776a31a901ad80b1bf4d7faa2c1a367efbb","analyzedAt":"2026-08-15T03:51:26.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}