-
Notifications
You must be signed in to change notification settings - Fork 191
Description
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.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.
Parent epic: #4120 — vMCP: add embedded authorization server
RFC document: docs/proposals/THV-0053-vmcp-embedded-authserver.md
Dependencies: 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 change -
VirtualMCPServerSpechas a new fieldAuthServerConfigRef *ExternalAuthConfigRefwith+optionalmarker andjson:"authServerConfigRef,omitempty"tag -
VirtualMCPServer.Validate()callsr.validateAuthServerConfig(), which returns an error whenAuthServerConfigRefis non-nil andAuthServerConfigRef.Nameis empty -
ConditionTypeAuthServerConfigValid = "AuthServerConfigValid"constant is added alongside the existing condition type constants invirtualmcpserver_types.go -
pkg/vmcp/config.Confighas a new fieldAuthServer *AuthServerConfigwithjson:"authServer,omitempty" yaml:"authServer,omitempty"and a// +optionalcomment -
pkg/vmcp/config.AuthServerConfigstruct wraps*authserver.RunConfigwith+kubebuilder:object:generate=trueannotation -
pkg/vmcp/config.OIDCConfighas a newJwksAllowPrivateIP boolfield withjson:"jwksAllowPrivateIP,omitempty" yaml:"jwksAllowPrivateIP,omitempty"tag -
JwksAllowPrivateIPis 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/DeepCopyforAuthServerConfig -
task 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/) - All existing unit tests pass (
task test) -
task lintpasses (ortask lint-fixproduces 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
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).- Struct definition move (same package): Moving
ExternalAuthConfigReffrommcpserver_types.gotomcpexternalauthconfig_types.gois a no-op for all callers because both files are in packagev1alpha1. No import changes needed anywhere. - Validation helper pattern:
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 headers: Every Go file must start with
// 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:
// 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=true
type AuthServerConfig struct {
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).
// +optional
AuthServer *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).
JwksAllowPrivateIP bool `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.
// +optional
AuthServerConfigRef *ExternalAuthConfigRef `json:"authServerConfigRef,omitempty"`New condition type constant in virtualmcpserver_types.go:
// ConditionTypeAuthServerConfigValid indicates whether the auth server config reference is valid
ConditionTypeAuthServerConfigValid = "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 {
if r.Spec.AuthServerConfigRef != nil && r.Spec.AuthServerConfigRef.Name == "" {
return fmt.Errorf("spec.authServerConfigRef.name is required when authServerConfigRef is set")
}
return nil
}Updated auth.TokenValidatorConfig construction in pkg/vmcp/auth/factory/incoming.go — confirm the exact field mapping with pkg/auth:
oidcConfig := &auth.TokenValidatorConfig{
Issuer: oidcCfg.Issuer,
ClientID: oidcCfg.ClientID,
Audience: oidcCfg.Audience,
ResourceURL: oidcCfg.Resource,
AllowPrivateIP: oidcCfg.ProtectedResourceAllowPrivateIP || oidcCfg.JwksAllowPrivateIP,
InsecureAllowHTTP: oidcCfg.InsecureAllowHTTP,
Scopes: oidcCfg.Scopes,
}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 nilAuthServermust still be valid) -
cmd/thv-operator/api/v1alpha1/— any existingvirtualmcpserver_types_test.gotests 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) -
AuthServerConfigRefwith non-emptyName—Validate()returns nil -
AuthServerConfigRefwith emptyName—Validate()returns error containing"spec.authServerConfigRef.name is required"
Generated Code Checks
-
zz_generated.deepcopy.go(inpkg/vmcp/config/) includesDeepCopyInto/DeepCopyforAuthServerConfig -
cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.goincludes updated deepcopy forVirtualMCPServerSpecreflecting the newAuthServerConfigReffield - CRD YAML in
config/crd/bases/includesauthServerConfigRefin theVirtualMCPServerspec schema
Edge 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
- Any runtime behavior change — the vMCP server does not read
Config.AuthServerin this phase RegisterHandlersmethod onEmbeddedAuthServer(Phase 2)- Conditional AS creation in
cmd/vmcp/app/commands.go(Phase 2) AuthServer *runner.EmbeddedAuthServerfield onpkg/vmcp/server.Config(Phase 2)- Replacing the
/.well-known/catch-all handler (Phase 2) validateAuthServerIntegrationfunction and V-01..V-07 rules (Phase 3)StrategyTypeUpstreamInjectconstant andUpstreamInjectConfigstruct (Phase 3)- Operator reconciler cross-resource validation and
AuthServerConfigValidcondition surfacing (Phase 4) - CRD-to-config converter changes in
converter.go(Phase 4) - E2E tests (Phase 4)
- Documentation updates to
docs/arch/(Phase 4)
References
- RFC-0053 design document:
docs/proposals/THV-0053-vmcp-embedded-authserver.md - Parent epic: vMCP: add embedded authorization server #4120
- RFC-0052 (multi-upstream IDP, required for full E2E): Auth Server: multi-upstream provider support #3924
- Operator CRD development guide:
cmd/thv-operator/CLAUDE.md EmbeddedAuthServerConfig(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.go- Incoming auth factory (pattern for
JwksAllowPrivateIPwiring):pkg/vmcp/auth/factory/incoming.go - Deepcopy generation guide: Run
task gen(repo root) ortask operator-generate(operator path)