Skip to main content

Migrate to v1beta1

The ToolHive Kubernetes Operator has undergone a series of CRD API changes in preparation for the v1beta1 API version promotion. These changes span releases v0.15.0 through v0.20.0, each introducing breaking changes that require manifest and tooling updates.

This guide covers every breaking change in order, so you can start from whichever version you are currently running and work forward.

Upgrade one version at a time

Upgrade through each breaking version in sequence rather than skipping straight to the latest. Later CRD schemas remove fields that earlier versions still accept, so jumping ahead can leave resources in a state that is difficult to recover from. Follow the version sections below in order, validating your cluster after each step.

Change summary

The table below lists every breaking CRD change across the stabilization track. Find your current version in the Version column and review everything from that point forward.

VersionChangeAffected resourcesImpact
v0.15.0Deprecated fields removedMCPServer, MCPRemoteProxy, VirtualMCPServerManifests using spec.port, spec.targetPort, inline spec.tools, plaintext clientSecret, or thvCABundlePath fail validation
v0.15.0referencingServers replacedMCPOIDCConfig, MCPToolConfig, MCPExternalAuthConfig, MCPTelemetryConfigStatus field changed from []string to structured []{kind, name}
v0.15.0Cedar policy scope expandedAll workloads with Cedar enabledPreviously unchecked operations may now be denied
v0.16.0MCPOIDCConfig condition renamedMCPOIDCConfigReady condition renamed to Valid
v0.16.0Integer fields narrowed to int32VirtualMCPServerStatus, MCPGroupStatus, MCPRegistryDatabaseConfig, SyncStatusTyped clients must be regenerated
v0.16.0SSA list-type annotations addedAll CRDs (approx. 40 slice fields)Duplicate list keys rejected at admission
v0.16.0Helm operator.env type changedHelm valuesMap syntax (MY_VAR: value) must become list syntax
v0.17.0Phase value standardized to ReadyMCPServer, EmbeddingServer, MCPRegistryScripts checking .status.phase == "Running" must update
v0.17.0MCPRegistry spec restructuredMCPRegistryFlat registries[] replaced with sources[] / registries[] split
v0.17.0MCPRegistry status simplifiedMCPRegistrysyncStatus / apiStatus replaced with phase + Ready condition
v0.18.0MCPRegistry legacy fields removedMCPRegistryconfigYAML is now required; typed fields removed
v0.19.0remoteURL / externalURL renamedMCPRemoteProxy, MCPServerEntryJSON tags changed to camelCase; existing etcd data silently lost
v0.19.0enforceServers removedMCPRegistryField removed from schema; manifests with it fail validation
v0.20.0groupRef changed to typed structMCPServer, MCPRemoteProxy, MCPServerEntry, VirtualMCPServerBare string groupRef: name must become groupRef: { name: name }
v0.20.0protectedResourceAllowPrivateIP separatedVirtualMCPServer (OIDC config)Must be set explicitly; no longer inherited from jwksAllowPrivateIP

Deprecations not yet removed

These fields still work but will be removed at or after the v1beta1 promotion. Migrate when convenient.

Deprecated fieldReplacementAffected resourcesDeprecated in
spec.oidcConfig (inline)spec.oidcConfigRef (MCPOIDCConfig)MCPServer, VirtualMCPServerv0.15.0
spec.telemetry (inline)spec.telemetryConfigRef (MCPTelemetryConfig)MCPServerv0.15.0
spec.telemetry (inline)spec.telemetryConfigRef (MCPTelemetryConfig)MCPRemoteProxyv0.19.0
backendAuthType: external_auth_config_refexternalAuthConfigRefVirtualMCPServerv0.19.0
spec.config.groupRefspec.groupRef (typed struct)VirtualMCPServerv0.20.0
spec.config.telemetryspec.telemetryConfigRefVirtualMCPServerv0.20.0

General upgrade procedure

