litellm - ✅(Solved) Fix [Bug]: OpenAPI MCP `extra_headers` are not forwarded from client requests [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#26794Fetched 2026-04-30 06:19:45
View on GitHub
Comments
0
Participants
1
Timeline
5
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×3labeled ×2

Error Message

Actual: the upstream receives no X-TOKEN header. APIs that require the header return a missing-header / unauthorized error.

Root Cause

  • #18169 - static_headers ignored for OpenAPI-based MCP servers. This covers static config-time headers, while this issue is about request-time extra_headers passthrough.
  • #16508 - OAuth2 headers not passed to OpenAPI-based MCP tools. Same general root cause, but this issue covers arbitrary configured headers such as X-TOKEN.

Fix Action

Workaround

Use static_headers if the upstream token is shared/static:

static_headers:
  X-TOKEN: "..."

But this does not work for caller-specific tokens and is not equivalent to extra_headers.

PR fix notes

PR #26818: Forward extra headers for OpenAPI MCP tools

Description (problem / solution / changelog)

Relevant issues

Fixes #26794

Linear ticket

N/A

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a 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).

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Screenshots / Proof of Fix

Validated locally with:

git diff --check
python3 -m ruff check litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py litellm/proxy/_experimental/mcp_server/server.py tests/test_litellm/proxy/_experimental/mcp_server/test_openapi_to_mcp_generator.py tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
python3 -m py_compile litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py litellm/proxy/_experimental/mcp_server/server.py tests/test_litellm/proxy/_experimental/mcp_server/test_openapi_to_mcp_generator.py tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
uv run --extra proxy pytest -q tests/test_litellm/proxy/_experimental/mcp_server/test_openapi_to_mcp_generator.py tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py -q

Type

🐛 Bug Fix ✅ Test

Changes

OpenAPI-generated MCP tools now forward request headers listed in the MCP server extra_headers config to the upstream OpenAPI request.

This matches the existing managed MCP server behavior and preserves existing x-mcp-auth Authorization override precedence.

Changed files

  • litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py (modified, +18/-4)
  • litellm/proxy/_experimental/mcp_server/server.py (modified, +30/-0)
  • tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py (modified, +121/-0)
  • tests/test_litellm/proxy/_experimental/mcp_server/test_openapi_to_mcp_generator.py (modified, +65/-0)

PR #26832: fix(mcp): forward extra_headers for OpenAPI-backed MCP servers

Description (problem / solution / changelog)

What

Fixes #26794

When an MCP server is backed by an OpenAPI spec (spec_path), the extra_headers configuration is silently ignored. Only managed (SSE/HTTP transport) MCP servers correctly forward extra_headers from caller requests to the upstream API.

Why

extra_headers is a list of header names (e.g. ["X-TOKEN"]) that should be forwarded from the client request to the upstream API. For managed MCP servers, this works correctly via _prepare_mcp_server_headers. But for OpenAPI-backed servers, the dispatch path bypasses header forwarding entirely — there's no mechanism to carry per-request headers from the caller to the generated tool function.

How

Added a new ContextVar[Optional[Dict[str, str]]] called _request_extra_headers in openapi_to_mcp_generator.py (same pattern as the existing _request_auth_header):

  1. openapi_to_mcp_generator.py — added _request_extra_headers ContextVar; the generated tool function merges it into effective_headers before making the upstream request
  2. server.py — in the local/OpenAPI tool dispatch branch, builds the extra headers dict from raw_headers using the server's configured extra_headers list, and sets the ContextVar before calling the tool handler
  3. mcp_server_manager.py — in _call_openapi_tool_handler, sets the ContextVar with forwarded headers and removes the warning about headers being unsupported for OpenAPI servers

Uses the same header normalization pattern as _call_regular_mcp_tool and _prepare_mcp_server_headers for consistency.

Changes

  • openapi_to_mcp_generator.py — new ContextVar + merge in tool function
  • server.py — build and set extra headers in local tool dispatch
  • mcp_server_manager.py — forward headers in OpenAPI tool handler, remove unsupported warning

Made with Cursor

Changed files

  • litellm/proxy/_experimental/mcp_server/mcp_server_manager.py (modified, +44/-12)
  • litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py (modified, +12/-0)
  • litellm/proxy/_experimental/mcp_server/server.py (modified, +18/-0)

Code Example

mcp_servers:
  data_api:
    url: http://upstream-api.local
    spec_path: http://upstream-api.local/openapi.json
    auth_type: none
    extra_headers:
      - X-TOKEN

---

curl -s -X POST http://localhost:4000/data_api/mcp \
  -H "x-litellm-api-key: Bearer $LITELLM_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "X-TOKEN: $UPSTREAM_TOKEN" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "id": 1,
    "params": {
      "name": "<some-openapi-generated-tool>",
      "arguments": {}
    }
  }'

---

if server.extra_headers and raw_headers:
    if extra_headers is None:
        extra_headers = {}

    normalized_raw_headers = {
        str(k).lower(): v for k, v in raw_headers.items() if isinstance(k, str)
    }

    for header in server.extra_headers:
        header_value = normalized_raw_headers.get(header.lower())
        if header_value is not None:
            extra_headers[header] = header_value

---

# Note: `extra_headers` on MCPServer is a List[str] of header names to forward
# from the client request (not available in this OpenAPI tool generation step).
# `static_headers` is a dict of concrete headers to always send.
headers = (
    merge_mcp_headers(
        extra_headers=headers,
        static_headers=server.static_headers,
    )
    or {}
)

---

effective_headers = dict(headers)
override_auth = _request_auth_header.get()
if override_auth:
    effective_headers["Authorization"] = override_auth

---

auth_header_value = None
if mcp_auth_header:
    if server_auth_type == MCPAuth.api_key:
        auth_header_value = f"ApiKey {mcp_auth_header}"
    elif server_auth_type == MCPAuth.basic:
        auth_header_value = f"Basic {mcp_auth_header}"
    else:
        auth_header_value = f"Bearer {mcp_auth_header}"
_request_auth_header.set(auth_header_value)

---

extra_headers:
  - X-TOKEN

---

effective_headers = dict(headers)

request_extra_headers = _request_extra_headers.get()
if request_extra_headers:
    effective_headers.update(request_extra_headers)

override_auth = _request_auth_header.get()
if override_auth:
    effective_headers["Authorization"] = override_auth

---

static_headers:
  X-TOKEN: "..."

---

Upstream API response depends on the API, but generally looks like:
{"detail":[{"loc":["headers","x-token"],"msg":"X-TOKEN header missing"}]}

---
RAW_BUFFERClick to expand / collapse

Check for existing issues

  • I have searched the existing issues and checked that my issue is not a duplicate.

What happened?

What happened?

For MCP servers generated from an OpenAPI spec (spec_path / OpenAPI MCP), extra_headers does not forward request headers to the upstream API.

This makes it impossible to register an OpenAPI-backed MCP where the upstream API requires a caller-provided custom header such as X-TOKEN.

Example config:

mcp_servers:
  data_api:
    url: http://upstream-api.local
    spec_path: http://upstream-api.local/openapi.json
    auth_type: none
    extra_headers:
      - X-TOKEN

Then call the MCP endpoint with the required header:

curl -s -X POST http://localhost:4000/data_api/mcp \
  -H "x-litellm-api-key: Bearer $LITELLM_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "X-TOKEN: $UPSTREAM_TOKEN" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "id": 1,
    "params": {
      "name": "<some-openapi-generated-tool>",
      "arguments": {}
    }
  }'

Expected: LiteLLM should forward X-TOKEN to the upstream OpenAPI endpoint.

Actual: the upstream receives no X-TOKEN header. APIs that require the header return a missing-header / unauthorized error.

Why this happens

The managed MCP path appears to support extra_headers passthrough. In server.py, _prepare_mcp_server_headers() copies configured server.extra_headers from raw_headers:

if server.extra_headers and raw_headers:
    if extra_headers is None:
        extra_headers = {}

    normalized_raw_headers = {
        str(k).lower(): v for k, v in raw_headers.items() if isinstance(k, str)
    }

    for header in server.extra_headers:
        header_value = normalized_raw_headers.get(header.lower())
        if header_value is not None:
            extra_headers[header] = header_value

But OpenAPI MCP tools do not use this managed MCP-client path. They are registered as local tool handlers during startup in mcp_server_manager.py.

During OpenAPI tool registration, LiteLLM explicitly notes that request-time extra_headers are not available:

# Note: `extra_headers` on MCPServer is a List[str] of header names to forward
# from the client request (not available in this OpenAPI tool generation step).
# `static_headers` is a dict of concrete headers to always send.
headers = (
    merge_mcp_headers(
        extra_headers=headers,
        static_headers=server.static_headers,
    )
    or {}
)

The generated OpenAPI tool then only has access to the closed-over headers dict plus a hard-coded Authorization ContextVar override:

effective_headers = dict(headers)
override_auth = _request_auth_header.get()
if override_auth:
    effective_headers["Authorization"] = override_auth

And the local/OpenAPI tool dispatch path in server.py only populates that ContextVar from mcp_auth_header (x-mcp-auth), formatting it as an auth header:

auth_header_value = None
if mcp_auth_header:
    if server_auth_type == MCPAuth.api_key:
        auth_header_value = f"ApiKey {mcp_auth_header}"
    elif server_auth_type == MCPAuth.basic:
        auth_header_value = f"Basic {mcp_auth_header}"
    else:
        auth_header_value = f"Bearer {mcp_auth_header}"
_request_auth_header.set(auth_header_value)

So there is currently no code path that carries arbitrary request headers like X-TOKEN into OpenAPI-generated tool execution.

Expected behavior

For OpenAPI-backed MCP servers, extra_headers should behave the same way it does for managed MCP servers: any configured header names should be copied from the incoming LiteLLM request and forwarded to the upstream OpenAPI endpoint.

For example:

extra_headers:
  - X-TOKEN

should cause incoming X-TOKEN: ... to be forwarded to the upstream request made by the generated OpenAPI tool.

Actual behavior

X-TOKEN is not forwarded.

Only these alternatives currently work:

  • static_headers, if the token is fixed for all callers
  • authentication_token, if the upstream accepts one of LiteLLM's supported auth formats
  • x-mcp-auth, but only as a hard-coded Authorization override, not arbitrary headers

None of those solve per-request passthrough for APIs that require X-TOKEN, X-API-Key, or other custom caller-provided headers.

Related issues

This is related to, but not a duplicate of:

  • #18169 - static_headers ignored for OpenAPI-based MCP servers. This covers static config-time headers, while this issue is about request-time extra_headers passthrough.
  • #16508 - OAuth2 headers not passed to OpenAPI-based MCP tools. Same general root cause, but this issue covers arbitrary configured headers such as X-TOKEN.

Suggested fix

OpenAPI-generated MCP tools need a request-time header context similar to the existing _request_auth_header, but generalized for arbitrary configured headers.

One possible approach:

  1. In the OpenAPI/local tool dispatch branch in server.py, build a dict from raw_headers using mcp_server.extra_headers.
  2. Store that dict in a ContextVar before calling _handle_local_mcp_tool.
  3. In openapi_to_mcp_generator.py, merge that ContextVar dict into effective_headers before making the upstream HTTP request.

Pseudo-shape:

effective_headers = dict(headers)

request_extra_headers = _request_extra_headers.get()
if request_extra_headers:
    effective_headers.update(request_extra_headers)

override_auth = _request_auth_header.get()
if override_auth:
    effective_headers["Authorization"] = override_auth

This would preserve existing behavior while making extra_headers work consistently across managed MCP servers and OpenAPI-backed MCP servers.

Workaround

Use static_headers if the upstream token is shared/static:

static_headers:
  X-TOKEN: "..."

But this does not work for caller-specific tokens and is not equivalent to extra_headers.

What part of LiteLLM is this about?

Proxy / MCP / OpenAPI MCP

What LiteLLM version are you on?

v1.83.10-stable

Relevant log output

Upstream API response depends on the API, but generally looks like:
{"detail":[{"loc":["headers","x-token"],"msg":"X-TOKEN header missing"}]}

Steps to Reproduce

  1. Create an OpenAPI-backed MCP server with spec_path and extra_headers: ["X-TOKEN"].
  2. Use an upstream OpenAPI endpoint that requires the X-TOKEN request header.
  3. Call the generated MCP tool with X-TOKEN set on the request to LiteLLM.
  4. Observe that the upstream API reports the header as missing.

Relevant log output

What part of LiteLLM is this about?

Proxy

What LiteLLM version are you on ?

v1.83.3-stable

Twitter / LinkedIn details

No response

extent analysis

TL;DR

To fix the issue of extra_headers not being forwarded to the upstream API for OpenAPI-backed MCP servers, introduce a request-time header context similar to the existing _request_auth_header but generalized for arbitrary configured headers.

Guidance

  • Identify the code paths responsible for handling extra_headers in both managed MCP servers and OpenAPI-backed MCP servers to understand the discrepancy.
  • Implement a ContextVar to store the dict of extra_headers from raw_headers using mcp_server.extra_headers in the OpenAPI/local tool dispatch branch.
  • Merge this ContextVar dict into effective_headers before making the upstream HTTP request in openapi_to_mcp_generator.py.
  • Test the fix by creating an OpenAPI-backed MCP server with spec_path and extra_headers, and verify that the upstream API receives the expected headers.

Example

# In server.py, before calling _handle_local_mcp_tool
request_extra_headers = {}
for header in server.extra_headers:
    header_value = normalized_raw_headers.get(header.lower())
    if header_value is not None:
        request_extra_headers[header] = header_value
_request_extra_headers.set(request_extra_headers)

# In openapi_to_mcp_generator.py, before making the upstream HTTP request
effective_headers = dict(headers)
request_extra_headers = _request_extra_headers.get()
if request_extra_headers:
    effective_headers.update(request_extra_headers)

Notes

This fix assumes that the extra_headers configuration is correctly populated and that the upstream API expects the headers in the same format as they are received by LiteLLM.

Recommendation

Apply the suggested fix to introduce a request-time header context for arbitrary configured headers, as it provides a consistent behavior across managed MCP servers and OpenAPI-backed MCP servers.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

For OpenAPI-backed MCP servers, extra_headers should behave the same way it does for managed MCP servers: any configured header names should be copied from the incoming LiteLLM request and forwarded to the upstream OpenAPI endpoint.

For example:

extra_headers:
  - X-TOKEN

should cause incoming X-TOKEN: ... to be forwarded to the upstream request made by the generated OpenAPI tool.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

litellm - ✅(Solved) Fix [Bug]: OpenAPI MCP `extra_headers` are not forwarded from client requests [2 pull requests, 1 participants]