#38208·litellm

[Bug]: MCP sending x-litellm-api-key to a dcr_bridge server returns an empty tool list

Author: vlebourlCreated Aug 25, 2026Updated Sep 17, 2026
Labelsbug

Check for existing issues

  • I have searched the existing issues and checked that my issue is not a duplicate.

What happened?

I set up an MCP server behind the gateway using auth_type: oauth_delegate with dcr_bridge: true, because the upstream requires each user to sign in themselves.

The browser sign-in works and the gateway hands my client a token. But the token request only succeeds if my client also sends x-litellm-api-key... Without it the token endpoint replies:

this server issues a gateway-bound credential; complete the interactive sign-in, or send a litellm credential (x-litellm-api-key or Authorization) on the token request

So I configured my client to send that header. My client sends the same headers on every request, and once that header is present, tools/list comes back with an empty tools array and HTTP 200.

Sending the exact same token without that header returns all 30 tools. That is the only difference between the two calls.

User Flow

The application is an internal AI gateway used by about a dozen employees. It serves chat models and MCP tools to Claude Desktop, which talks to it through mcp-remote. Everyone uses one URL and one virtual key.

Before a (hypothetical) fix

The flow fails at step 6: the tool list comes back empty, so Claude Desktop shows the server as unusable.

  1. User adds the server to Claude Desktop as npx -y mcp-remote https://gateway.example.com/demo/mcp --header "x-litellm-api-key:Bearer <their virtual key>", then restarts the app.
  2. Client requests GET https://gateway.example.com/.well-known/oauth-protected-resource/demo/mcp200, pointing at https://gateway.example.com/demo.
  3. Client requests POST https://gateway.example.com/demo/register200, receives a client_id.
  4. A browser window opens on https://gateway.example.com/demo/authorize?.... The user signs in with their own account and is redirected back with an authorization code.
  5. Client requests POST https://gateway.example.com/demo/token with that code, carrying its x-litellm-api-key header → 200, receives a token beginning llm_env_.
  6. Client requests POST https://gateway.example.com/demo/mcp with Authorization: Bearer llm_env_… and the same x-litellm-api-key header, asking for tools/list200, but the body is "tools": [].
  7. Claude Desktop shows the server as failed: "This server offered no tools". The user cannot use any tool.

No authorization consequence: nobody gains access they shouldn't have. The request is denied, not wrongly allowed.

After a (hypothetical) fix

The flow completes: the user sees the tools and can call them.

  1. Identical.
  2. Identical.
  3. Identical.
  4. Identical.
  5. Identical.
  6. Client requests POST https://gateway.example.com/demo/mcp with Authorization: Bearer llm_env_… and the same x-litellm-api-key header, asking for tools/list200 with all 30 tools listed.
  7. Claude Desktop shows the server as connected. The user calls a tool and gets the upstream's answer back.

Still no authorization consequence: each user only ever reaches what their own sign-in allows, since their own token is the one used upstream.

Proof the bug occurs

Version: v1.98.0, image docker.litellm.ai/berriai/litellm-database:v1.98.0. Also present on main at the time of writing. Client: mcp-remote 0.2.1, Streamable HTTP.

config.yaml (only the relevant part):

yaml
mcp_servers:
  demo:
    url: os.environ/MCP_DEMO_URL
    transport: http
    auth_type: oauth_delegate
    dcr_bridge: true

Env vars the proxy ran with (secrets redacted, the rest kept as-is):

MCP_DEMO_URL=https://upstream.example.com/mcp
LITELLM_MASTER_KEY=<redacted>
DATABASE_URL=<redacted>
STORE_MODEL_IN_DB=True

The upstream is an OAuth 2.0 protected resource (RFC 9728). Its authorization server advertises authorization_code and refresh_token only — no client_credentials — so a per-user browser sign-in is the only way in.

I obtained the llm_env_… token by completing the real browser flow through the gateway, then replayed tools/list twice with that same token, changing only the presence of one header. Both calls were made against the live proxy.

A — token only

bash
$ curl -sS -i -X POST https://gateway.example.com/demo/mcp \
    -H 'Authorization: Bearer <ENVELOPE>' \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json, text/event-stream' \
    -H 'mcp-session-id: <SESSION>' \
    -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

HTTP/1.1 200 OK
content-type: text/event-stream
mcp-session-id: 7353d907a8064391a5d38399a4ee9dfc

event: message
data: {"jsonrpc":"2.0","id":2,"result":{"_meta":{"litellm.ai/server_outcomes":{"demo":{"status":"ok","tool_count":30}}},"tools":[ …30 tools… ]}}

B — same token, plus x-litellm-api-key

bash
$ curl -sS -i -X POST https://gateway.example.com/demo/mcp \
    -H 'Authorization: Bearer <ENVELOPE>' \
    -H 'x-litellm-api-key: Bearer <VIRTUAL_KEY>' \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json, text/event-stream' \
    -H 'mcp-session-id: <SESSION>' \
    -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

HTTP/1.1 200 OK
content-type: text/event-stream
mcp-session-id: 6e70ce612e9a49b984f6cdc35dca06b9

event: message
data: {"jsonrpc":"2.0","id":2,"result":{"_meta":{"litellm.ai/server_outcomes":{"demo":{"status":"auth_required","http_status":401}}},"tools":[]}}

Proxy log for call B:

Error listing tools from demo: Client error '401 Unauthorized' for url 'https://upstream.example.com/mcp'

The upstream returns 401 because no credential reaches it on call B, while the identical token works on call A.

This bug does not involve an LLM provider call, so no /v1/chat/completions, /v1/responses or /v1/messages proof applies — the failing route is the MCP endpoint.

What part of LiteLLM is this about?

Other

What LiteLLM version are you on ?

v1.98.0

Twitter / LinkedIn details

No response