Work through each version in sequence. For each version upgrade:

  1. Back up existing resources before applying CRD changes:

    kubectl get toolhive -A -o yaml > toolhive-backup.yaml
  2. Apply updated CRDs for the target version, using whichever method matches your initial installation (see Deploy the operator for full details):

    Helm
    helm upgrade --install toolhive-operator-crds \
    oci://ghcr.io/stacklok/toolhive/toolhive-operator-crds \
    --version <VERSION> -n toolhive-system
    kubectl
    kubectl apply --server-side \
    -f https://raw.githubusercontent.com/stacklok/toolhive/refs/tags/<VERSION>/deploy/charts/operator-crds/crds/
  3. Update manifests according to the version-specific instructions below. Some versions require manifest changes before re-applying resources - check each section for the exact order.

  4. Upgrade the operator via Helm:

    helm upgrade toolhive-operator \
    oci://ghcr.io/stacklok/toolhive/toolhive-operator \
    -n toolhive-system -f your-values.yaml
  5. Validate that resources reconcile correctly:

    kubectl get toolhive -A

v0.15.0

Full release notes

Removed deprecated CRD fields

Six deprecated fields have been removed from MCPServer and MCPRemoteProxy. Manifests using any of these fields fail validation after upgrading.

Removed fieldReplacementResources
spec.portspec.proxyPortMCPServer, MCPRemoteProxy
spec.targetPortspec.mcpPortMCPServer
spec.tools (inline ToolsFilter)spec.toolConfigRef referencing an MCPToolConfigMCPServer
spec.oidcConfig.inline.clientSecretspec.oidcConfig.inline.clientSecretRef (Secret reference)MCPServer, MCPRemoteProxy, VirtualMCPServer
spec.oidcConfig.inline.thvCABundlePathspec.oidcConfig.inline.caBundleRef (ConfigMap reference)MCPServer, MCPRemoteProxy, VirtualMCPServer

Port fields - direct rename:

# Before
spec:
port: 9090
targetPort: 3000

# After
spec:
proxyPort: 9090
mcpPort: 3000

Tools filter - create a separate MCPToolConfig resource and reference it:

# Before
spec:
tools:
- name: allowed-tool

# After (create the MCPToolConfig resource, then reference it)
spec:
toolConfigRef:
name: my-tool-config

Client secret - move the plaintext value into a Kubernetes Secret:

# Before
spec:
oidcConfig:
inline:
clientSecret: my-secret-value

# After
spec:
oidcConfig:
inline:
clientSecretRef:
name: oidc-secret
key: client-secret

CA bundle path - store the certificate in a ConfigMap:

# Before
spec:
oidcConfig:
inline:
thvCABundlePath: /path/to/ca.crt

# After
spec:
oidcConfig:
inline:
caBundleRef:
configMapRef:
name: ca-bundle
key: ca.crt

referencingServers replaced with referencingWorkloads

The status.referencingServers field (a plain []string) has been replaced with status.referencingWorkloads (an array of {kind, name} objects) on four shared configuration CRDs: MCPOIDCConfig, MCPToolConfig, MCPExternalAuthConfig, and MCPTelemetryConfig.

# Before
status:
referencingServers:
- "my-server"
- "my-other-server"

# After
status:
referencingWorkloads:
- kind: MCPServer
name: my-server
- kind: VirtualMCPServer
name: my-other-server

Update any scripts, monitoring, or tooling that reads .status.referencingServers to read .status.referencingWorkloads[].name (and optionally .kind).

Expanded Cedar policy enforcement

Cedar authorization now covers optimizer meta-tools (find_tool, call_tool) and upstream IDP token claims. If you have Cedar policies enabled, review your policy sets - operations that were previously unchecked may now be denied.

Specifically:

  • Optimizer meta-tools: find_tool and call_tool are now filtered and authorized by Cedar. If your policies don't permit these tools, clients will see zero tools when the optimizer is enabled.
  • Upstream IDP claims: Cedar policies can now evaluate claims from upstream identity provider tokens (e.g., GitHub login, Okta groups). If the upstream token is opaque (non-JWT), the authorizer denies the request.

v0.16.0

Full release notes

MCPOIDCConfig condition type renamed

