Summary
MCPServerEntry.spec.headerForward.addPlaintextHeaders is silently ignored. Headers configured on a remote-URL MCPServerEntry (proxied through a VirtualMCPServer) never reach the upstream MCP server. The field is defined in the CRD and accepted by the operator, but is dropped during the conversion from CR to runtime vmcp.Backend and has no representation anywhere in the vMCP outgoing HTTP client.
Verified in v0.27.1.
Reproduction
MCPServerEntry configured against GitHub's Copilot MCP at api.githubcopilot.com/mcp/, with the X-MCP-Toolsets header configured to select a specific set of toolsets:
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPServerEntry
metadata:
name: github-copilot-projects
spec:
remoteUrl: "https://api.githubcopilot.com/mcp/"
transport: streamable-http
externalAuthConfigRef:
name: github-oauth-upstream-inject
headerForward:
addPlaintextHeaders:
X-MCP-Toolsets: "projects,issues,pull_requests,users,repos"
Direct call to https://api.githubcopilot.com/mcp/ with the same token + same X-MCP-Toolsets header returns 39 tools strictly filtered to the 5 requested toolsets, including projects_get, projects_list, projects_write.
Through the vMCP, the upstream returns 43 tools matching GitHub's default toolset for the Copilot endpoint — which includes copilot, orgs, secret_protection tools (NOT requested) and is missing projects_* (which IS requested). The fingerprint matches "header not sent → GitHub returns defaults."
Root cause
Source trace in v0.27.1:
-
CRD field exists: cmd/thv-operator/api/v1beta1/mcpserverentry_types.go:38-41 defines HeaderForward *HeaderForwardConfig; cmd/thv-operator/api/v1beta1/mcpremoteproxy_types.go:12-16 defines HeaderForwardConfig.AddPlaintextHeaders.
-
Reconciler doesn't read it: pkg/vmcp/workloads/k8s.go:503-586 (mcpServerEntryToBackend) reads RemoteURL, Transport, CABundleRef, ExternalAuthConfigRef — but never reads entry.Spec.HeaderForward.
-
No field to carry it: pkg/vmcp/types.go:290-333 vmcp.Backend has no field for static headers.
-
No middleware to apply it: pkg/vmcp/client/client.go:265-311 shows the vMCP outgoing HTTP client's transport chain is size limiter → trace propagation → identity propagation → auth → HTTP. There is no static-header layer.
-
MCPRemoteProxy works via a separate code path: cmd/thv-operator/controllers/mcpremoteproxy_runconfig.go:270-297 calls runner.WithHeaderForward(proxy.Spec.HeaderForward.AddPlaintextHeaders), which flows through pkg/runner/middleware.go:301-310 into pkg/transport/middleware/header_forward.go:100-137. MCPServerEntry bypasses RunConfig entirely and goes straight to the vMCP Backend struct, so it never picks up this middleware.
Suggested fix
- Add a
HeaderForward map[string]string field (or equivalent) to vmcp.Backend in pkg/vmcp/types.go.
- Populate it from
entry.Spec.HeaderForward.AddPlaintextHeaders in mcpServerEntryToBackend (pkg/vmcp/workloads/k8s.go).
- Add a static-header middleware layer in the vMCP HTTP client transport chain (
pkg/vmcp/client/client.go) that reads the per-backend headers and injects them into outgoing requests.
- Consider doing the same for
addHeadersFromSecret so secret-sourced headers are also forwarded.
Impact
Any vMCP backend that depends on header-based configuration of the upstream (e.g. GitHub's X-MCP-Toolsets, API keys passed via custom headers, correlation IDs) silently does nothing. The field accepting valid input without applying it is the worst form — there is no validation error, no warning in logs, nothing in kubectl describe to indicate the headers aren't being sent.
Summary
MCPServerEntry.spec.headerForward.addPlaintextHeadersis silently ignored. Headers configured on a remote-URLMCPServerEntry(proxied through aVirtualMCPServer) never reach the upstream MCP server. The field is defined in the CRD and accepted by the operator, but is dropped during the conversion from CR to runtimevmcp.Backendand has no representation anywhere in the vMCP outgoing HTTP client.Verified in
v0.27.1.Reproduction
MCPServerEntryconfigured against GitHub's Copilot MCP atapi.githubcopilot.com/mcp/, with theX-MCP-Toolsetsheader configured to select a specific set of toolsets:Direct call to
https://api.githubcopilot.com/mcp/with the same token + sameX-MCP-Toolsetsheader returns 39 tools strictly filtered to the 5 requested toolsets, includingprojects_get,projects_list,projects_write.Through the vMCP, the upstream returns 43 tools matching GitHub's default toolset for the Copilot endpoint — which includes
copilot,orgs,secret_protectiontools (NOT requested) and is missingprojects_*(which IS requested). The fingerprint matches "header not sent → GitHub returns defaults."Root cause
Source trace in
v0.27.1:CRD field exists:
cmd/thv-operator/api/v1beta1/mcpserverentry_types.go:38-41definesHeaderForward *HeaderForwardConfig;cmd/thv-operator/api/v1beta1/mcpremoteproxy_types.go:12-16definesHeaderForwardConfig.AddPlaintextHeaders.Reconciler doesn't read it:
pkg/vmcp/workloads/k8s.go:503-586(mcpServerEntryToBackend) readsRemoteURL,Transport,CABundleRef,ExternalAuthConfigRef— but never readsentry.Spec.HeaderForward.No field to carry it:
pkg/vmcp/types.go:290-333vmcp.Backendhas no field for static headers.No middleware to apply it:
pkg/vmcp/client/client.go:265-311shows the vMCP outgoing HTTP client's transport chain issize limiter → trace propagation → identity propagation → auth → HTTP. There is no static-header layer.MCPRemoteProxyworks via a separate code path:cmd/thv-operator/controllers/mcpremoteproxy_runconfig.go:270-297callsrunner.WithHeaderForward(proxy.Spec.HeaderForward.AddPlaintextHeaders), which flows throughpkg/runner/middleware.go:301-310intopkg/transport/middleware/header_forward.go:100-137.MCPServerEntrybypasses RunConfig entirely and goes straight to the vMCPBackendstruct, so it never picks up this middleware.Suggested fix
HeaderForward map[string]stringfield (or equivalent) tovmcp.Backendinpkg/vmcp/types.go.entry.Spec.HeaderForward.AddPlaintextHeadersinmcpServerEntryToBackend(pkg/vmcp/workloads/k8s.go).pkg/vmcp/client/client.go) that reads the per-backend headers and injects them into outgoing requests.addHeadersFromSecretso secret-sourced headers are also forwarded.Impact
Any vMCP backend that depends on header-based configuration of the upstream (e.g. GitHub's
X-MCP-Toolsets, API keys passed via custom headers, correlation IDs) silently does nothing. The field accepting valid input without applying it is the worst form — there is no validation error, no warning in logs, nothing inkubectl describeto indicate the headers aren't being sent.