BerriAI/litellm · error · HTTPException

tool_server_mismatch

tool_server_mismatch

Error message

Tool '{name}' belongs to MCP server '{mcp_server.name}' but request specified server_id for '{requested_server.name}'.

What it means

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.

Source

Thrown at litellm/proxy/_experimental/mcp_server/server.py:2712

            original_tool_name = name
        else:
            # Resolve from tool name (MCP JSON-RPC or prefixed REST tool names).
            mcp_server = global_mcp_server_manager._get_mcp_server_from_tool_name(name)
            if mcp_server is None and requested_server is not None:
                for known_prefix in iter_known_server_prefixes(requested_server):
                    candidate = global_mcp_server_manager._get_mcp_server_from_tool_name(
                        add_server_prefix_to_name(name, known_prefix)
                    )
                    if candidate is not None:
                        mcp_server = candidate
                        break
            if mcp_server is not None:
                server_name = mcp_server.name
                original_tool_name = strip_known_server_prefix(name, mcp_server)

            if requested_server is not None:
                if mcp_server is not None and mcp_server.server_id != requested_server.server_id:
                    raise HTTPException(
                        status_code=403,
                        detail={
                            "error": "tool_server_mismatch",
                            "message": (
                                f"Tool '{name}' belongs to MCP server "
                                f"'{mcp_server.name}' but request specified "
                                f"server_id for '{requested_server.name}'."
                            ),
                        },
                    )
                if mcp_server is None:
                    mcp_server = requested_server
                    server_name = requested_server.name
                    original_tool_name = strip_known_server_prefix(name, requested_server)

        # Only enforce server-level permissions when we can resolve a server
        if server_name:
            if not MCPRequestHandler.is_tool_allowed(

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Drop the server_id from the request and let name resolution route the call.
  2. Or pass the server_id of the server that actually owns the tool — confirm with GET /mcp-rest/tools/list per server.
  3. After deleting and re-adding a server, refresh any cached server_id in client code.

Example fix

# before — server_id points at the wrong server
{"server_id": "srv-B", "name": "github__create_issue"}

# after — either use the owning server's id
{"server_id": "srv-A", "name": "github__create_issue"}
# or omit server_id entirely
{"name": "github__create_issue"}
Defensive patterns

Strategy: validation

Validate before calling

async def owning_server_id(client: httpx.AsyncClient, tool_name: str) -> str | None:
    tools = (await client.get(f"{base}/mcp-rest/tools/list")).json().get("tools", [])
    for t in tools:
        if t.get("name") == tool_name or t.get("name", "").endswith(f"__{tool_name}"):
            return t.get("server_id") or t.get("spec", {}).get("server_id")
    return None

sid = await owning_server_id(client, name)
payload = {"name": name, "arguments": args} if sid is None else {"server_id": sid, "name": name, "arguments": args}

Try / catch

except httpx.HTTPStatusError as e:
    d = e.response.json().get("detail", {})
    if e.response.status_code == 403 and isinstance(d, dict) and d.get("error") == "tool_server_mismatch":
        # drop server_id and retry on name alone, or resend with the owning server's id
        payload.pop("server_id", None)
        return await client.post(f"{base}/mcp-rest/tools/call", json=payload)
    raise

Prevention

When it happens

Trigger: 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.

Common situations: Clients caching server_id across re-registrations; copy-pasted payloads mixing values from different servers; duplicate tool names after merging MCP configs.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/56d381b190908882. Report an issue: GitHub.