Repository navigation
fix(mcp): forward caller bearer on REST oauth_delegate tool calls - #42787
Conversation
Co-Authored-By: bot_apk <apk@cognition.ai>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
|
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…en servers Co-Authored-By: bot_apk <apk@cognition.ai>
TLDR
Problem this solves:
POST /mcp-rest/tools/callon anoauth_delegateserver dropped the caller'sAuthorization/mcp,/{server}/mcp,/mcp/sse) already forwarded itHow it solves it:
oauth_delegateortrue_passthrough(the client-forwarded-token modes)oauth2servers never receive the caller's request header from REST, so an admission credential sent asAuthorizationcannot reach themUser Flow
Before: a user calling an MCP tool over the REST route gets a result, but the upstream server never sees who they are
x-litellm-api-key: <virtual key>,Authorization: Bearer <their own token>and body{"name": "<alias>-add", "arguments": {"a": 2, "b": 3}, "server_id": "<id>"}{"content":[{"type":"text","text":"5"}],"isError":false}oauth_delegate) logs the tool call with noAuthorizationheader, so any per-user authorization or audit on that side silently sees an anonymous callAfter: the same request reaches the upstream server carrying the user's own bearer
x-litellm-api-key: <virtual key>,Authorization: Bearer <their own token>and the same body{"content":[{"type":"text","text":"5"}],"isError":false}Authorization: Bearer <their own token>, exactly as it already did for the/mcprouteAuthorization: Bearer <virtual key>still cannot leak that LiteLLM key upstream; the upstream sees noAuthorizationfor that callRelevant issues
Follow-up to #42711, which added the integration test with the BUG skip this PR removes
Affected release
Linear ticket
Resolves LIT-8472
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Setup: live proxy on :4000 started from
tests/integration/proxy_config.yaml(real Postgres and Redis), plus a test-owned MCP peer that records the headers of every tool call it receives. The peer is registered as anoauth_delegateserver via POST /v1/mcp/server, a virtual key scoped to it is minted via POST /key/generate, and the caller's own token isBearer user-48eb2c715fdc. The MCP package and integration tests are byte-identical between the Before commit and the merge basee0af9917a1, so the Before run was not repeatedBefore (1c289e5)
REST tool call with a distinct caller bearer
curl -s -X POST http://127.0.0.1:4000/mcp-rest/tools/call -H 'x-litellm-api-key: <virtual-key>' -H 'Authorization: Bearer user-4bf57d7aebd5' -H 'Content-Type: application/json' -d '{"name": "<alias>-add", "arguments": {"a": 2, "b": 3}, "server_id": "<server-id>"}'{"content":[{"type":"text","text":"5"}],"isError":false}[None]Protocol control, same headers on /mcp
curl -s -X POST http://127.0.0.1:4000/mcp -H 'x-litellm-api-key: <virtual-key>' -H 'Authorization: Bearer user-4bf57d7aebd5' -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "<alias>-add", "arguments": {"a": 2, "b": 3}}}'data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"text":"5","type":"text"}],"isError":false}}['Bearer user-4bf57d7aebd5']After (9fb2233)
REST tool call with a distinct caller bearer
curl -s -X POST http://127.0.0.1:4000/mcp-rest/tools/call -H 'x-litellm-api-key: <virtual-key>' -H 'Authorization: Bearer user-48eb2c715fdc' -H 'Content-Type: application/json' -d '{"name": "dlc8d38713-add", "arguments": {"a": 2, "b": 3}, "server_id": "2a443118-09bf-42be-b672-c2d318db83d5"}'{"content":[{"type":"text","text":"5","annotations":null,"_meta":null}],"structuredContent":{"result":5},"isError":false,"resultType":"complete"}['Bearer user-48eb2c715fdc']Negative control, REST admitted with the virtual key in Authorization only
curl -s -X POST http://127.0.0.1:4000/mcp-rest/tools/call -H 'Authorization: Bearer <virtual-key>' -H 'Content-Type: application/json' -d '{"name": "dlc8d38713-add", "arguments": {"a": 2, "b": 3}, "server_id": "2a443118-09bf-42be-b672-c2d318db83d5"}'{"content":[{"type":"text","text":"5","annotations":null,"_meta":null}],"structuredContent":{"result":5},"isError":false,"resultType":"complete"}[None](the LiteLLM admission key is not forwarded upstream)Protocol control, same headers on /mcp
curl -s -X POST http://127.0.0.1:4000/mcp -H 'x-litellm-api-key: <virtual-key>' -H 'Authorization: Bearer user-48eb2c715fdc' -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "dlc8d38713-add", "arguments": {"a": 2, "b": 3}}}'data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"text":"5","type":"text"}],"isError":false,"structuredContent":{"result":5}}}['Bearer user-48eb2c715fdc']Tests:
tests/integration/mcp/test_mcp_oauth_flows.py::test_delegated_auth_forwards_the_callers_bearer_untouchednow passes for all five entry points including[rest]with the BUG skip removed (was 4 passed, 1 skipped). Mutation check: with the one-line change inrest_endpoints.pyreverted, the newtest_forwards_callers_bearer_as_oauth2_headers[oauth_delegate-None-expected0]unit case fails and the integration[rest]case fails on the peer seeing no Authorization. With the auth-type gate removed, the[oauth2-None-None]case fails because a gateway-managed server would receive the caller headerType
🐛 Bug Fix
Caveats (if any)
Low
GET /mcp-rest/tools/liston anoauth_delegateserver likely has the same gap; out of scope for this ticketFinal Attestation
Link to Devin session: https://app.devin.ai/sessions/d6f5a9a9cc734e76a195b771f0683376
Open in Devin Desktop: https://app.devin.ai/desktop/session/d6f5a9a9cc734e76a195b771f0683376?variant=devin