Skip to content

fix(mcp): expose client HTTP headers to logging callbacks and hooks - #36724

Merged
yucheng-berri merged 7 commits into
litellm_internal_stagingfrom
litellm_mcp_client_headers_to_logging_lit_5480
Aug 14, 2026
Merged

fix(mcp): expose client HTTP headers to logging callbacks and hooks#36724
yucheng-berri merged 7 commits into
litellm_internal_stagingfrom
litellm_mcp_client_headers_to_logging_lit_5480

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • /mcp tool calls logged only content-type as headers
  • Custom headers were invisible to callbacks, hooks, guardrails
  • /mcp-rest/tools/call already exposed the full set
  • Responses API MCP calls logged an empty header dict

How it solves it:

  • Rebuild the synthetic request from the connection's headers
  • Share one builder with the sampling path
  • Pass sanitized headers to the pre-call hook payload
  • Strip proxy key headers, including a custom configured name
  • Drop upstream MCP auth headers before anything observability-facing
  • Drop client-sent redaction opt-out headers on the MCP path

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_hook have nothing to correlate on

  1. Their client opens an MCP session against https://litellm-domain/mcp with x-nuid, x-app-id and x-user-id on the HTTP request
  2. The client calls a tool through that session, and the call succeeds
  3. Their callback reads kwargs["litellm_params"]["metadata"]["headers"] and gets only {"content-type": "application/json"}
  4. The same tool called over https://litellm-domain/mcp-rest/tools/call does hand back all three headers, so the two routes disagree
  5. Header-based guardrails and tag routing that work on chat completions silently do nothing on the MCP route

After: the same session exposes the same headers the REST route already did, without any credential riding along

  1. The client opens the same MCP session with the same three headers, and calls the same tool
  2. Their callback now reads x-nuid, x-app-id and x-user-id off metadata.headers, matching what the REST route returns
  3. Their MCP hook and any header-based guardrail see the same values, so correlation and routing behave the same on both routes
  4. Tool calls issued through /v1/responses with MCP tools log the same headers instead of an empty dict
  5. The proxy's own key headers never appear, including a custom litellm_key_header_name such as x-company-key, and credential-bearing headers like authorization, x-api-key and cookie are 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***)
  6. A client sending litellm-disable-message-redaction no longer turns off the redaction the admin configured, on tool calls or on tool listing
  7. The credential the client sends for the upstream MCP server, x-mcp-auth (or the configured mcp_client_side_auth_header_name) and the per-server x-mcp-{server}-{header} family, is not in the logged headers either, so a third-party token cannot land in a logging sink
  8. enforced_params keeps behaving exactly as on base for MCP tool calls, since the custom key header is dropped in the synthetic request builder instead of by handing general_settings to add_litellm_data_to_request

Relevant issues

Linear ticket

