Skip to content

fix(mcp): preserve per-user OAuth Authorization over signer JWT on tools/call (#31977) - #32129

Open
Tushar-024 wants to merge 1 commit into
BerriAI:litellm_oss_stagingfrom
Tushar-024:fix/mcp-jwt-signer-oauth-header
Open

fix(mcp): preserve per-user OAuth Authorization over signer JWT on tools/call (#31977)#32129
Tushar-024 wants to merge 1 commit into
BerriAI:litellm_oss_stagingfrom
Tushar-024:fix/mcp-jwt-signer-oauth-header

Conversation

@Tushar-024

Copy link
Copy Markdown

What / Why

Closes #31977.

When mcp_jwt_signer runs as a pre_mcp_call guardrail, tools/call to an OAuth-backed MCP server failed: the signer's JWT overwrote the user's OAuth Authorization header, so the upstream server rejected the request. tools/list already handles this correctly (per #28227) — it skips JWT injection when an Authorization credential is already present, because per-user OAuth must take precedence. The tools/call path (_call_regular_mcp_tool) never got the same protection: it only logged a warning, then ran extra_headers.update(hook_extra_headers) and clobbered the token.

Change

  • Extract the hook-header merge in _call_regular_mcp_tool into a pure helper, _merge_hook_extra_headers_with_precedence(...), and give it the same precedence rule as the tools/list path:
    • A per-user OAuth Authorization — the server auth_value (server_auth_header), the per-user mcp_auth_header, or a caller-supplied (non-static) Authorization already in extra_headers — is preserved; the hook's Authorization is dropped (its other headers still apply).
    • The signer JWT still overrides an admin-configured static Authorization (server.static_headers), preserving existing behavior.
  • Authorization matching is case-insensitive.

Testing

pytest tests/test_litellm/proxy/experimental/mcp_server/test_hook_headers_precedence.py   # 7 passed
pytest tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_hook_extra_headers.py    # 29 passed

Added unit tests for the precedence (OAuth via extra_headers / server_auth_header / mcp_auth_header preserved; JWT applied when no credential; JWT still overrides static; non-Authorization hook headers always applied; case-insensitive). The existing test_hook_headers_override_static_headers still passes, confirming static-override behavior is unchanged.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug on the tools/call path where the mcp_jwt_signer guardrail's Authorization header was unconditionally overwriting a per-user OAuth token, causing OAuth-backed MCP servers to reject the request. The fix extracts header-merge logic into a new pure helper, _merge_hook_extra_headers_with_precedence, applying the same precedence rule that tools/list already had.

  • _merge_hook_extra_headers_with_precedence preserves per-user OAuth credentials (from server_auth_header, mcp_auth_header, or a non-static Authorization already in extra_headers) over the JWT signer, while still allowing the JWT to override an admin-configured static Authorization in server.static_headers.
  • Seven new unit tests cover the main precedence scenarios; all tests are pure (no network calls) and the existing 29 test_mcp_hook_extra_headers tests are reported to still pass.

Confidence Score: 4/5

The core fix is correct and well-tested; the two findings are observability and directory-convention issues that do not affect runtime behaviour.

The precedence logic in the new helper correctly mirrors the tools/list path and is backed by targeted unit tests. The main risk is that the old warning log for JWT-overrides-static-Authorization was removed without replacement, reducing admin visibility. The test file is also placed in a directory that does not match the existing _experimental/ convention, which could cause it to be missed by targeted test runs.

tests/test_litellm/proxy/experimental/mcp_server/test_hook_headers_precedence.py should be verified to be discoverable by the CI test suite, as it is in a different directory from the rest of the MCP server tests.

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/mcp_server_manager.py Adds _merge_hook_extra_headers_with_precedence helper and replaces the old warn-and-clobber block in _call_regular_mcp_tool; logic is correct for the common cases, but the old warning that fired when the JWT overwrote a static Authorization is now silently dropped.
tests/test_litellm/proxy/experimental/mcp_server/test_hook_headers_precedence.py New pure-unit test file covering the 7 main precedence scenarios; no real network calls; placed in experimental/ (no underscore) while existing MCP tests live in _experimental/ (with underscore).

Reviews (1): Last reviewed commit: "fix(mcp): preserve per-user OAuth Author..." | Re-trigger Greptile

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Comment on lines +288 to +292
headers_to_apply = {
k: v for k, v in headers_to_apply.items() if not (isinstance(k, str) and k.lower() == "authorization")
}

merged.update(headers_to_apply)

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 Silent JWT-overrides-static path loses its warning log

The old code emitted a warning-level log entry whenever the hook JWT was about to overwrite an existing (static) Authorization in extra_headers. That entry was the only signal an admin had that two credentials were competing. The new helper only emits a debug message on the preserve-OAuth branch; when the JWT does win (i.e. preserve_existing_authorization is False and a static Authorization gets overwritten), no message is logged at all. An admin who configured static_headers.Authorization will have no visibility into why their credential is being silently replaced by the JWT signer.

@@ -0,0 +1,84 @@
"""

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 Test directory doesn't match the existing MCP test convention

The new file lives under tests/test_litellm/proxy/experimental/mcp_server/ (no leading underscore), while the rest of the MCP server test suite—including test_mcp_hook_extra_headers.py cited in the PR description—is under tests/test_litellm/proxy/_experimental/mcp_server/ (with underscore, matching the source path). This may cause the file to be missed by test-discovery configurations that glob for _experimental/ or to diverge from future test reorganisations.

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!

@Tushar-024
Tushar-024 force-pushed the fix/mcp-jwt-signer-oauth-header branch from 63e9db0 to ad48a54 Compare July 4, 2026 17:37
Comment thread litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
@veria-ai

veria-ai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

…ols/call

When mcp_jwt_signer runs as a pre_mcp_call guardrail, tools/call to an
OAuth-backed MCP server overwrote the user's OAuth Authorization header with
the signed JWT, so the upstream server rejected the call. tools/list already
skips JWT injection when an Authorization credential exists (per BerriAI#28227); the
tools/call path only warned, then clobbered it.

Merge hook headers via a new _merge_hook_extra_headers_with_precedence helper
that preserves a per-user OAuth Authorization (server auth_value, per-user
mcp_auth_header, or a caller-supplied non-static Authorization in
extra_headers), while still letting the signer JWT override an admin static
Authorization. Mirrors the tools/list precedence.

Closes BerriAI#31977
@Tushar-024
Tushar-024 force-pushed the fix/mcp-jwt-signer-oauth-header branch from ad48a54 to 64efa88 Compare July 4, 2026 17:50
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.

1 participant