Skip to content

Wire MCPAuthzConfig references into workload controllers#4778

Open
JAORMX wants to merge 2 commits intojaosorior/mcpauthzconfig-crdfrom
jaosorior/mcpauthzconfig-wiring
Open

Wire MCPAuthzConfig references into workload controllers#4778
JAORMX wants to merge 2 commits intojaosorior/mcpauthzconfig-crdfrom
jaosorior/mcpauthzconfig-wiring

Conversation

@JAORMX
Copy link
Copy Markdown
Collaborator

@JAORMX JAORMX commented Apr 13, 2026

Summary

  • The MCPAuthzConfig CRD from Add MCPAuthzConfig CRD for backend-agnostic authorization #4777 defines shared authorization configuration, but workload controllers don't yet consume it. This wires the references so that MCPServer, MCPRemoteProxy, and VirtualMCPServer can resolve and apply authorization from referenced MCPAuthzConfig resources.
  • Adds CEL mutual exclusion validation, hash-based change detection, status conditions, controller watches, volume mount generation, and runtime config resolution for all three workload types.

Type of change

  • New feature

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)

Changes

File Change
api/v1alpha1/mcpserver_types.go Add AuthzConfigRef condition constants, AuthzConfigHash status field, CEL mutual exclusion
api/v1alpha1/mcpremoteproxy_types.go Add AuthzConfigHash status field, CEL mutual exclusion
api/v1alpha1/virtualmcpserver_types.go Add AuthzConfigHash status field, CEL mutual exclusion on IncomingAuthConfig
pkg/controllerutil/authz.go Add GetAuthzConfigForWorkload, ValidateAuthzConfigReady, GenerateAuthzVolumeConfigFromRef, EnsureAuthzConfigMapFromRef, BuildFullAuthzConfigJSON (moved from controller), AddAuthzConfigRefOptions
controllers/mcpauthzconfig_controller.go Use shared BuildFullAuthzConfigJSON from controllerutil
controllers/mcpserver_controller.go Add handleAuthzConfig, setAuthzConfigRefCondition, mapAuthzConfigToMCPServer watch, AuthzConfigRef volume mount
controllers/mcpserver_runconfig.go Resolve AuthzConfigRef in runconfig builder
controllers/mcpremoteproxy_controller.go Add handleAuthzConfig, mapAuthzConfigToMCPRemoteProxy watch
controllers/mcpremoteproxy_runconfig.go Resolve AuthzConfigRef in runconfig builder, extract addAuthzOptions helper
controllers/virtualmcpserver_controller.go Add handleAuthzConfig, validateConfigRefs, mapAuthzConfigToVirtualMCPServer watch
pkg/virtualmcpserverstatus/types.go Add SetAuthzConfigHash to StatusManager interface
pkg/virtualmcpserverstatus/collector.go Implement SetAuthzConfigHash
pkg/vmcpconfig/converter.go Add resolveAuthzConfigRef for Cedar policy extraction from MCPAuthzConfig

Special notes for reviewers

  • Follows the existing OIDC config ref pattern exactly (fetch → validate ready → track hash → set condition → watch).
  • BuildFullAuthzConfigJSON was moved from the MCPAuthzConfig controller to controllerutil so both the config controller and workload controllers can use it without circular imports.
  • The vmcpconfig converter currently only extracts Cedar policies from MCPAuthzConfig. Full support for non-Cedar backends in VirtualMCPServer runtime config is tracked as a follow-up (vmcpconfig.AuthzConfig is Cedar-specific).

Generated with Claude Code

JAORMX and others added 2 commits April 13, 2026 13:32
The existing AuthzConfigRef inline variant is Cedar-specific, with
fields that only make sense for the Cedar authorizer. This adds a new
MCPAuthzConfig CRD that accepts any registered authorizer backend
configuration via a type discriminator and an opaque config blob.

The MCPAuthzConfig CRD follows the established shared-config pattern
(MCPOIDCConfig, MCPExternalAuthConfig) with status conditions, hash-based
change detection, referencing workload tracking, and deletion protection.

A new ConfigKey() method on the AuthorizerFactory interface enables the
controller to reconstruct the full runtime config JSON from the CRD spec.

The AuthzConfigRef field is added to MCPServer, MCPRemoteProxy, and
VirtualMCPServer as a new option alongside the existing authzConfig field,
enabling incremental migration with no breaking changes.