The status condition type on MCPOIDCConfig has been renamed from Ready to Valid, aligning it with MCPExternalAuthConfig, MCPTelemetryConfig, and MCPToolConfig.

Update any automation, alerts, or scripts that watch this condition:

# Find references to the old condition name
grep -r '"Ready"' --include="*.yaml" . | grep -i oidc

Replace type: Ready with type: Valid on any MCPOIDCConfig status condition references.

CRD integer fields changed to int32

Eight CRD fields previously typed as int64 are now int32:

CRDFields
VirtualMCPServerStatusBackendCount
MCPGroupStatusServerCount, RemoteProxyCount
MCPRegistryDatabaseConfig (removed in v0.18.0)Port, MaxOpenConns, MaxIdleConns
SyncStatus (removed in v0.17.0 status simplification)AttemptCount, ServerCount

YAML manifests are not affected - the values themselves are unchanged. If you have typed Go, Python, or other language clients generated from the previous CRD schema, regenerate them after applying the updated CRDs.

Server-side apply list-type annotations

All CRD slice fields now carry x-kubernetes-list-type annotations, activating correct Server-Side Apply merge semantics. This affects approximately 40 slice fields across all 11 CRD types.

If you use GitOps tools with server-side apply (Flux, Argo CD), this change enforces list-key uniqueness at admission. Manifests with duplicate keys in list fields will be rejected.

Before upgrading, review your manifests for duplicate keys (e.g., two env entries with the same name).

Helm operator.env type changed

The operator.env Helm value was changed from map syntax to list syntax:

# Before (invalid)
operator:
env:
MY_VAR: my-value

# After (correct)
operator:
env:
- name: MY_VAR
value: my-value

v0.17.0

Full release notes

Phase value standardized to Ready

MCPServer, EmbeddingServer, and MCPRegistry previously reported Running as their healthy phase value. All workload CRDs now consistently use Ready.

Update any scripts, monitoring alerts, Helm hooks, or CI pipelines:

# Find references to the old phase value
grep -rn '"Running"' --include="*.yaml" --include="*.sh" .

Replace .status.phase == "Running" with .status.phase == "Ready".

MCPRegistry spec restructured

The MCPRegistry CRD spec has been restructured to align with the registry server v2 config format. The flat registries[] with inline source configs has been replaced with separate top-level sources[] and registries[] fields.

Key changes:

  • PVC-based registry sources have been removed entirely.
  • Auto-injection of a default Kubernetes source has been removed - you must explicitly declare all sources.
  • A new configYAML escape hatch is available as an alternative to the typed fields.
info

The typed spec fields (sources, registries, databaseConfig, authConfig, telemetryConfig) were deprecated in v0.17.0 and removed in v0.18.0. See the v0.18.0 section for the required configYAML format.

MCPRegistry status simplified

MCPRegistryStatus has been flattened from a three-phase model (SyncStatus + APIStatus + DeriveOverallPhase) to the standard Kubernetes workload pattern: Phase + Ready condition + ReadyReplicas + URL.

If you read .status.syncStatus or .status.apiStatus, switch to .status.phase and .status.conditions (type Ready). kubectl wait --for=condition=Ready now works consistently for MCPRegistry.

v0.18.0

Full release notes

MCPRegistry legacy fields removed

The spec.configYAML field is now required. The five legacy typed fields (sources, registries, databaseConfig, authConfig, telemetryConfig) have been removed from the schema.

# Before (legacy typed fields)
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: my-registry
spec:
sources:
- name: production
format: toolhive
configMapRef:
name: prod-registry
key: registry.json
syncPolicy:
interval: "1h"
registries:
- name: default
sources: ["production"]
databaseConfig:
host: postgres
port: 5432
user: db_app
database: registry
sslMode: require
dbAppUserPasswordSecretRef:
name: db-credentials
key: app_password
authConfig:
mode: anonymous