Resolves LIT-5480

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • 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

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 CustomLogger printing kwargs["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

sudo service postgresql start
cd litellm/proxy && uv run --no-sync prisma db push --accept-data-loss --skip-generate && cd ../..

# after, this branch
uv run --no-sync litellm --config lit5480/config.yaml --detailed_debug --port 4000 > /tmp/after.log 2>&1 &
# before, base commit in a worktree, same venv
cd /home/ubuntu/litellm-before && PYTHONPATH=$PWD /home/ubuntu/repos/litellm/.venv/bin/litellm \
  --config lit5480/config.yaml --detailed_debug --port 4001 > /tmp/before.log 2>&1 &

# same client call at each, headers x-nuid / x-app-id / x-user-id plus x-mcp-auth
LITELLM_KEY=$BEFORE_KEY python call_tool.py http://localhost:4001/mcp
LITELLM_KEY=$AFTER_KEY  python call_tool.py http://localhost:4000/mcp
grep -a "LIT-5480 CALLBACK" -A 16 /tmp/before.log /tmp/after.log

Before, 6209b89:

[LIT-5480 CALLBACK] model=MCP: deepwiki-read_wiki_structure
[LIT-5480 CALLBACK] metadata.headers =
{ "content-type": "application/json" }

After, efba3b6:

[LIT-5480 CALLBACK] model=MCP: deepwiki-read_wiki_structure
[LIT-5480 CALLBACK] metadata.headers =
{ "content-type": "application/json", "host": "localhost:4000", "accept-encoding": "gzip, deflate, br",
  "user-agent": "python-httpx/0.28.1", "x-nuid": "nuid-1", "x-app-id": "app-1", "x-user-id": "user-1",
  "accept": "application/json, text/event-stream", "mcp-session-id": "98536f41d15a4bc09c3087d8ee04e653",
  "mcp-protocol-version": "2025-11-25", "x-forwarded-for": "127.0.0.1" }

The virtual key, the cookie value and the x-mcp-auth token appear nowhere in the after dict, and the async_post_mcp_tool_call_hook payload matched the callback on both runs

Live verification on the latest head, a6f00b9

Same live proxy, this time configured with a renamed proxy key header and with enforced_params set, which is what the last two review findings were about:

general_settings:
  master_key: sk-1234
  litellm_key_header_name: x-company-key
  enforced_params:
    - metadata.generation_name
litellm_settings:
  turn_off_message_logging: true
  callbacks:
    - mcp_header_logger.proxy_handler_instance
sudo service postgresql start
uv run --no-sync litellm --config lit5480/config_head.yaml --port 4000 > /tmp/head_verify.log 2>&1 &

# client authenticates with the renamed key header, and also sends the upstream MCP
# credential, a cookie and the redaction opt-out
# x-company-key: Bearer sk-1234, x-mcp-auth: Bearer upstream-token,
# cookie: session=super-secret-cookie, litellm-disable-message-redaction: true,
# plus x-nuid / x-app-id / x-user-id
python call_tool_head.py http://localhost:4000/mcp
grep -a "LIT-5480" -A 16 /tmp/head_verify.log

Client output, the tool call still succeeds with enforced_params configured:

tools: ['deepwiki-ask_question', 'deepwiki-read_wiki_contents', 'deepwiki-read_wiki_structure']

tool call isError: False
tool result (first 200 chars): Available pages for BerriAI/litellm:  - 1 Overview   - 1.1 Getting Started ...

Proxy log, callback and hook:

[LIT-5480 HOOK] post_mcp_tool_call metadata.headers = {"content-type": "application/json", "host":
  "localhost:4000", "accept-encoding": "gzip, deflate, br", "user-agent": "python-httpx/0.28.1",
  "x-nuid": "nuid-1", "x-app-id": "app-1", "x-user-id": "user-1", "accept":
  "application/json, text/event-stream", "mcp-session-id": "fb9c092b9979414fb06eae5d970962ea",
  "mcp-protocol-version": "2025-11-25", "x-forwarded-for": "127.0.0.1"}

[LIT-5480 CALLBACK] model=MCP: deepwiki-read_wiki_structure
[LIT-5480 CALLBACK] metadata.headers =
{ "content-type": "application/json", "host": "localhost:4000", "accept-encoding": "gzip, deflate, br",
  "user-agent": "python-httpx/0.28.1", "x-nuid": "nuid-1", "x-app-id": "app-1", "x-user-id": "user-1",
  "accept": "application/json, text/event-stream", "mcp-session-id": "fb9c092b9979414fb06eae5d970962ea",
  "mcp-protocol-version": "2025-11-25", "x-forwarded-for": "127.0.0.1" }

The renamed key header, the cookie, the upstream x-mcp-auth token and the client's litellm-disable-message-redaction are all absent, the context headers are all present, and the call is not rejected by enforced_params

Type

🐛 Bug Fix

Caveats (if any)

  • Sampling request builder is now shared, behavior unchanged
  • Synthetic request server address is the previous 127.0.0.1:4000 fallback
  • Any x-mcp-* header besides servers and access-groups is treated as a credential
  • MCP clients cannot opt out of message redaction at all

Link to Devin session: https://app.devin.ai/sessions/7bc8bfff65ef4a5dbf4e4d6fda90b28a
Requested by: @shivamrawat1

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-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@CLAassistant

CLAassistant commented Aug 13, 2026

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 all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ shivamrawat1
❌ devin-ai-integration[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR propagates sanitized MCP client headers to logging callbacks, hooks, guardrails, sampling calls, and Responses API tool calls

  • Adds shared helpers for synthetic MCP requests and observability-safe headers
  • Removes proxy keys, upstream MCP credentials, and client-controlled redaction overrides
  • Adds regression coverage for standard and custom credential-header configurations

Confidence Score: 5/5

The 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

Important Files Changed

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

Comment thread litellm/proxy/_experimental/mcp_server/utils.py Outdated
@codecov

codecov Bot commented Aug 13, 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 13, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_mcp_client_headers_to_logging_lit_5480 (a6f00b9) with litellm_internal_staging (8841cbc)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (d9530bf) during the generation of this report, so 8841cbc was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

…from logging copies

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Fixed: the configured custom key header is now passed to clean_headers, and upstream x-mcp-* credentials are dropped too, with regression tests. @greptileai

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Verified live on local proxies, base commit vs this branch, same MCP client and same callback

After: callback sees the client headers

after

Before: only content-type

before

No credential leaks, and UI logs now show header-derived tags

noleak
ui

shivamrawat1 and others added 2 commits August 13, 2026 20:27
…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>
@yucheng-berri

Copy link
Copy Markdown
Contributor

@greptileai review latest head

@yucheng-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

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.

✅ 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.

@yucheng-berri

Copy link
Copy Markdown
Contributor

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.

@yucheng-berri

Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Contributor Author

Re the general_settings omission at server.py:1052: fixed in b68a90f, the call now passes it so a renamed key header is stripped from both header copies. Regression test added

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Re litellm-disable-message-redaction: logging_safe_mcp_headers now drops the untrusted control headers unconditionally, since that path has no key or team to authorize an opt-out

@yucheng-berri

Copy link
Copy Markdown
Contributor

@greptileai review latest head

@yucheng-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

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.

✅ 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.

Comment thread litellm/proxy/_experimental/mcp_server/utils.py Outdated
@yucheng-berri

Copy link
Copy Markdown
Contributor

check greptile comments devin

@yucheng-berri

Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Contributor Author

Re the enforced_params regression: reverted in a6f00b9, tool calls no longer pass general_settings, and the custom key header is dropped in the request builder instead

@yucheng-berri

Copy link
Copy Markdown
Contributor

@greptileai review latest head

@yucheng-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

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.

✅ 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.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

proxy-endpoints fails on the same 3 TestTemporaryMCPSessionEndpoints tests on staging pushes too, e.g. runs 31762185466 and 31760840778, so it is not from this branch

@yucheng-berri

Copy link
Copy Markdown
Contributor

update the PR body with live verification on the latest head

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@yucheng-berri done, PR body now has a live run at a6f00b9 with litellm_key_header_name: x-company-key and enforced_params set: header dropped, tool call still succeeds

@yucheng-berri
yucheng-berri merged commit 59eeae3 into litellm_internal_staging Aug 14, 2026
84 of 85 checks passed
@yucheng-berri
yucheng-berri deleted the litellm_mcp_client_headers_to_logging_lit_5480 branch August 14, 2026 03:07
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.

3 participants