Refs: #3157

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MCPAuthzConfig resources created in PR 1 need to be consumed by the
workload controllers (MCPServer, MCPRemoteProxy, VirtualMCPServer) so
that shared authorization configs actually take effect at runtime.

- Add CEL mutual exclusion for authzConfig/authzConfigRef on all specs
- Add AuthzConfigHash to all workload status types for change detection
- Add condition constants (AuthzConfigRefValidated) and reasons
- Move BuildFullAuthzConfigJSON to controllerutil for shared use
- Add controllerutil helpers: GetAuthzConfigForWorkload,
  ValidateAuthzConfigReady, GenerateAuthzVolumeConfigFromRef,
  EnsureAuthzConfigMapFromRef, AddAuthzConfigRefOptions
- Add handleAuthzConfig to MCPServer, MCPRemoteProxy, VirtualMCPServer
  controllers following the OIDC config ref pattern
- Add MCPAuthzConfig watches to all three workload controllers
- Update runconfig builders to resolve AuthzConfigRef
- Update vmcpconfig converter to extract Cedar policies from
  MCPAuthzConfig for VirtualMCPServer runtime config
- Add SetAuthzConfigHash to VirtualMCPServer status manager

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added the size/L Large PR: 600-999 lines changed label Apr 13, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 13, 2026

Codecov Report

❌ Patch coverage is 9.60000% with 339 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.44%. Comparing base (734e8db) to head (440ffe3).

Files with missing lines Patch % Lines
...d/thv-operator/controllers/mcpserver_controller.go 7.95% 77 Missing and 4 partials ⚠️
...-operator/controllers/mcpremoteproxy_controller.go 6.09% 75 Missing and 2 partials ⚠️
cmd/thv-operator/pkg/controllerutil/authz.go 17.24% 70 Missing and 2 partials ⚠️
...perator/controllers/virtualmcpserver_controller.go 7.35% 60 Missing and 3 partials ⚠️
cmd/thv-operator/pkg/vmcpconfig/converter.go 0.00% 25 Missing and 2 partials ⚠️
...v-operator/controllers/mcpremoteproxy_runconfig.go 22.22% 5 Missing and 2 partials ⚠️
...md/thv-operator/controllers/mcpserver_runconfig.go 0.00% 5 Missing and 2 partials ⚠️
...v-operator/pkg/virtualmcpserverstatus/collector.go 16.66% 4 Missing and 1 partial ⚠️
Additional details and impacted files
@@                       Coverage Diff                        @@
##           jaosorior/mcpauthzconfig-crd    #4778      +/-   ##
================================================================
- Coverage                         68.85%   68.44%   -0.41%     
================================================================
  Files                               520      519       -1     
  Lines                             55137    55199      +62     
================================================================
- Hits                              37964    37783     -181     
- Misses                            14242    14499     +257     
+ Partials                           2931     2917      -14     

☔ View full report in Codecov by Sentry.
📢 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.

Comment on lines 2338 to +2356
)
}