# After (configYAML)
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: my-registry
spec:
configYAML: |
sources:
- name: production
format: toolhive
file:
path: /config/registry/production/registry.json
syncPolicy:
interval: 1h
registries:
- name: default
sources: ["production"]
database:
host: postgres
port: 5432
user: db_app
database: registry
sslMode: require
auth:
mode: anonymous
volumes:
- name: registry-data
configMap:
name: prod-registry
items:
- key: registry.json
path: registry.json
volumeMounts:
- name: registry-data
mountPath: /config/registry/production
readOnly: true
pgpassSecretRef:
name: my-pgpass-secret
key: .pgpass

Migration steps:

  1. Add spec.configYAML with your registry server config in the registry server's native config.yaml format.
  2. Move ConfigMap, PVC, and Secret references to spec.volumes and spec.volumeMounts with explicit mount paths matching the file paths in configYAML.
  3. If using databaseConfig, create a pgpass-formatted Secret and reference it via spec.pgpassSecretRef. The operator handles the init container and file permissions automatically.
  4. Do not inline credentials in configYAML - it is stored in a ConfigMap, not a Secret. Mount actual secrets using volumes/volumeMounts.
  5. Remove the legacy typed fields from your manifests.

v0.19.0

Full release notes

remoteURL / externalURL renamed to camelCase

The JSON tags for URL fields on MCPRemoteProxy and MCPServerEntry have been renamed to standard camelCase.

CRDOld fieldNew field
MCPRemoteProxy .specremoteURLremoteUrl
MCPRemoteProxy .statusexternalURLexternalUrl
MCPServerEntry .specremoteURLremoteUrl
Risk: silent data loss

This is a high-risk change. After the CRD upgrade, existing resources stored in etcd still contain the old remoteURL key. The controller deserializes this as an empty string, causing reconciliation to fail. You must re-apply all affected resources after updating the CRD.

Migration steps:

  1. Update the CRD manifests.
  2. Find and replace remoteURL: with remoteUrl: in all MCPRemoteProxy and MCPServerEntry manifests.
  3. Re-apply every affected resource to rewrite the etcd data. Waiting for reconciliation alone will not help.
  4. Update any JSONPath queries or tooling (e.g., kubectl get mcprp -o jsonpath='{.spec.remoteUrl}').
# Before
spec:
remoteURL: https://mcp.example.com

# After
spec:
remoteUrl: https://mcp.example.com

enforceServers removed

The enforceServers field has been removed from the MCPRegistry schema. Manifests that include it will fail validation.

This feature was non-functional since v0.6.0, so removing it has no behavioral effect. Delete enforceServers from all MCPRegistry manifests.

v0.20.0

Full release notes

groupRef changed to typed struct

The groupRef field on MCPServer, MCPRemoteProxy, MCPServerEntry, and VirtualMCPServer has changed from a bare string to a typed struct.

# Before
spec:
groupRef: my-group

# After
spec:
groupRef:
name: my-group

For VirtualMCPServer, also move spec.config.groupRef to spec.groupRef (the old path still works but is deprecated).

warning

Existing resources in etcd have the old string format and will fail re-validation. You must delete and re-create affected resources, or use kubectl replace after updating your manifests.

Migration steps:

  1. Update all YAML manifests, GitOps pipelines, and Helm values to use the struct format.

  2. Apply the new CRDs.

  3. Back up, delete, and re-create affected resources:

    # Back up
    kubectl get mcpservers -A -o yaml > mcpservers-backup.yaml
    # Edit backup to use new format, then:
    kubectl delete mcpservers --all -A
    kubectl apply -f mcpservers-backup.yaml
    # Repeat for mcpremoteproxies, mcpserverentries, virtualmcpservers

protectedResourceAllowPrivateIP separated

The protectedResourceAllowPrivateIP field on VirtualMCPServer's OIDC configuration is no longer derived from jwksAllowPrivateIP. If your protected resource endpoint is on a private IP, you must set both fields explicitly.

# Before (protectedResourceAllowPrivateIP was silently
# inherited from jwksAllowPrivateIP)
spec:
config:
incomingAuth:
oidc:
jwksAllowPrivateIP: true

# After (both fields must be set explicitly)
spec:
config:
incomingAuth:
oidc:
jwksAllowPrivateIP: true
protectedResourceAllowPrivateIP: true

Next steps