You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add the foundational structural changes required for the vMCP embedded authorization server (RFC-0053, Phase 1). This task introduces all new config model types, CRD fields, and structural validation without changing any runtime behavior — every new field is optional (omitempty) and the vMCP server does not read them yet. Passing all existing tests unchanged is the acceptance gate.
Context
RFC-0053 adds an optional embedded OAuth/OIDC authorization server to vMCP. When Config.AuthServer is nil (Mode A), behavior is byte-for-byte identical to today. When set (Mode B, introduced in Phase 2), the AS acts as the OIDC issuer for incoming clients. This Phase 1 ticket establishes the structural skeleton that Phases 2, 3, and 4 build on: it moves a struct to its canonical home, adds new config and CRD fields, adds a JwksAllowPrivateIP gap-fill needed for Mode B loopback OIDC discovery, and regenerates the deepcopy and CRD manifests.
ExternalAuthConfigRef struct is defined only in cmd/thv-operator/api/v1alpha1/mcpexternalauthconfig_types.go and is no longer defined in mcpserver_types.go; all existing callers (v1alpha1.ExternalAuthConfigRef) compile without change
VirtualMCPServerSpec has a new field AuthServerConfigRef *ExternalAuthConfigRef with +optional marker and json:"authServerConfigRef,omitempty" tag
VirtualMCPServer.Validate() calls r.validateAuthServerConfig(), which returns an error when AuthServerConfigRef is non-nil and AuthServerConfigRef.Name is empty
ConditionTypeAuthServerConfigValid = "AuthServerConfigValid" constant is added alongside the existing condition type constants in virtualmcpserver_types.go
pkg/vmcp/config.Config has a new field AuthServer *AuthServerConfig with json:"authServer,omitempty" yaml:"authServer,omitempty" and a // +optional comment
pkg/vmcp/config.AuthServerConfig struct wraps *authserver.RunConfig with +kubebuilder:object:generate=true annotation
pkg/vmcp/config.OIDCConfig has a new JwksAllowPrivateIP bool field with json:"jwksAllowPrivateIP,omitempty" yaml:"jwksAllowPrivateIP,omitempty" tag
JwksAllowPrivateIP is wired through pkg/vmcp/auth/factory/incoming.go's newOIDCAuthMiddleware to auth.TokenValidatorConfig.AllowPrivateIP (note: this field currently maps ProtectedResourceAllowPrivateIP; JwksAllowPrivateIP must map to the same underlying AllowPrivateIP field — confirm the correct mapping with the auth package)
pkg/vmcp/config/zz_generated.deepcopy.go is regenerated and includes DeepCopyInto/DeepCopy for AuthServerConfig
task operator-generate runs without error (regenerates cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go)
task operator-manifests runs without error (regenerates CRD YAML in config/crd/bases/)
task crdref-gen runs without error (run from inside cmd/thv-operator/)
All existing unit tests pass (task test)
task lint passes (or task lint-fix produces no unresolvable issues)
All new Go files include the SPDX license header
Technical Approach
Recommended Implementation
Work in four discrete, independently-committable steps: (1) move ExternalAuthConfigRef in the operator CRD types, (2) add CRD field and validation to VirtualMCPServerSpec, (3) add AuthServerConfig and JwksAllowPrivateIP to the config model and wire the new field through the incoming auth factory, (4) regenerate all generated files. None of these steps changes any runtime behavior because all new fields are guarded by nil checks that Phase 2 introduces.
Patterns and Frameworks
omitempty for all optional fields: Match the convention used by every optional field in pkg/vmcp/config/config.go and cmd/thv-operator/api/v1alpha1/ — both json:"fieldName,omitempty" and yaml:"fieldName,omitempty" tags are required on fields that appear in both contexts.
+kubebuilder:object:generate=true annotation: Required on AuthServerConfig so controller-gen generates DeepCopyInto/DeepCopy. Follow the exact pattern used by IncomingAuthConfig (line 170 of pkg/vmcp/config/config.go).
Struct definition move (same package): Moving ExternalAuthConfigRef from mcpserver_types.go to mcpexternalauthconfig_types.go is a no-op for all callers because both files are in package v1alpha1. No import changes needed anywhere.
Validation helper pattern: validateAuthServerConfig() should follow the validateEmbeddingServer() pattern in virtualmcpserver_types.go — a private method on *VirtualMCPServer with a nil-guard at the top, checking the ref name is non-empty when the ref is set.
SPDX headers: Every Go file must start with // SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. and // SPDX-License-Identifier: Apache-2.0. Use task license-fix to add missing headers automatically.
Code Pointers
cmd/thv-operator/api/v1alpha1/mcpserver_types.go (lines 632–638) — ExternalAuthConfigRef struct definition to be moved (not copied) to mcpexternalauthconfig_types.go. Verify no other struct in mcpserver_types.go references ExternalAuthConfigRef inline before removing.
cmd/thv-operator/api/v1alpha1/mcpexternalauthconfig_types.go (around line 200, after EmbeddedAuthServerConfig) — destination for ExternalAuthConfigRef. Place it near the other reference-type structs at the bottom of the file.
cmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go (lines 17–76) — VirtualMCPServerSpec struct. Add AuthServerConfigRef *ExternalAuthConfigRef after EmbeddingServerRef. Add ConditionTypeAuthServerConfigValid constant in the existing const block at lines 211–228.
cmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go (lines 400–433) — validateEmbeddingServer() — use as the pattern for the new validateAuthServerConfig() helper.
pkg/vmcp/config/config.go (lines 89–159) — Config struct. Add AuthServer *AuthServerConfig after Optimizer. Add new AuthServerConfig struct below (near IncomingAuthConfig at line 161).
pkg/vmcp/auth/factory/incoming.go (lines 128–159) — newOIDCAuthMiddleware. Update the auth.TokenValidatorConfig literal to map JwksAllowPrivateIP from the OIDC config. Note that AllowPrivateIP is currently set from ProtectedResourceAllowPrivateIP; determine whether JwksAllowPrivateIP should be a separate field on TokenValidatorConfig or reuse the same field (check pkg/auth — the TokenValidatorConfig struct).
pkg/vmcp/config/zz_generated.deepcopy.go — do not edit manually; regenerate with task gen (from repo root) or the equivalent controller-gen invocation.
pkg/authserver/config.go — authserver.RunConfig struct (lines 33–74). This is the type wrapped by the new AuthServerConfig. Import path: github.com/stacklok/toolhive/pkg/authserver.
Component Interfaces
New struct in pkg/vmcp/config/config.go:
// AuthServerConfig wraps the auth server's RunConfig for vMCP.// When non-nil, vMCP starts an embedded OAuth authorization server (Mode B).// When nil, vMCP uses an external OIDC issuer (Mode A).// +kubebuilder:object:generate=truetypeAuthServerConfigstruct {
RunConfig*authserver.RunConfig`json:"runConfig" yaml:"runConfig"`
}
New field on Config in pkg/vmcp/config/config.go:
// AuthServer configures the embedded OAuth authorization server.// nil = Mode A (no AS). non-nil = Mode B (AS enabled).// +optionalAuthServer*AuthServerConfig`json:"authServer,omitempty" yaml:"authServer,omitempty"`
New field on OIDCConfig in pkg/vmcp/config/config.go:
// JwksAllowPrivateIP allows OIDC discovery and JWKS fetches to private IP addresses.// Required when IncomingAuth.OIDC.Issuer points to an in-cluster service (Mode B loopback).// Default: false (private IPs rejected, consistent with production security posture).JwksAllowPrivateIPbool`json:"jwksAllowPrivateIP,omitempty" yaml:"jwksAllowPrivateIP,omitempty"`
New field on VirtualMCPServerSpec in cmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go:
// AuthServerConfigRef references an MCPExternalAuthConfig of type "embeddedAuthServer".// The referenced resource must exist in the same namespace. nil = Mode A.// +optionalAuthServerConfigRef*ExternalAuthConfigRef`json:"authServerConfigRef,omitempty"`
New condition type constant in virtualmcpserver_types.go:
// ConditionTypeAuthServerConfigValid indicates whether the auth server config reference is validConditionTypeAuthServerConfigValid="AuthServerConfigValid"
New validation method on *VirtualMCPServer in virtualmcpserver_types.go:
// validateAuthServerConfig validates the AuthServerConfigRef field.// Only checks structural validity (name non-empty); cross-resource validation// (type check, issuer consistency) is deferred to the reconciler in Phase 4.func (r*VirtualMCPServer) validateAuthServerConfig() error {
ifr.Spec.AuthServerConfigRef!=nil&&r.Spec.AuthServerConfigRef.Name=="" {
returnfmt.Errorf("spec.authServerConfigRef.name is required when authServerConfigRef is set")
}
returnnil
}
Updated auth.TokenValidatorConfig construction in pkg/vmcp/auth/factory/incoming.go — confirm the exact field mapping with pkg/auth:
Note: If pkg/auth.TokenValidatorConfig has separate fields for resource endpoint and JWKS/discovery private IP allowances, use them separately. If it has a single AllowPrivateIP field (as currently wired), the OR approach above is the correct stopgap until the auth package is extended.
Testing Strategy
Unit Tests
No new test files are required for Phase 1 (all validation unit tests are in Phase 3). However, the following existing tests must continue to pass:
pkg/vmcp/config/validator_test.go — all existing test cases pass unchanged (Mode A config with nil AuthServer must still be valid)
cmd/thv-operator/api/v1alpha1/ — any existing virtualmcpserver_types_test.go tests pass unchanged
Structural Validation Tests (Phase 1 — optional but encouraged)
If the team adds early validation coverage, a minimal test for validateAuthServerConfig can go in cmd/thv-operator/api/v1alpha1/virtualmcpserver_types_test.go:
Nil AuthServerConfigRef — Validate() returns nil (no regression)
AuthServerConfigRef with non-empty Name — Validate() returns nil
AuthServerConfigRef with empty Name — Validate() returns error containing "spec.authServerConfigRef.name is required"
Generated Code Checks
zz_generated.deepcopy.go (in pkg/vmcp/config/) includes DeepCopyInto/DeepCopy for AuthServerConfig
cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go includes updated deepcopy for VirtualMCPServerSpec reflecting the new AuthServerConfigRef field
CRD YAML in config/crd/bases/ includes authServerConfigRef in the VirtualMCPServer spec schema
Edge Cases
Config.AuthServer = nil in all YAML serialization round-trips — the field must be absent (omitempty ensures this; verify with a marshaling test or manual inspection)
VirtualMCPServerSpec.AuthServerConfigRef = nil — no behavior change in reconciler (Phase 4 adds the reconciler logic; Phase 1 must not break the reconciler with a nil pointer)
Out of Scope
Any runtime behavior change — the vMCP server does not read Config.AuthServer in this phase
RegisterHandlers method on EmbeddedAuthServer (Phase 2)
Conditional AS creation in cmd/vmcp/app/commands.go (Phase 2)
AuthServer *runner.EmbeddedAuthServer field on pkg/vmcp/server.Config (Phase 2)
Replacing the /.well-known/ catch-all handler (Phase 2)
validateAuthServerIntegration function and V-01..V-07 rules (Phase 3)
StrategyTypeUpstreamInject constant and UpstreamInjectConfig struct (Phase 3)
Operator reconciler cross-resource validation and AuthServerConfigValid condition surfacing (Phase 4)
CRD-to-config converter changes in converter.go (Phase 4)
Description
Add the foundational structural changes required for the vMCP embedded authorization server (RFC-0053, Phase 1). This task introduces all new config model types, CRD fields, and structural validation without changing any runtime behavior — every new field is optional (
omitempty) and the vMCP server does not read them yet. Passing all existing tests unchanged is the acceptance gate.Context
RFC-0053 adds an optional embedded OAuth/OIDC authorization server to vMCP. When
Config.AuthServeris nil (Mode A), behavior is byte-for-byte identical to today. When set (Mode B, introduced in Phase 2), the AS acts as the OIDC issuer for incoming clients. This Phase 1 ticket establishes the structural skeleton that Phases 2, 3, and 4 build on: it moves a struct to its canonical home, adds new config and CRD fields, adds aJwksAllowPrivateIPgap-fill needed for Mode B loopback OIDC discovery, and regenerates the deepcopy and CRD manifests.Parent epic: #4120 — vMCP: add embedded authorization server
RFC document:
docs/proposals/THV-0053-vmcp-embedded-authserver.mdDependencies: None (root task — can start immediately)
Blocks: Phase 2 (server wiring), Phase 3 (startup validation), Phase 4 (operator reconciler)
Acceptance Criteria
ExternalAuthConfigRefstruct is defined only incmd/thv-operator/api/v1alpha1/mcpexternalauthconfig_types.goand is no longer defined inmcpserver_types.go; all existing callers (v1alpha1.ExternalAuthConfigRef) compile without changeVirtualMCPServerSpechas a new fieldAuthServerConfigRef *ExternalAuthConfigRefwith+optionalmarker andjson:"authServerConfigRef,omitempty"tagVirtualMCPServer.Validate()callsr.validateAuthServerConfig(), which returns an error whenAuthServerConfigRefis non-nil andAuthServerConfigRef.Nameis emptyConditionTypeAuthServerConfigValid = "AuthServerConfigValid"constant is added alongside the existing condition type constants invirtualmcpserver_types.gopkg/vmcp/config.Confighas a new fieldAuthServer *AuthServerConfigwithjson:"authServer,omitempty" yaml:"authServer,omitempty"and a// +optionalcommentpkg/vmcp/config.AuthServerConfigstruct wraps*authserver.RunConfigwith+kubebuilder:object:generate=trueannotationpkg/vmcp/config.OIDCConfighas a newJwksAllowPrivateIP boolfield withjson:"jwksAllowPrivateIP,omitempty" yaml:"jwksAllowPrivateIP,omitempty"tagJwksAllowPrivateIPis wired throughpkg/vmcp/auth/factory/incoming.go'snewOIDCAuthMiddlewaretoauth.TokenValidatorConfig.AllowPrivateIP(note: this field currently mapsProtectedResourceAllowPrivateIP;JwksAllowPrivateIPmust map to the same underlyingAllowPrivateIPfield — confirm the correct mapping with the auth package)pkg/vmcp/config/zz_generated.deepcopy.gois regenerated and includesDeepCopyInto/DeepCopyforAuthServerConfigtask operator-generateruns without error (regeneratescmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go)task operator-manifestsruns without error (regenerates CRD YAML inconfig/crd/bases/)task crdref-genruns without error (run from insidecmd/thv-operator/)task test)task lintpasses (ortask lint-fixproduces no unresolvable issues)Technical Approach
Recommended Implementation
Work in four discrete, independently-committable steps: (1) move
ExternalAuthConfigRefin the operator CRD types, (2) add CRD field and validation toVirtualMCPServerSpec, (3) addAuthServerConfigandJwksAllowPrivateIPto the config model and wire the new field through the incoming auth factory, (4) regenerate all generated files. None of these steps changes any runtime behavior because all new fields are guarded by nil checks that Phase 2 introduces.Patterns and Frameworks
omitemptyfor all optional fields: Match the convention used by every optional field inpkg/vmcp/config/config.goandcmd/thv-operator/api/v1alpha1/— bothjson:"fieldName,omitempty"andyaml:"fieldName,omitempty"tags are required on fields that appear in both contexts.+kubebuilder:object:generate=trueannotation: Required onAuthServerConfigsocontroller-gengeneratesDeepCopyInto/DeepCopy. Follow the exact pattern used byIncomingAuthConfig(line 170 ofpkg/vmcp/config/config.go).ExternalAuthConfigReffrommcpserver_types.gotomcpexternalauthconfig_types.gois a no-op for all callers because both files are in packagev1alpha1. No import changes needed anywhere.validateAuthServerConfig()should follow thevalidateEmbeddingServer()pattern invirtualmcpserver_types.go— a private method on*VirtualMCPServerwith a nil-guard at the top, checking the ref name is non-empty when the ref is set.// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.and// SPDX-License-Identifier: Apache-2.0. Usetask license-fixto add missing headers automatically.Code Pointers
cmd/thv-operator/api/v1alpha1/mcpserver_types.go(lines 632–638) —ExternalAuthConfigRefstruct definition to be moved (not copied) tomcpexternalauthconfig_types.go. Verify no other struct inmcpserver_types.goreferencesExternalAuthConfigRefinline before removing.cmd/thv-operator/api/v1alpha1/mcpexternalauthconfig_types.go(around line 200, afterEmbeddedAuthServerConfig) — destination forExternalAuthConfigRef. Place it near the other reference-type structs at the bottom of the file.cmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go(lines 17–76) —VirtualMCPServerSpecstruct. AddAuthServerConfigRef *ExternalAuthConfigRefafterEmbeddingServerRef. AddConditionTypeAuthServerConfigValidconstant in the existing const block at lines 211–228.cmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go(lines 362–398) —VirtualMCPServer.Validate(). Addr.validateAuthServerConfig()call beforereturn r.validateEmbeddingServer().cmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go(lines 400–433) —validateEmbeddingServer()— use as the pattern for the newvalidateAuthServerConfig()helper.pkg/vmcp/config/config.go(lines 89–159) —Configstruct. AddAuthServer *AuthServerConfigafterOptimizer. Add newAuthServerConfigstruct below (nearIncomingAuthConfigat line 161).pkg/vmcp/config/config.go(lines 186–218) —OIDCConfigstruct. AddJwksAllowPrivateIP boolafterProtectedResourceAllowPrivateIP.pkg/vmcp/auth/factory/incoming.go(lines 128–159) —newOIDCAuthMiddleware. Update theauth.TokenValidatorConfigliteral to mapJwksAllowPrivateIPfrom the OIDC config. Note thatAllowPrivateIPis currently set fromProtectedResourceAllowPrivateIP; determine whetherJwksAllowPrivateIPshould be a separate field onTokenValidatorConfigor reuse the same field (checkpkg/auth— theTokenValidatorConfigstruct).pkg/vmcp/config/zz_generated.deepcopy.go— do not edit manually; regenerate withtask gen(from repo root) or the equivalentcontroller-geninvocation.pkg/authserver/config.go—authserver.RunConfigstruct (lines 33–74). This is the type wrapped by the newAuthServerConfig. Import path:github.com/stacklok/toolhive/pkg/authserver.Component Interfaces
New struct in
pkg/vmcp/config/config.go:New field on
Configinpkg/vmcp/config/config.go:New field on
OIDCConfiginpkg/vmcp/config/config.go:New field on
VirtualMCPServerSpecincmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go:New condition type constant in
virtualmcpserver_types.go:New validation method on
*VirtualMCPServerinvirtualmcpserver_types.go:Updated
auth.TokenValidatorConfigconstruction inpkg/vmcp/auth/factory/incoming.go— confirm the exact field mapping withpkg/auth:Note: If
pkg/auth.TokenValidatorConfighas separate fields for resource endpoint and JWKS/discovery private IP allowances, use them separately. If it has a singleAllowPrivateIPfield (as currently wired), the OR approach above is the correct stopgap until the auth package is extended.Testing Strategy
Unit Tests
No new test files are required for Phase 1 (all validation unit tests are in Phase 3). However, the following existing tests must continue to pass:
pkg/vmcp/config/validator_test.go— all existing test cases pass unchanged (Mode A config with nilAuthServermust still be valid)cmd/thv-operator/api/v1alpha1/— any existingvirtualmcpserver_types_test.gotests pass unchangedStructural Validation Tests (Phase 1 — optional but encouraged)
If the team adds early validation coverage, a minimal test for
validateAuthServerConfigcan go incmd/thv-operator/api/v1alpha1/virtualmcpserver_types_test.go:AuthServerConfigRef—Validate()returns nil (no regression)AuthServerConfigRefwith non-emptyName—Validate()returns nilAuthServerConfigRefwith emptyName—Validate()returns error containing"spec.authServerConfigRef.name is required"Generated Code Checks
zz_generated.deepcopy.go(inpkg/vmcp/config/) includesDeepCopyInto/DeepCopyforAuthServerConfigcmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.goincludes updated deepcopy forVirtualMCPServerSpecreflecting the newAuthServerConfigReffieldconfig/crd/bases/includesauthServerConfigRefin theVirtualMCPServerspec schemaEdge Cases
Config.AuthServer = nilin all YAML serialization round-trips — the field must be absent (omitemptyensures this; verify with a marshaling test or manual inspection)VirtualMCPServerSpec.AuthServerConfigRef = nil— no behavior change in reconciler (Phase 4 adds the reconciler logic; Phase 1 must not break the reconciler with a nil pointer)Out of Scope
Config.AuthServerin this phaseRegisterHandlersmethod onEmbeddedAuthServer(Phase 2)cmd/vmcp/app/commands.go(Phase 2)AuthServer *runner.EmbeddedAuthServerfield onpkg/vmcp/server.Config(Phase 2)/.well-known/catch-all handler (Phase 2)validateAuthServerIntegrationfunction and V-01..V-07 rules (Phase 3)StrategyTypeUpstreamInjectconstant andUpstreamInjectConfigstruct (Phase 3)AuthServerConfigValidcondition surfacing (Phase 4)converter.go(Phase 4)docs/arch/(Phase 4)References
docs/proposals/THV-0053-vmcp-embedded-authserver.mdcmd/thv-operator/CLAUDE.mdEmbeddedAuthServerConfig(CRD type, already implemented):cmd/thv-operator/api/v1alpha1/mcpexternalauthconfig_types.go(lines 152–200)authserver.RunConfig(wrapped byAuthServerConfig):pkg/authserver/config.go(lines 33–74)EmbeddedAuthServer(already implemented, used in Phase 2):pkg/authserver/runner/embeddedauthserver.goJwksAllowPrivateIPwiring):pkg/vmcp/auth/factory/incoming.gotask gen(repo root) ortask operator-generate(operator path)