// handleAuthzConfig validates and tracks the hash of the referenced MCPAuthzConfig.
// It updates the MCPServer status when the authorization configuration changes and sets
// the AuthzConfigRefValidated condition.
func (r *MCPServerReconciler) handleAuthzConfig(ctx context.Context, m *mcpv1alpha1.MCPServer) error {
ctxLogger := log.FromContext(ctx)

if m.Spec.AuthzConfigRef == nil {
// No MCPAuthzConfig referenced, clear any stored hash
if m.Status.AuthzConfigHash != "" {
m.Status.AuthzConfigHash = ""
if err := r.Status().Update(ctx, m); err != nil {
return fmt.Errorf("failed to clear MCPAuthzConfig hash from status: %w", err)
}
}
return nil
}
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.

🔴 When AuthzConfigRef is removed from an MCPServer or VirtualMCPServer spec, the ConditionAuthzConfigRefValidated condition is left stale in status instead of being cleared. This causes the condition to remain True indefinitely, misleading operators and any tooling that inspects status conditions about whether authorization is still active.

Extended reasoning...

Root cause — MCPServer

In MCPServer.handleAuthzConfig() (mcpserver_controller.go), the nil-ref branch clears AuthzConfigHash but never calls meta.RemoveStatusCondition:

if m.Spec.AuthzConfigRef == nil {
    if m.Status.AuthzConfigHash != "" {
        m.Status.AuthzConfigHash = ""
        if err := r.Status().Update(ctx, m); err != nil { ... }
    }
    return nil   // ← ConditionAuthzConfigRefValidated is never removed
}

Root cause — VirtualMCPServer

In VirtualMCPServer.handleAuthzConfig() (virtualmcpserver_controller.go), the nil-ref branch has the same omission through the StatusManager abstraction:

if vmcp.Spec.IncomingAuth == nil || vmcp.Spec.IncomingAuth.AuthzConfigRef == nil {
    if vmcp.Status.AuthzConfigHash != "" {
        statusManager.SetAuthzConfigHash("")
    }
    return nil   // ← no SetCondition/removal call for ConditionAuthzConfigRefValidated
}

The reference implementation shows the correct pattern

MCPRemoteProxy.handleAuthzConfig() (introduced in the same PR) does it correctly:

if proxy.Spec.AuthzConfigRef == nil {
    meta.RemoveStatusCondition(&proxy.Status.Conditions, mcpv1alpha1.ConditionAuthzConfigRefValidated)
    if proxy.Status.AuthzConfigHash != "" { ... }
    return nil
}

Step-by-step proof (MCPServer)

  1. User creates an MCPServer with spec.authzConfigRef.name: my-authz.
  2. Controller reconciles: handleAuthzConfig fetches the config, validates it, sets ConditionAuthzConfigRefValidated=True and stores the hash.
  3. User removes spec.authzConfigRef from the MCPServer (sets to nil).
  4. Controller reconciles: handleAuthzConfig enters the nil branch, clears AuthzConfigHash, and returns — no call to meta.RemoveStatusCondition.
  5. Result: kubectl get mcpserver <name> -o yaml still shows ConditionAuthzConfigRefValidated=True indefinitely, even though no authorization config is referenced.

The same sequence applies to VirtualMCPServer via the StatusManager path.

Why existing code doesn't prevent it

The StatusCollector for VirtualMCPServer is instantiated fresh each reconciliation with an empty conditions map. Conditions not explicitly set during a reconcile pass are left untouched in the live status (only conditions stored in s.conditions are applied in UpdateStatus). So the omission of a SetCondition call with removal intent means the live stale condition persists.

Fix

  • MCPServer: Add meta.RemoveStatusCondition(&m.Status.Conditions, mcpv1alpha1.ConditionAuthzConfigRefValidated) at the start of the nil branch, matching MCPRemoteProxy.
  • VirtualMCPServer: Call statusManager.SetCondition(mcpv1alpha1.ConditionAuthzConfigRefValidated, "", "", "") or use the existing empty-Status marker trick in RemoveConditionsWithPrefix so the collector removes the condition in UpdateStatus.

Copy link
Copy Markdown
Contributor

@jhrozek jhrozek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good -- solid pattern consistency with MCPOIDCConfig. Two minor nits inline.

// set the type. Full support for other backends is a follow-up.
return &vmcpconfig.AuthzConfig{
Type: authzConfig.Spec.Type,
}, nil
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.

nit: For non-Cedar types this returns an AuthzConfig with only Type set and no actual config. At runtime the vMCP would fail with an unclear error. Would it be better to fail fast here?

Suggested change
}, nil
return nil, fmt.Errorf("MCPAuthzConfig type %%q is not yet supported for VirtualMCPServer; only cedarv1 is supported", authzConfig.Spec.Type)

@jhrozek
Copy link
Copy Markdown
Contributor

jhrozek commented Apr 13, 2026

nit: cmd/thv-operator/controllers/mcpauthzconfig_controller.go — The MCPOIDCConfig controller declares RBAC markers for the workload types it lists/watches (virtualmcpservers, mcpremoteproxies). This controller lists all three workload types in findReferencingWorkloads and watches them in SetupWithManager, but only declares RBAC for mcpauthzconfigs. Works today via aggregation from other controllers, but should these be added for completeness?

// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpservers,verbs=get;list;watch
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=virtualmcpservers,verbs=get;list;watch
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpremoteproxies,verbs=get;list;watch

@JAORMX JAORMX force-pushed the jaosorior/mcpauthzconfig-crd branch from 734e8db to d571e89 Compare April 14, 2026 06:19
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.

2 participants