Skip to content

fix(mcp): forward dcr_bridge-injected credential instead of discarding it for oauth_delegate - #36359

Open
shqear93 wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
shqear93:fix/mcp-dcr-bridge-passthrough-auth-discarded
Open

fix(mcp): forward dcr_bridge-injected credential instead of discarding it for oauth_delegate#36359
shqear93 wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
shqear93:fix/mcp-dcr-bridge-passthrough-auth-discarded

Conversation

@shqear93

@shqear93 shqear93 commented Aug 9, 2026

Copy link
Copy Markdown

Summary

Fixes #36358.

For a dcr_bridge + oauth_delegate MCP server (the gateway-DCR bridge mode that lets an external OAuth client like claude.ai's custom connector UI authenticate against an MCP server LiteLLM otherwise reaches with a static credential), the OAuth handshake completes successfully end-to-end, but the upstream MCP server never receives a valid Authorization header, so no tools are ever returned.

_admit_dcr_bridge_delegate correctly opens the caller's signed envelope during admission and injects the real, unsealed upstream credential as mcp_auth_header. But _create_mcp_client's v1-override exemption unconditionally kept the v2 resolver's spec for any PassthroughConfig-mapped server (which is what oauth_delegate maps to in outbound_credentials/adapter.py) — discarding that correctly-injected credential and falling through to the v2 resolver's own passthrough-header resolution instead. An external DCR client has no way to populate those headers itself; it only ever presents the one envelope bearer, and that gets stripped separately by _should_strip_caller_authorization since it lacks the LiteLLM-specific x-litellm-api-key disambiguation header. Net result: the upstream server gets no Authorization header at all.

This PR makes dcr_bridge servers always take the v1 override path in _create_mcp_client, regardless of spec.config, since their mcp_auth_header is a cryptographically verified credential recovered during admission — not an arbitrary caller-supplied bearer. The security concern the PassthroughConfig exemption exists for (a caller substituting another user's resolved credential) doesn't apply to this origin.

Root cause

Traced end-to-end against a live reproduction: OAuth flow (registerauthorize → SSO login → callbacktoken) all return 200, the external client shows "Connected," but the upstream MCP server's own session count stays at 0 for the entire session — it never receives a request with a matching Authorization header. Full trace is in #36358.

Test plan

  • Added test_dcr_bridge_oauth_delegate_uses_admission_injected_credential to tests/mcp_tests/test_mcp_auth_priority.py, asserting _create_mcp_client forwards the injected mcp_auth_header for a dcr_bridge=True, auth_type=oauth_delegate server.
  • Confirmed the new test fails against the pre-fix code (AssertionError: assert None == 'Bearer unsealed_upstream_token') and passes with the fix.
  • ruff format / ruff check pass on both changed files (pre-existing unrelated lint findings in the test file's import block were left untouched).
  • Ran the full tests/mcp_tests/test_mcp_auth_priority.py suite — all 4 tests pass.

…g it for oauth_delegate

_admit_dcr_bridge_delegate opens the caller's signed DCR-bridge envelope
during admission and injects the real unsealed upstream credential as
mcp_auth_header. _create_mcp_client's v1-override exemption kept the v2
spec for PassthroughConfig unconditionally (which oauth_delegate maps to),
discarding that injected credential and falling back to the v2 resolver's
raw passthrough headers -- headers an external DCR client (e.g. claude.ai)
has no way to populate, since it only ever presents the one envelope
bearer that admission already strips before egress. The upstream MCP
server then receives no Authorization header at all.

dcr_bridge servers now always take the v1 override path here, since their
mcp_auth_header is a cryptographically verified credential from admission,
not an arbitrary caller-supplied bearer -- the security concern the
PassthroughConfig exemption exists for doesn't apply to this origin.

Fixes BerriAI#36358
@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 Aug 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR changes MCP client authentication priority so an admitted DCR bridge credential is forwarded through the existing client path, and adds a regression test for that behavior.

  • Gives DCR bridge servers precedence when selecting the supplied MCP authorization value.
  • Adds coverage for the DCR bridge and delegated OAuth configuration.

Confidence Score: 4/5

The PR appears safe to merge after the non-blocking source-comment concision issue is addressed.

The credential-priority change matches the documented admission-to-egress flow, and the only accepted concern is the unnecessarily lengthy explanatory comment.

Files Needing Attention: litellm/proxy/_experimental/mcp_server/mcp_server_manager.py

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/mcp_server_manager.py Adjusts credential-resolution priority for DCR bridge servers; the behavior appears coherent, but the accompanying explanation violates the repository’s concise-comment convention.
tests/mcp_tests/test_mcp_auth_priority.py Adds focused regression coverage showing that the supplied bridge credential reaches the constructed MCP client.

Reviews (1): Last reviewed commit: "fix(mcp): forward dcr_bridge-injected cr..." | Re-trigger Greptile

Comment on lines +3196 to +3204
#
# dcr_bridge is a different origin, not a caller override: for a client-forwarded-token
# server admitted through the gateway-DCR bridge, `_admit_dcr_bridge_delegate` has already
# opened the caller's signed envelope and injected the real upstream credential it sealed —
# `mcp_auth_header` here IS that unsealed credential, not an arbitrary bearer the caller
# attached. Keeping `spec` for PassthroughConfig in this case discards that credential and
# falls through to the v2 resolver's own passthrough headers, which the DCR caller has no
# way to populate (it only ever presents the one envelope bearer, which admission already
# strips before egress). So a dcr_bridge server always takes the v1 path here regardless of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Overlong credential-flow comment

This nine-line comment repeats the full admission and resolver flow instead of concisely documenting the credential-origin invariant, making this authentication branch harder to scan and maintain.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing shqear93:fix/mcp-dcr-bridge-passthrough-auth-discarded (2585e89) with litellm_internal_staging (ecba48d)

Open in CodSpeed

@tin-berri

Copy link
Copy Markdown
Contributor

Thanks for the trace. The passthrough arm already forwards the injected credential; the failure is earlier, at access resolution. #37865 denies loudly at authorize

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.

MCP gateway-DCR bridge (oauth_delegate + dcr_bridge): unsealed upstream credential discarded on tool calls, upstream never receives Authorization

3 participants