[Bug]: MCP sending x-litellm-api-key to a dcr_bridge server returns an empty tool list
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 requestSo 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.
- 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. - Client requests
GET https://gateway.example.com/.well-known/oauth-protected-resource/demo/mcp→200, pointing athttps://gateway.example.com/demo. - Client requests
POST https://gateway.example.com/demo/register→200, receives aclient_id. - 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. - Client requests
POST https://gateway.example.com/demo/tokenwith that code, carrying itsx-litellm-api-keyheader →200, receives a token beginningllm_env_. - Client requests
POST https://gateway.example.com/demo/mcpwithAuthorization: Bearer llm_env_…and the samex-litellm-api-keyheader, asking fortools/list→200, but the body is"tools": []. - 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.
- Identical.
- Identical.
- Identical.
- Identical.
- Identical.
- Client requests
POST https://gateway.example.com/demo/mcpwithAuthorization: Bearer llm_env_…and the samex-litellm-api-keyheader, asking fortools/list→200with all 30 tools listed. - 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):
mcp_servers:
demo:
url: os.environ/MCP_DEMO_URL
transport: http
auth_type: oauth_delegate
dcr_bridge: trueEnv 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=TrueThe 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
$ 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
$ 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
Source: BerriAI/litellm