stacklok / stacklok/toolhive

Double auth transformation when MCPServer is behind VirtualMCPServer

Open
#4,863 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

auth bug enhancement kubernetes operator vmcp
Dominant language
Go
Stars
2.2k
Forks
300
Avg merge
1d 15h
Merged PRs (30d)
184

Description

Problem

When a VirtualMCPServer fronts an MCPServer that has externalAuthConfigRef (or authServerRef), auth transformations are applied twice:

  1. The vMCP performs outgoing auth (token exchange, upstream inject, etc.) and sends an already-transformed token
  2. The MCPServer's proxy applies the same transformation again via its own middleware

This affects tokenExchange and embeddedAuthServer type configs. Other types (headerInjection, upstreamInject, unauthenticated) are not affected because the MCPServer proxy either no-ops or defers to vMCP for those.

Root cause

externalAuthConfigRef on MCPServer serves two purposes:

  1. Proxy middleware config — tells the MCPServer's proxy runner how to transform tokens (for standalone access)
  2. Discovery metadata — tells the vMCP what auth strategy the backend needs (discovered mode)

These are conflated into one field. Both the vMCP and the MCPServer independently act on the same config.

Current workaround

Don't set externalAuthConfigRef on the MCPServer. Specify outgoing auth inline on the vMCP's spec.outgoingAuth.backends. The MCPServer's proxy passes tokens through without transformation. Downside: the MCPServer can't work standalone.

Proposed solution: authMode field

Add an authMode field to MCPServerSpec and MCPRemoteProxySpec:

// AuthMode controls whether this server applies its own auth transformation.
// - "self" (default): Server applies auth middleware from externalAuthConfigRef/authServerRef.
//   Use when the server is accessed directly by clients.
// - "external": Auth is handled by an external proxy (e.g., VirtualMCPServer).
//   externalAuthConfigRef is retained as discovery metadata only; no auth
//   middleware is added to the proxy. authServerRef is not started.
// +kubebuilder:validation:Enum=self;external
// +kubebuilder:default=self
// +optional
AuthMode string `json:"authMode,omitempty"`
Behavior
authMode externalAuthConfigRef effect authServerRef effect
self (default) Configures proxy middleware (token exchange, bearer token, etc.) Starts embedded auth server + upstream swap middleware
external Retained for vMCP discovery only, no middleware added No auth server started, no upstream swap middleware

Files to change

CRD types:

  • cmd/thv-operator/api/v1alpha1/mcpserver_types.go — add AuthMode field to MCPServerSpec
  • cmd/thv-operator/api/v1alpha1/mcpremoteproxy_types.go — add AuthMode field to MCPRemoteProxySpec

RunConfig generation (the core fix):

  • cmd/thv-operator/controllers/mcpserver_runconfig.go (~line 242) — gate AddExternalAuthConfigOptions and ValidateAndAddAuthServerRefOptions on authMode != "external"
  • cmd/thv-operator/controllers/mcpremoteproxy_runconfig.go (~line 135) — same gate

Deployment generation (env vars, volumes):

  • cmd/thv-operator/controllers/mcpserver_controller.go (~lines 1098, 1210) — gate GenerateTokenExchangeEnvVars, GenerateBearerTokenEnvVar, GenerateAuthServerConfig on authMode != "external"
  • cmd/thv-operator/controllers/mcpremoteproxy_controller.go — equivalent gates

CRD generation:

  • Run task operator-generate, task operator-manifests, task crdref-gen

Tests:

  • Unit tests for RunConfig generation with authMode=external
  • Integration tests verifying no middleware is added when authMode=external
  • Existing tests continue passing (default authMode=self preserves current behavior)

Documentation:

  • docs/operator/crd-api.md — document the new field
  • docs/operator/virtualmcpserver-kubernetes-guide.md — guidance on when to use external

Backward compatibility

Default is self — existing resources behave identically. No CRD migration needed.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the AuthMode fields in cmd/thv-operator/api/v1alpha1/mcpserver_types.go and mcpremoteproxy_types.go, then trace RunConfig and deployment generation in the listed controllers. Run the specified operator-generation tasks and add RunConfig and integration coverage for authMode=external. Done means external mode keeps discovery metadata but adds no auth middleware, auth server, environment variables, or volumes, while the default preserves existing behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, kubernetes
Domain
backend, devops
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.