fix(mcp): expose client HTTP headers to logging callbacks and hooks - #36724
Conversation
MCP protocol tool calls built a synthetic Request with only content-type, so metadata.headers reaching logging callbacks and guardrails was empty while /mcp-rest/tools/call exposed the full set. Rebuild the synthetic request from the connection's raw headers (shared with the sampling path), and pass sanitized headers to the pre-call hook, the MCP to LLM guardrail bridge and the Responses API MCP bridge. Credential headers stay masked and proxy key headers stripped. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
Greptile SummaryThis PR propagates sanitized MCP client headers to logging callbacks, hooks, guardrails, sampling calls, and Responses API tool calls
Confidence Score: 5/5The PR appears safe to merge The previous custom-key-header exposure is fixed across sampling and other MCP observability paths, and no blocking failure remains
|
| Filename | Overview |
|---|---|
| litellm/proxy/_experimental/mcp_server/utils.py | Centralizes synthetic-request construction and sanitization, including removal of the configured custom proxy-key header |
| litellm/proxy/_experimental/mcp_server/server.py | Propagates connection headers and client identity through sanitized MCP logging and pre-call paths |
| litellm/proxy/_experimental/mcp_server/sampling_handler.py | Reuses the shared synthetic-request builder, fixing the previously reported sampling-path exposure |
| litellm/responses/mcp/litellm_proxy_mcp_handler.py | Supplies sanitized client headers to Responses API MCP logging metadata |
| litellm/proxy/_experimental/mcp_server/mcp_server_manager.py | Exposes sanitized client headers to MCP pre-call hook payloads |
| litellm/proxy/litellm_pre_call_utils.py | Makes the untrusted request-header control set reusable by MCP sanitization |
| tests/test_litellm/proxy/_experimental/mcp_server/test_utils.py | Covers custom proxy keys, upstream credentials, redaction controls, and synthetic-request sanitization |
Reviews (5): Last reviewed commit: "fix(mcp): drop custom proxy key header i..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…from logging copies Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Fixed: the configured custom key header is now passed to |
…rs_to_logging_lit_5480 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@greptileai review latest head |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit f190c66. Configure here.
|
server.py:1052 omits general_settings, so with litellm_key_header_name set the virtual key now lands in metadata.headers and proxy_server_request.headers. Verified base clean, head leaks. |
|
logging_safe_mcp_headers skips _strip_untrusted_request_header_controls, so a client sending litellm-disable-message-redaction disables admin redaction on MCP list_tools. Live: base logged redacted, head logged the full catalogue. |
…p headers Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Re the |
|
Re |
|
@greptileai review latest head |
…rs_to_logging_lit_5480
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b68a90f. Configure here.
|
check greptile comments devin |
|
b68a90f: passing general_settings also enables _enforced_params_check, so with enforced_params set every MCP tool call now fails. Base passes, head errors. |
Strips general_settings.litellm_key_header_name in build_synthetic_mcp_request so every caller, including sampling, is covered, and reverts passing general_settings into add_litellm_data_to_request on the tool call path since that also switches on enforced_params. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Re the |
|
@greptileai review latest head |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit a6f00b9. Configure here.
|
|
|
update the PR body with live verification on the latest head |
|
@yucheng-berri done, PR body now has a live run at a6f00b9 with |
59eeae3
into
litellm_internal_staging
TLDR
Problem this solves:
/mcptool calls logged onlycontent-typeas headers/mcp-rest/tools/callalready exposed the full setHow it solves it:
User Flow
Before: a platform team routing MCP tool calls through the gateway cannot see the per-request context headers their app sends, so their logging callback and their
async_post_mcp_tool_call_hookhave nothing to correlate onx-nuid,x-app-idandx-user-idon the HTTP requestkwargs["litellm_params"]["metadata"]["headers"]and gets only{"content-type": "application/json"}After: the same session exposes the same headers the REST route already did, without any credential riding along
x-nuid,x-app-idandx-user-idoffmetadata.headers, matching what the REST route returnslitellm_key_header_namesuch asx-company-key, and credential-bearing headers likeauthorization,x-api-keyandcookieare dropped from the logged set (a value that survives the drop, such as an OAuth-style Authorization the gateway forwards, comes through masked as***REDACTED***)litellm-disable-message-redactionno longer turns off the redaction the admin configured, on tool calls or on tool listingx-mcp-auth(or the configuredmcp_client_side_auth_header_name) and the per-serverx-mcp-{server}-{header}family, is not in the logged headers either, so a third-party token cannot land in a logging sinkenforced_paramskeeps behaving exactly as on base for MCP tool calls, since the custom key header is dropped in the synthetic request builder instead of by handinggeneral_settingstoadd_litellm_data_to_requestRelevant issues
Linear ticket
Resolves LIT-5480
Pre-Submission checklist
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
Verified live against two local proxies with a public MCP server (deepwiki) registered and a custom
CustomLoggerprintingkwargs["litellm_params"]["metadata"]["headers"], driving the same MCP streamable-http client call at both. Before run captured at 6209b89 (base), after run at efba3b6 (this branch)Screen recording of the before and after runs is in the Slack thread: https://berriaillm.slack.com/archives/C0BE49SAUE6/p1786586702217899?thread_ts=1786586702.217899&cid=C0BE49SAUE6
Before, 6209b89:
After, efba3b6:
The virtual key, the
cookievalue and thex-mcp-authtoken appear nowhere in the after dict, and theasync_post_mcp_tool_call_hookpayload matched the callback on both runsLive verification on the latest head, a6f00b9
Same live proxy, this time configured with a renamed proxy key header and with
enforced_paramsset, which is what the last two review findings were about:Client output, the tool call still succeeds with
enforced_paramsconfigured:Proxy log, callback and hook:
The renamed key header, the cookie, the upstream
x-mcp-authtoken and the client'slitellm-disable-message-redactionare all absent, the context headers are all present, and the call is not rejected byenforced_paramsType
🐛 Bug Fix
Caveats (if any)
x-mcp-*header besides servers and access-groups is treated as a credentialLink to Devin session: https://app.devin.ai/sessions/7bc8bfff65ef4a5dbf4e4d6fda90b28a
Requested by: @shivamrawat1