{"record":{"id":"0da332151a1a16e6","repo":"PrefectHQ/fastmcp","slug":"ambiguous-app-tool-tool-name-r-len-distinct","errorCode":null,"errorMessage":"Ambiguous app tool {tool_name!r}: {len(distinct)} components share the identity {tool_hash!r}. The same app is composed more than once, so this call cannot be routed to a single tool.","messagePattern":"Ambiguous app tool (.+?): (.+?) components share the identity (.+?)\\. The same app is composed more than once, so this call cannot be routed to a single tool\\.","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/providers/proxy.py","lineNumber":996,"sourceCode":"        for tool in cache.items:\n            meta = tool.meta or {}\n            fastmcp_meta = meta.get(\"fastmcp\")\n            ui_meta = meta.get(\"ui\")\n            visibility = (\n                ui_meta.get(\"visibility\", []) if isinstance(ui_meta, dict) else []\n            )\n            if (\n                isinstance(fastmcp_meta, dict)\n                and fastmcp_meta.get(TOOL_HASH_META_KEY) == tool_hash\n                and \"app\" in visibility\n            ):\n                matches.append(tool)\n\n        if not matches:\n            return None\n        distinct = {tool.name for tool in matches}\n        if len(distinct) > 1:\n            raise ToolError(\n                f\"Ambiguous app tool {tool_name!r}: {len(distinct)} components share \"\n                f\"the identity {tool_hash!r}. The same app is composed more than \"\n                f\"once, so this call cannot be routed to a single tool.\"\n            )\n        return max(matches, key=version_sort_key)\n\n    # -------------------------------------------------------------------------\n    # Resource methods\n    # -------------------------------------------------------------------------\n\n    async def _list_resources(self) -> Sequence[Resource]:\n        \"\"\"List all resources from the remote server.\"\"\"\n        try:\n            client = await self._get_client()\n            async with client:\n                mcp_resources = await client.list_resources()\n                resources = [\n                    ProxyResource.from_mcp_resource(self.client_factory, r)","sourceCodeStart":978,"sourceCodeEnd":1014,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/providers/proxy.py#L978-L1014","documentation":"ProxyProvider.get_tool_by_hash() resolves an app tool by its identity hash. If more than one distinct tool name maps to the same hash, the same app was composed more than once and the proxy cannot deterministically route a call to a single tool, so it raises ToolError. The hash encodes identity, and duplicate composition breaks that 1:1 mapping.","triggerScenarios":"Calling get_tool_by_hash() (e.g. during tool-call routing for composed apps) when multiple tools with different names share the same tool_hash because the same app was mounted/composed multiple times on the server.","commonSituations":"An app (e.g. a Claude-app composed tool set) accidentally registered twice under different names — often from re-running registration code, duplicate mounting in config, or loading the same app from two providers.","solutions":["Remove the duplicate app composition so the app is registered exactly once under one name","If intentional versioning is desired, give the composed tools distinct identities/versions so hashes differ","Enumerate tools sharing the hash (filter your tool list by tool_hash) and delete/consolidate the redundant entries before re-registering"],"exampleFix":"// before: same app composed twice\nserver.add_app(my_app, name='app_a')\nserver.add_app(my_app, name='app_b')\n// after: compose once\nserver.add_app(my_app, name='app_a')","handlingStrategy":"validation","validationCode":"from collections import Counter\nhashes = Counter(t.key for t in server_tools if is_app_tool(t))\ndupes = [h for h, n in hashes.items() if n > 1]\nif dupes:\n    raise RuntimeError(f'Apps composed more than once: {dupes}')","typeGuard":null,"tryCatchPattern":"try:\n    tool = await provider.get_tool_by_hash(name, tool_hash)\nexcept ToolError as e:\n    if 'Ambiguous app tool' in str(e):\n        tool = pick_canonical_tool(name)  # explicit disambiguation policy","preventionTips":["Register each app exactly once; guard registration code against re-execution","Add a startup assertion that tool hashes are unique across the server","When composing variants, give them distinct identities/versions"],"tags":["proxy","tool-routing","duplicate-registration","ambiguity"],"backgroundTag":"ambiguous-tool-identity","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}