{"record":{"id":"56d381b190908882","repo":"BerriAI/litellm","slug":"tool-server-mismatch","errorCode":"tool_server_mismatch","errorMessage":"Tool '{name}' belongs to MCP server '{mcp_server.name}' but request specified server_id for '{requested_server.name}'.","messagePattern":"Tool '(.+?)' belongs to MCP server '(.+?)' but request specified server_id for '(.+?)'\\.","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"litellm/proxy/_experimental/mcp_server/server.py","lineNumber":2712,"sourceCode":"            original_tool_name = name\n        else:\n            # Resolve from tool name (MCP JSON-RPC or prefixed REST tool names).\n            mcp_server = global_mcp_server_manager._get_mcp_server_from_tool_name(name)\n            if mcp_server is None and requested_server is not None:\n                for known_prefix in iter_known_server_prefixes(requested_server):\n                    candidate = global_mcp_server_manager._get_mcp_server_from_tool_name(\n                        add_server_prefix_to_name(name, known_prefix)\n                    )\n                    if candidate is not None:\n                        mcp_server = candidate\n                        break\n            if mcp_server is not None:\n                server_name = mcp_server.name\n                original_tool_name = strip_known_server_prefix(name, mcp_server)\n\n            if requested_server is not None:\n                if mcp_server is not None and mcp_server.server_id != requested_server.server_id:\n                    raise HTTPException(\n                        status_code=403,\n                        detail={\n                            \"error\": \"tool_server_mismatch\",\n                            \"message\": (\n                                f\"Tool '{name}' belongs to MCP server \"\n                                f\"'{mcp_server.name}' but request specified \"\n                                f\"server_id for '{requested_server.name}'.\"\n                            ),\n                        },\n                    )\n                if mcp_server is None:\n                    mcp_server = requested_server\n                    server_name = requested_server.name\n                    original_tool_name = strip_known_server_prefix(name, requested_server)\n\n        # Only enforce server-level permissions when we can resolve a server\n        if server_name:\n            if not MCPRequestHandler.is_tool_allowed(","sourceCodeStart":2694,"sourceCodeEnd":2730,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/_experimental/mcp_server/server.py#L2694-L2730","documentation":"In the MCP session tool-call path, LiteLLM resolves which server owns the requested tool name (accepting server-prefixed spellings). If the request also carries a server_id and that ID belongs to a different server than the resolved owner, the call is rejected with HTTP 403 and error code tool_server_mismatch — a guard against ambiguous or stale routing.","triggerScenarios":"The request passes server_id of server B while the (prefixed) tool name belongs to server A; a stale server_id captured before the server was deleted and re-registered under a new ID; the same tool name exposed by two servers.","commonSituations":"Clients caching server_id across re-registrations; copy-pasted payloads mixing values from different servers; duplicate tool names after merging MCP configs.","solutions":["Drop the server_id from the request and let name resolution route the call.","Or pass the server_id of the server that actually owns the tool — confirm with GET /mcp-rest/tools/list per server.","After deleting and re-adding a server, refresh any cached server_id in client code."],"exampleFix":"# before — server_id points at the wrong server\n{\"server_id\": \"srv-B\", \"name\": \"github__create_issue\"}\n\n# after — either use the owning server's id\n{\"server_id\": \"srv-A\", \"name\": \"github__create_issue\"}\n# or omit server_id entirely\n{\"name\": \"github__create_issue\"}","handlingStrategy":"validation","validationCode":"async def owning_server_id(client: httpx.AsyncClient, tool_name: str) -> str | None:\n    tools = (await client.get(f\"{base}/mcp-rest/tools/list\")).json().get(\"tools\", [])\n    for t in tools:\n        if t.get(\"name\") == tool_name or t.get(\"name\", \"\").endswith(f\"__{tool_name}\"):\n            return t.get(\"server_id\") or t.get(\"spec\", {}).get(\"server_id\")\n    return None\n\nsid = await owning_server_id(client, name)\npayload = {\"name\": name, \"arguments\": args} if sid is None else {\"server_id\": sid, \"name\": name, \"arguments\": args}","typeGuard":null,"tryCatchPattern":"except httpx.HTTPStatusError as e:\n    d = e.response.json().get(\"detail\", {})\n    if e.response.status_code == 403 and isinstance(d, dict) and d.get(\"error\") == \"tool_server_mismatch\":\n        # drop server_id and retry on name alone, or resend with the owning server's id\n        payload.pop(\"server_id\", None)\n        return await client.post(f\"{base}/mcp-rest/tools/call\", json=payload)\n    raise","preventionTips":["Never cache server_id across server re-registrations; re-resolve after any registry change.","When both server_id and a prefixed tool name are sent, verify the prefix matches that server.","Prefer the server-prefixed tool name alone — it carries the routing and avoids stale IDs."],"tags":["mcp","routing","http-403","tool-resolution"],"backgroundTag":"tool-server-id-mismatch","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}