Skip to content

fix(mcp): forward caller bearer on REST oauth_delegate tool calls - #42787

Merged
joshua-berri merged 2 commits into
mainfrom
litellm_1790192008_mcp_rest_oauth_delegate_bearer
Sep 23, 2026
Merged

joshua-berri merged 2 commits into
mainfrom
litellm_1790192008_mcp_rest_oauth_delegate_bearer

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • POST /mcp-rest/tools/call on an oauth_delegate server dropped the caller's Authorization
  • Upstream MCP server got the tool call with no bearer at all
  • Protocol routes (/mcp, /{server}/mcp, /mcp/sse) already forwarded it

How it solves it:

  • REST tool call now builds the caller OAuth headers like the protocol routes do, but only when the target server is oauth_delegate or true_passthrough (the client-forwarded-token modes)
  • Gateway-managed oauth2 servers never receive the caller's request header from REST, so an admission credential sent as Authorization cannot reach them
  • Per-user OAuth token still wins over the request header
  • Admission key stripping and per-server credentials are unchanged

User Flow

Before: a user calling an MCP tool over the REST route gets a result, but the upstream server never sees who they are

  1. They send POST https://litellm-domain/mcp-rest/tools/call with x-litellm-api-key: <virtual key>, Authorization: Bearer <their own token> and body {"name": "<alias>-add", "arguments": {"a": 2, "b": 3}, "server_id": "<id>"}
  2. They get 200 with {"content":[{"type":"text","text":"5"}],"isError":false}
  3. The upstream MCP server (configured as oauth_delegate) logs the tool call with no Authorization header, so any per-user authorization or audit on that side silently sees an anonymous call

After: the same request reaches the upstream server carrying the user's own bearer

  1. They send POST https://litellm-domain/mcp-rest/tools/call with x-litellm-api-key: <virtual key>, Authorization: Bearer <their own token> and the same body
  2. They get 200 with {"content":[{"type":"text","text":"5"}],"isError":false}
  3. The upstream MCP server logs the tool call with Authorization: Bearer <their own token>, exactly as it already did for the /mcp route
  4. A user who authenticates with only Authorization: Bearer <virtual key> still cannot leak that LiteLLM key upstream; the upstream sees no Authorization for that call

Relevant 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

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. 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
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to 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 an oauth_delegate server via POST /v1/mcp/server, a virtual key scoped to it is minted via POST /key/generate, and the caller's own token is Bearer user-48eb2c715fdc. The MCP package and integration tests are byte-identical between the Before commit and the merge base e0af9917a1, so the Before run was not repeated

Before (1c289e5)

REST tool call with a distinct caller bearer

  1. 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>"}'
  2. {"content":[{"type":"text","text":"5"}],"isError":false}
  3. Peer saw authorization: [None]

Protocol control, same headers on /mcp

  1. 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}}}'
  2. data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"text":"5","type":"text"}],"isError":false}}
  3. Peer saw authorization: ['Bearer user-4bf57d7aebd5']

After (9fb2233)

REST tool call with a distinct caller bearer

  1. 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"}'
  2. {"content":[{"type":"text","text":"5","annotations":null,"_meta":null}],"structuredContent":{"result":5},"isError":false,"resultType":"complete"}
  3. Peer saw authorization: ['Bearer user-48eb2c715fdc']

Negative control, REST admitted with the virtual key in Authorization only

  1. 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"}'
  2. {"content":[{"type":"text","text":"5","annotations":null,"_meta":null}],"structuredContent":{"result":5},"isError":false,"resultType":"complete"}
  3. Peer saw authorization: [None] (the LiteLLM admission key is not forwarded upstream)

Protocol control, same headers on /mcp

  1. 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}}}'
  2. data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"text":"5","type":"text"}],"isError":false,"structuredContent":{"result":5}}}
  3. Peer saw authorization: ['Bearer user-48eb2c715fdc']

Tests: tests/integration/mcp/test_mcp_oauth_flows.py::test_delegated_auth_forwards_the_callers_bearer_untouched now 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 in rest_endpoints.py reverted, the new test_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 header

Type

🐛 Bug Fix

Caveats (if any)

Low

  • GET /mcp-rest/tools/list on an oauth_delegate server likely has the same gap; out of scope for this ticket
  • The remaining BUG skip in the same test file (token exchange without subject token returns 500) is unrelated and left as is

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/d6f5a9a9cc734e76a195b771f0683376
Open in Devin Desktop: https://app.devin.ai/desktop/session/d6f5a9a9cc734e76a195b771f0683376?variant=devin

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration
devin-ai-integration Bot requested a review from a team September 23, 2026 19:35
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the earlier credential-isolation concern is resolved by the authentication-mode gate and existing downstream admission-header stripping.

Summary

Aligns REST MCP tool-call credential forwarding with the existing protocol routes while limiting forwarding to client-managed authentication modes.

  • Adds an explicit authentication-mode gate for caller OAuth headers.
  • Preserves precedence for stored per-user OAuth credentials.
  • Adds unit coverage for delegated, per-user, and gateway-managed OAuth behavior.
  • Enables the existing REST integration assertion for delegated authentication.

Reviews (2) · Last reviewed commit: "fix(mcp): only forward caller bearer on ..."

Comment thread litellm/proxy/_experimental/mcp_server/rest_endpoints.py Outdated
@codspeed

codspeed Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_1790192008_mcp_rest_oauth_delegate_bearer (9fb2233) with main (5db8543)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (3522570) during the generation of this report, so 5db8543 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@codecov

codecov Bot commented Sep 23, 2026

Copy link
Copy Markdown

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>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

@joshua-berri
joshua-berri merged commit 49d0ece into main Sep 23, 2026
102 of 103 checks passed
@joshua-berri
joshua-berri deleted the litellm_1790192008_mcp_rest_oauth_delegate_bearer branch September 23, 2026 21:27

This branch was successfully deployed

1 active deployment
e2e-changed — 9fb22333 Deployed Sep 23, 2026 by devin-ai-integration[bot] via oauth #639
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants