Skip to content

Add passthroughHeaders to VirtualMCPServer for header forwarding#5466

Open
juancarlosm wants to merge 16 commits into
stacklok:mainfrom
ackstorm:feat/vmcp-passthrough-headers
Open

Add passthroughHeaders to VirtualMCPServer for header forwarding#5466
juancarlosm wants to merge 16 commits into
stacklok:mainfrom
ackstorm:feat/vmcp-passthrough-headers

Conversation

@juancarlosm

@juancarlosm juancarlosm commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

When a client calls vMCP with a header its backend needs — e.g. x-mcp-api-key that the backend resolves to a specific user — vMCP silently drops it. vMCP opens a fresh request to each backend and sends only its own headers, so the caller's header never arrives. Backends that authenticate per-user from a header therefore can't be used through vMCP today — only by hitting the backend directly.

This adds spec.passthroughHeaders: an allowlist of incoming header names that vMCP forwards, unchanged, to every backend it calls.

spec:
  passthroughHeaders:
    - x-mcp-api-key

A listed header is read from the client request and attached to that session's backend requests. Headers not listed are ignored. Restricted names (Host, Authorization, X-Forwarded-*, hop-by-hop) are rejected.

Approach discussed and agreed with @jerm-dro on #3958 (assigned to me there).

Closes #3958

Type of change

  • New feature

Test plan

  • Unit tests across all layers (identity redaction, header merge, validation, capture middleware, converter precedence)
  • Integration test (test/integration/vmcp/passthrough_headers_test.go): real client → capture → backend; asserts allowlisted header forwarded, non-allowlisted dropped
  • golangci-lint clean; CRD/deepcopy/docs regenerated (no drift)

Does this introduce a user-facing change?

Yes:

spec:
  passthroughHeaders: [x-litellm-api-key]

Special notes for reviewers

  • Implementation: the header is captured onto the per-request *auth.Identity at the incoming-auth boundary (cloned, not mutated) and injected by the per-session backend client — it flows as an explicit parameter, not a context value, so no vmcp-anti-patterns §1/§7 coupling and the round-tripper is unchanged. This is the no-context-plumbing path discussed in vMCP: add header_passthrough outgoing auth strategy to forward dynamic incoming headers to backends #3958 once the session refactor landed; the vmcp-review audit was clean.
  • Limitation: on session restore (HA), only the (iss, sub) tuple is persisted — forwarded headers aren't, same as bearer tokens. Low impact with sessionAffinity: ClientIP.

Large PR Justification

Most of the diff is required tests (~700 lines: unit + an in-process e2e) and generated code that can't be split (CRD manifests in two API versions × two chart copies, deepcopy, API docs). Hand-written source is ~240 lines. Already trimmed from ~1157 to ~1030 added lines by de-duplicating tests (coverage unchanged); the remainder is cohesive single-feature code that wouldn't be safe to split.

@github-actions github-actions Bot added the size/XL Extra large PR: 1000+ lines changed label Jun 8, 2026

@github-actions github-actions 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.

Large PR Detected

This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.

How to unblock this PR:

Add a section to your PR description with the following format:

## Large PR Justification

[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformation

Alternative:

Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.

See our Contributing Guidelines for more details.


This review will be automatically dismissed once you add the justification section.

@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.27273% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.74%. Comparing base (b672d82) to head (7ad9e91).

Files with missing lines Patch % Lines
pkg/vmcp/session/internal/backend/mcp_session.go 93.75% 1 Missing and 1 partial ⚠️
pkg/vmcp/cli/serve.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5466      +/-   ##
==========================================
+ Coverage   69.72%   69.74%   +0.01%     
==========================================
  Files         645      646       +1     
  Lines       65598    65697      +99     
==========================================
+ Hits        45737    45818      +81     
- Misses      16517    16531      +14     
- Partials     3344     3348       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@juancarlosm juancarlosm force-pushed the feat/vmcp-passthrough-headers branch from 0d98f3a to 508a72a Compare June 8, 2026 20:29
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jun 8, 2026
@github-actions github-actions Bot dismissed their stale review June 8, 2026 20:29

Large PR justification has been provided. Thank you!

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

✅ Large PR justification has been provided. The size review has been dismissed and this PR can now proceed with normal review.

@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jun 8, 2026
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jun 9, 2026
Comment thread pkg/auth/identity.go Outdated
Move the forwarded-header carrier off auth.Identity (and out of the auth
factory) onto a typed context value in pkg/vmcp/headerforward, wired in the
vMCP server chain via Config.PassthroughHeaders. Addresses @jerm-dro's review
on stacklok#5466. Behavior unchanged; validated end-to-end on a live cluster.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size/L Large PR: 600-999 lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jun 10, 2026
@juancarlosm juancarlosm requested a review from jerm-dro June 10, 2026 08:04
@github-actions github-actions Bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Jun 10, 2026

@jerm-dro jerm-dro 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.

New direction LGTM — just a few minor notes inline.

Comment thread pkg/vmcp/config/config.go Outdated
Comment thread pkg/vmcp/session/internal/backend/mcp_session.go
Comment thread pkg/vmcp/config/validator.go
Comment thread test/integration/vmcp/passthrough_headers_test.go
Comment thread pkg/vmcp/auth/factory/incoming.go Outdated
juancarlosm and others added 4 commits June 14, 2026 07:44
A forwarded passthrough header whose name also appears in a backend's
static header-forward config (plaintext or secret) is an admin
misconfiguration. Return an error at session creation instead of
silently picking a winner, so the conflict surfaces and can be fixed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reword the PassthroughHeaders doc comment to match the post-refactor
capture/consume flow, and restore the unrelated auth-middleware comment
in incoming.go to its pre-PR form to keep this PR scoped to the feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Forwarded headers are captured once at backend-session creation. Add a
second tool call with a changed caller header and assert the backend
still observes the original value, locking that contract against a
regression that re-reads headers per request or drops them after the
first call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gh-headers

# Conflicts:
#	pkg/vmcp/cli/serve.go
#	pkg/vmcp/server/server.go
@juancarlosm juancarlosm force-pushed the feat/vmcp-passthrough-headers branch from 3aea11e to 997ecfc Compare June 14, 2026 06:08
@github-actions github-actions Bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Jun 14, 2026
The main merge brought both the forwarded-header capture branch and
upstream's rate-limit branch into Handler, pushing gocyclo to 16 (>15).
Extract the capture block into applyForwardedHeaderCapture, mirroring
applyRateLimiting, restoring complexity to 15. Behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Jun 14, 2026
The PassthroughHeaders doc reword in pkg/vmcp/config feeds the
VirtualMCPServer CRD's spec.config.passthroughHeaders description.
Regenerate the CRD manifests and API docs so the generated output
matches the source comment (fixes the Operator CI codegen checks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Jun 14, 2026
@juancarlosm

Copy link
Copy Markdown
Contributor Author

Thanks for the review — pushed a round addressing all the notes:

  • Headers on Identity (blocker): moved to a typed unexported context key in pkg/vmcp/headerforward; added an inline note on why this is request-scoped transport plumbing, not the context-coupling anti-pattern.
  • mergeForwardedHeaders precedence: now fails loud — a forwarded header that collides with a backend's static HeaderForward (plaintext or secret) returns an error at session creation instead of silently picking a winner. Propagated through createMCPClient.
  • Authorization: kept allowable (the LiteLLM Zero Trust case needs it) and folded into the fail-loud-on-collision approach we agreed on. The strategy-side check (passthrough vs upstream_inject/token_exchange) is tracked as a follow-up.
  • Session stability: added a second CallTool with a changed caller header, asserting the backend still sees the value captured at session creation.
  • Doc + scope: reworded the PassthroughHeaders doc to match the capture/consume flow; reverted the unrelated incoming.go auth-middleware comment to its pre-PR form.

Also merged latest main (resolved serve.go/server.goauthfactory alias + both PassthroughHeaders and RateLimitMiddleware) and fixed the resulting Handler gocyclo + CRD codegen regen. CI is green.

Follow-up (separate PR): fail loud when a passthrough header collides with a backend's outgoing-auth strategy that manages the same header.

@juancarlosm juancarlosm requested a review from jerm-dro June 14, 2026 06:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large PR: 600-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

vMCP: add header_passthrough outgoing auth strategy to forward dynamic incoming headers to backends

2 participants