diff --git a/cmd/thv-operator/api/v1alpha1/types.go b/cmd/thv-operator/api/v1alpha1/types.go
index ef84fd4cb8..8b2c9e50ae 100644
--- a/cmd/thv-operator/api/v1alpha1/types.go
+++ b/cmd/thv-operator/api/v1alpha1/types.go
@@ -339,9 +339,9 @@ type MCPToolConfigList struct {
//+kubebuilder:subresource:status
//+kubebuilder:resource:shortName=vmcpctd;compositetool,categories=toolhive
//+kubebuilder:printcolumn:name="Workflow",type="string",JSONPath=".spec.name",description="Workflow name"
-//+kubebuilder:printcolumn:name="Steps",type="integer",JSONPath=".spec.steps[*]",description="Number of steps"
+//+kubebuilder:printcolumn:name="Steps",type="integer",JSONPath=".status.stepCount",description="Number of steps"
//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.validationStatus",description="Validation status"
-//+kubebuilder:printcolumn:name="Refs",type="integer",JSONPath=".status.referencingVirtualServers[*]",description="Refs"
+//+kubebuilder:printcolumn:name="Refs",type="integer",JSONPath=".status.refCount",description="Referencing servers"
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status"
diff --git a/cmd/thv-operator/api/v1beta1/virtualmcpcompositetooldefinition_types.go b/cmd/thv-operator/api/v1beta1/virtualmcpcompositetooldefinition_types.go
index e2a4ec0da6..d71aa89fbe 100644
--- a/cmd/thv-operator/api/v1beta1/virtualmcpcompositetooldefinition_types.go
+++ b/cmd/thv-operator/api/v1beta1/virtualmcpcompositetooldefinition_types.go
@@ -18,6 +18,14 @@ type VirtualMCPCompositeToolDefinitionSpec struct {
// VirtualMCPCompositeToolDefinitionStatus defines the observed state of VirtualMCPCompositeToolDefinition
type VirtualMCPCompositeToolDefinitionStatus struct {
+ // StepCount is the number of steps in the composite tool workflow.
+ // +optional
+ StepCount int32 `json:"stepCount,omitempty"`
+
+ // RefCount is the number of VirtualMCPServers that reference this workflow.
+ // +optional
+ RefCount int32 `json:"refCount,omitempty"`
+
// ValidationStatus indicates the validation state of the workflow
// - Valid: Workflow structure is valid
// - Invalid: Workflow has validation errors
@@ -102,9 +110,9 @@ const (
//+kubebuilder:subresource:status
//+kubebuilder:resource:shortName=vmcpctd;compositetool,categories=toolhive
//+kubebuilder:printcolumn:name="Workflow",type="string",JSONPath=".spec.name",description="Workflow name"
-//+kubebuilder:printcolumn:name="Steps",type="integer",JSONPath=".spec.steps[*]",description="Number of steps"
+//+kubebuilder:printcolumn:name="Steps",type="integer",JSONPath=".status.stepCount",description="Number of steps"
//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.validationStatus",description="Validation status"
-//+kubebuilder:printcolumn:name="Refs",type="integer",JSONPath=".status.referencingVirtualServers[*]",description="Refs"
+//+kubebuilder:printcolumn:name="Refs",type="integer",JSONPath=".status.refCount",description="Referencing servers"
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status"
diff --git a/cmd/thv-operator/app/app.go b/cmd/thv-operator/app/app.go
index 4bfb1b52fc..0319208837 100644
--- a/cmd/thv-operator/app/app.go
+++ b/cmd/thv-operator/app/app.go
@@ -316,7 +316,7 @@ func setupRegistryController(mgr ctrl.Manager, imagePullSecretsDefaults imagepul
}
// setupAggregationControllers sets up Virtual MCP-related controllers and webhooks
-// (MCPGroup, VirtualMCPServer, and their webhooks). Must run after
+// (MCPGroup, VirtualMCPServer, VirtualMCPCompositeToolDefinition, and their webhooks). Must run after
// setupServerControllers, which creates the MCPServer.Spec.GroupRef field index
// these controllers depend on.
// imagePullSecretsDefaults are merged with vmcp.Spec.ImagePullSecrets when the
@@ -340,6 +340,13 @@ func setupAggregationControllers(mgr ctrl.Manager, imagePullSecretsDefaults imag
return fmt.Errorf("unable to create controller VirtualMCPServer: %w", err)
}
+ // Set up VirtualMCPCompositeToolDefinition controller
+ if err := (&controllers.VirtualMCPCompositeToolDefinitionReconciler{
+ Client: mgr.GetClient(),
+ }).SetupWithManager(mgr); err != nil {
+ return fmt.Errorf("unable to create controller VirtualMCPCompositeToolDefinition: %w", err)
+ }
+
return nil
}
diff --git a/cmd/thv-operator/controllers/virtualmcpcompositetooldefinition_controller.go b/cmd/thv-operator/controllers/virtualmcpcompositetooldefinition_controller.go
new file mode 100644
index 0000000000..3c0f4579de
--- /dev/null
+++ b/cmd/thv-operator/controllers/virtualmcpcompositetooldefinition_controller.go
@@ -0,0 +1,143 @@
+// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+package controllers
+
+import (
+ "context"
+ "sort"
+
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/types"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/handler"
+ "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+ mcpv1beta1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1beta1"
+ ctrlutil "github.com/stacklok/toolhive/cmd/thv-operator/pkg/controllerutil"
+)
+
+// VirtualMCPCompositeToolDefinitionReconciler maintains display-oriented status
+// for a VirtualMCPCompositeToolDefinition.
+type VirtualMCPCompositeToolDefinitionReconciler struct {
+ client.Client
+}
+
+// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=virtualmcpcompositetooldefinitions,verbs=get;list;watch
+// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=virtualmcpcompositetooldefinitions/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=virtualmcpservers,verbs=get;list;watch
+
+// Reconcile updates the scalar counts used by kubectl printer columns and
+// maintains the existing referencingVirtualServers status list.
+func (r *VirtualMCPCompositeToolDefinitionReconciler) Reconcile(
+ ctx context.Context,
+ req ctrl.Request,
+) (ctrl.Result, error) {
+ compositeToolDefinition := &mcpv1beta1.VirtualMCPCompositeToolDefinition{}
+ if err := r.Get(ctx, req.NamespacedName, compositeToolDefinition); err != nil {
+ if apierrors.IsNotFound(err) {
+ return ctrl.Result{}, nil
+ }
+ return ctrl.Result{}, err
+ }
+
+ referencingVirtualServers, err := r.findReferencingVirtualServers(ctx, compositeToolDefinition)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ stepCount := compositeToolDefinitionItemCount(len(compositeToolDefinition.Spec.Steps))
+ refCount := compositeToolDefinitionItemCount(len(referencingVirtualServers))
+
+ if err := ctrlutil.MutateAndPatchStatus(ctx, r.Client, compositeToolDefinition,
+ func(definition *mcpv1beta1.VirtualMCPCompositeToolDefinition) {
+ definition.Status.StepCount = stepCount
+ definition.Status.RefCount = refCount
+ definition.Status.ReferencingVirtualServers = referencingVirtualServers
+ }); err != nil {
+ return ctrl.Result{}, err
+ }
+
+ return ctrl.Result{}, nil
+}
+
+func (r *VirtualMCPCompositeToolDefinitionReconciler) findReferencingVirtualServers(
+ ctx context.Context,
+ compositeToolDefinition *mcpv1beta1.VirtualMCPCompositeToolDefinition,
+) ([]string, error) {
+ virtualMCPServers := &mcpv1beta1.VirtualMCPServerList{}
+ if err := r.List(ctx, virtualMCPServers, client.InNamespace(compositeToolDefinition.Namespace)); err != nil {
+ return nil, err
+ }
+
+ referencingVirtualServers := make([]string, 0)
+ for _, virtualMCPServer := range virtualMCPServers.Items {
+ for _, ref := range virtualMCPServer.Spec.Config.CompositeToolRefs {
+ if ref.Name == compositeToolDefinition.Name {
+ referencingVirtualServers = append(referencingVirtualServers, virtualMCPServer.Name)
+ break
+ }
+ }
+ }
+ sort.Strings(referencingVirtualServers)
+ return referencingVirtualServers, nil
+}
+
+func compositeToolDefinitionItemCount(length int) int32 {
+ return int32(length) //nolint:gosec // Kubernetes object size limits keep CRD list lengths within int32.
+}
+
+// mapVirtualMCPServerToCompositeToolDefinitions enqueues both definitions a server
+// currently references and definitions that still contain a stale reference to it.
+func (r *VirtualMCPCompositeToolDefinitionReconciler) mapVirtualMCPServerToCompositeToolDefinitions(
+ ctx context.Context,
+ obj client.Object,
+) []reconcile.Request {
+ virtualMCPServer, ok := obj.(*mcpv1beta1.VirtualMCPServer)
+ if !ok {
+ return nil
+ }
+
+ requests := make([]reconcile.Request, 0, len(virtualMCPServer.Spec.Config.CompositeToolRefs))
+ seen := make(map[types.NamespacedName]struct{})
+ for _, ref := range virtualMCPServer.Spec.Config.CompositeToolRefs {
+ name := types.NamespacedName{Name: ref.Name, Namespace: virtualMCPServer.Namespace}
+ if _, exists := seen[name]; exists {
+ continue
+ }
+ seen[name] = struct{}{}
+ requests = append(requests, reconcile.Request{NamespacedName: name})
+ }
+
+ definitions := &mcpv1beta1.VirtualMCPCompositeToolDefinitionList{}
+ if err := r.List(ctx, definitions, client.InNamespace(virtualMCPServer.Namespace)); err != nil {
+ log.FromContext(ctx).Error(err, "Failed to list VirtualMCPCompositeToolDefinitions for VirtualMCPServer watch")
+ return requests
+ }
+ for _, definition := range definitions.Items {
+ name := types.NamespacedName{Name: definition.Name, Namespace: definition.Namespace}
+ if _, exists := seen[name]; exists {
+ continue
+ }
+ for _, referencingVirtualServer := range definition.Status.ReferencingVirtualServers {
+ if referencingVirtualServer == virtualMCPServer.Name {
+ requests = append(requests, reconcile.Request{NamespacedName: name})
+ break
+ }
+ }
+ }
+ return requests
+}
+
+// SetupWithManager configures reconciliation of definitions and the virtual
+// servers whose references determine the Refs column.
+func (r *VirtualMCPCompositeToolDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ virtualMCPServerHandler := handler.EnqueueRequestsFromMapFunc(r.mapVirtualMCPServerToCompositeToolDefinitions)
+
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&mcpv1beta1.VirtualMCPCompositeToolDefinition{}).
+ Watches(&mcpv1beta1.VirtualMCPServer{}, virtualMCPServerHandler).
+ Complete(r)
+}
diff --git a/cmd/thv-operator/controllers/virtualmcpcompositetooldefinition_controller_test.go b/cmd/thv-operator/controllers/virtualmcpcompositetooldefinition_controller_test.go
new file mode 100644
index 0000000000..7ddf18c3bf
--- /dev/null
+++ b/cmd/thv-operator/controllers/virtualmcpcompositetooldefinition_controller_test.go
@@ -0,0 +1,275 @@
+// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+package controllers
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+ "sigs.k8s.io/controller-runtime/pkg/client/interceptor"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+ mcpv1beta1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1beta1"
+ vmcpconfig "github.com/stacklok/toolhive/pkg/vmcp/config"
+)
+
+func TestVirtualMCPCompositeToolDefinitionReconciler_Reconcile(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ definition *mcpv1beta1.VirtualMCPCompositeToolDefinition
+ virtualMCPServers []mcpv1beta1.VirtualMCPServer
+ expectedStepCount int32
+ expectedRefCount int32
+ expectedReferencingVMCPNames []string
+ }{
+ {
+ name: "populates step and reference counts",
+ definition: &mcpv1beta1.VirtualMCPCompositeToolDefinition{
+ ObjectMeta: metav1.ObjectMeta{Name: "workflow", Namespace: "default"},
+ Spec: mcpv1beta1.VirtualMCPCompositeToolDefinitionSpec{
+ CompositeToolConfig: vmcpconfig.CompositeToolConfig{
+ Steps: []vmcpconfig.WorkflowStepConfig{{ID: "first"}, {ID: "second"}},
+ },
+ },
+ },
+ virtualMCPServers: []mcpv1beta1.VirtualMCPServer{
+ virtualMCPServerWithCompositeToolRef("server-b", "workflow"),
+ virtualMCPServerWithCompositeToolRef("server-a", "workflow"),
+ virtualMCPServerWithCompositeToolRef("unrelated", "other-workflow"),
+ },
+ expectedStepCount: 2,
+ expectedRefCount: 2,
+ expectedReferencingVMCPNames: []string{"server-a", "server-b"},
+ },
+ {
+ name: "clears references no longer present",
+ definition: &mcpv1beta1.VirtualMCPCompositeToolDefinition{
+ ObjectMeta: metav1.ObjectMeta{Name: "workflow", Namespace: "default"},
+ Status: mcpv1beta1.VirtualMCPCompositeToolDefinitionStatus{
+ RefCount: 1,
+ ReferencingVirtualServers: []string{"removed-server"},
+ },
+ },
+ expectedStepCount: 0,
+ expectedRefCount: 0,
+ expectedReferencingVMCPNames: nil,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ scheme := runtime.NewScheme()
+ require.NoError(t, mcpv1beta1.AddToScheme(scheme))
+
+ objects := make([]runtime.Object, 0, len(tt.virtualMCPServers)+1)
+ objects = append(objects, tt.definition)
+ for i := range tt.virtualMCPServers {
+ objects = append(objects, &tt.virtualMCPServers[i])
+ }
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithRuntimeObjects(objects...).
+ WithStatusSubresource(&mcpv1beta1.VirtualMCPCompositeToolDefinition{}).
+ Build()
+ reconciler := &VirtualMCPCompositeToolDefinitionReconciler{Client: fakeClient}
+ key := types.NamespacedName{Name: tt.definition.Name, Namespace: tt.definition.Namespace}
+
+ _, err := reconciler.Reconcile(t.Context(), reconcile.Request{
+ NamespacedName: key,
+ })
+ require.NoError(t, err)
+
+ updated := &mcpv1beta1.VirtualMCPCompositeToolDefinition{}
+ require.NoError(t, fakeClient.Get(t.Context(), key, updated))
+ assert.Equal(t, tt.expectedStepCount, updated.Status.StepCount)
+ assert.Equal(t, tt.expectedRefCount, updated.Status.RefCount)
+ assert.Equal(t, tt.expectedReferencingVMCPNames, updated.Status.ReferencingVirtualServers)
+ })
+ }
+}
+
+func TestVirtualMCPCompositeToolDefinitionReconciler_ReconcileNotFound(t *testing.T) {
+ t.Parallel()
+
+ scheme := runtime.NewScheme()
+ require.NoError(t, mcpv1beta1.AddToScheme(scheme))
+
+ reconciler := &VirtualMCPCompositeToolDefinitionReconciler{
+ Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
+ }
+
+ result, err := reconciler.Reconcile(t.Context(), reconcile.Request{
+ NamespacedName: types.NamespacedName{Name: "missing", Namespace: "default"},
+ })
+
+ require.NoError(t, err)
+ assert.Equal(t, reconcile.Result{}, result)
+}
+
+func TestVirtualMCPCompositeToolDefinitionReconciler_ReconcileErrors(t *testing.T) {
+ t.Parallel()
+
+ scheme := runtime.NewScheme()
+ require.NoError(t, mcpv1beta1.AddToScheme(scheme))
+
+ definition := &mcpv1beta1.VirtualMCPCompositeToolDefinition{
+ ObjectMeta: metav1.ObjectMeta{Name: "workflow", Namespace: "default"},
+ }
+ getErr := errors.New("simulated get failure")
+ listErr := errors.New("simulated list failure")
+
+ t.Run("returns get failure", func(t *testing.T) {
+ t.Parallel()
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithInterceptorFuncs(interceptor.Funcs{
+ Get: func(
+ _ context.Context,
+ _ client.WithWatch,
+ _ client.ObjectKey,
+ _ client.Object,
+ _ ...client.GetOption,
+ ) error {
+ return getErr
+ },
+ }).
+ Build()
+ reconciler := &VirtualMCPCompositeToolDefinitionReconciler{Client: fakeClient}
+
+ _, err := reconciler.Reconcile(t.Context(), reconcile.Request{
+ NamespacedName: types.NamespacedName{Name: "workflow", Namespace: "default"},
+ })
+ require.ErrorIs(t, err, getErr)
+ })
+
+ t.Run("returns reference list failure", func(t *testing.T) {
+ t.Parallel()
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(definition.DeepCopy()).
+ WithInterceptorFuncs(interceptor.Funcs{
+ List: func(
+ _ context.Context,
+ _ client.WithWatch,
+ list client.ObjectList,
+ _ ...client.ListOption,
+ ) error {
+ if _, ok := list.(*mcpv1beta1.VirtualMCPServerList); ok {
+ return listErr
+ }
+ return nil
+ },
+ }).
+ Build()
+ reconciler := &VirtualMCPCompositeToolDefinitionReconciler{Client: fakeClient}
+
+ _, err := reconciler.Reconcile(t.Context(), reconcile.Request{
+ NamespacedName: types.NamespacedName{Name: definition.Name, Namespace: definition.Namespace},
+ })
+ require.ErrorIs(t, err, listErr)
+ })
+}
+
+func TestVirtualMCPCompositeToolDefinitionReconciler_MapVirtualMCPServer(t *testing.T) {
+ t.Parallel()
+
+ scheme := runtime.NewScheme()
+ require.NoError(t, mcpv1beta1.AddToScheme(scheme))
+ require.NoError(t, corev1.AddToScheme(scheme))
+
+ server := virtualMCPServerWithCompositeToolRef("server", "current")
+ server.Spec.Config.CompositeToolRefs = append(server.Spec.Config.CompositeToolRefs,
+ vmcpconfig.CompositeToolRef{Name: "current"})
+ current := &mcpv1beta1.VirtualMCPCompositeToolDefinition{
+ ObjectMeta: metav1.ObjectMeta{Name: "current", Namespace: "default"},
+ Status: mcpv1beta1.VirtualMCPCompositeToolDefinitionStatus{
+ ReferencingVirtualServers: []string{"server"},
+ },
+ }
+ stale := &mcpv1beta1.VirtualMCPCompositeToolDefinition{
+ ObjectMeta: metav1.ObjectMeta{Name: "stale", Namespace: "default"},
+ Status: mcpv1beta1.VirtualMCPCompositeToolDefinitionStatus{
+ ReferencingVirtualServers: []string{"server"},
+ },
+ }
+
+ t.Run("returns current and stale definitions without duplicates", func(t *testing.T) {
+ t.Parallel()
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(current.DeepCopy(), stale.DeepCopy()).
+ Build()
+ reconciler := &VirtualMCPCompositeToolDefinitionReconciler{Client: fakeClient}
+
+ assert.ElementsMatch(t, []reconcile.Request{
+ {NamespacedName: types.NamespacedName{Name: "current", Namespace: "default"}},
+ {NamespacedName: types.NamespacedName{Name: "stale", Namespace: "default"}},
+ }, reconciler.mapVirtualMCPServerToCompositeToolDefinitions(t.Context(), server.DeepCopy()))
+ })
+
+ t.Run("returns nil for unrelated object type", func(t *testing.T) {
+ t.Parallel()
+
+ reconciler := &VirtualMCPCompositeToolDefinitionReconciler{
+ Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
+ }
+ pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: "default"}}
+
+ assert.Nil(t, reconciler.mapVirtualMCPServerToCompositeToolDefinitions(t.Context(), pod))
+ })
+
+ t.Run("returns current definition if stale lookup fails", func(t *testing.T) {
+ t.Parallel()
+
+ listErr := errors.New("simulated definition list failure")
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithInterceptorFuncs(interceptor.Funcs{
+ List: func(
+ _ context.Context,
+ _ client.WithWatch,
+ list client.ObjectList,
+ _ ...client.ListOption,
+ ) error {
+ if _, ok := list.(*mcpv1beta1.VirtualMCPCompositeToolDefinitionList); ok {
+ return listErr
+ }
+ return nil
+ },
+ }).
+ Build()
+ reconciler := &VirtualMCPCompositeToolDefinitionReconciler{Client: fakeClient}
+
+ assert.Equal(t, []reconcile.Request{{
+ NamespacedName: types.NamespacedName{Name: "current", Namespace: "default"},
+ }}, reconciler.mapVirtualMCPServerToCompositeToolDefinitions(t.Context(), server.DeepCopy()))
+ })
+}
+
+func virtualMCPServerWithCompositeToolRef(name, definitionName string) mcpv1beta1.VirtualMCPServer {
+ return mcpv1beta1.VirtualMCPServer{
+ ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "default"},
+ Spec: mcpv1beta1.VirtualMCPServerSpec{
+ Config: vmcpconfig.Config{
+ CompositeToolRefs: []vmcpconfig.CompositeToolRef{{Name: definitionName}},
+ },
+ },
+ }
+}
diff --git a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpcompositetooldefinitions.yaml b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpcompositetooldefinitions.yaml
index a9c17deff5..3db50fa1e0 100644
--- a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpcompositetooldefinitions.yaml
+++ b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpcompositetooldefinitions.yaml
@@ -25,15 +25,15 @@ spec:
name: Workflow
type: string
- description: Number of steps
- jsonPath: .spec.steps[*]
+ jsonPath: .status.stepCount
name: Steps
type: integer
- description: Validation status
jsonPath: .status.validationStatus
name: Status
type: string
- - description: Refs
- jsonPath: .status.referencingVirtualServers[*]
+ - description: Referencing servers
+ jsonPath: .status.refCount
name: Refs
type: integer
- description: Age
@@ -395,6 +395,11 @@ spec:
It corresponds to the resource's generation, which is updated on mutation by the API Server
format: int64
type: integer
+ refCount:
+ description: RefCount is the number of VirtualMCPServers that reference
+ this workflow.
+ format: int32
+ type: integer
referencingVirtualServers:
description: |-
ReferencingVirtualServers lists VirtualMCPServer resources that reference this workflow
@@ -403,6 +408,11 @@ spec:
type: string
type: array
x-kubernetes-list-type: set
+ stepCount:
+ description: StepCount is the number of steps in the composite tool
+ workflow.
+ format: int32
+ type: integer
validationErrors:
description: ValidationErrors contains validation error messages if
ValidationStatus is Invalid
@@ -432,15 +442,15 @@ spec:
name: Workflow
type: string
- description: Number of steps
- jsonPath: .spec.steps[*]
+ jsonPath: .status.stepCount
name: Steps
type: integer
- description: Validation status
jsonPath: .status.validationStatus
name: Status
type: string
- - description: Refs
- jsonPath: .status.referencingVirtualServers[*]
+ - description: Referencing servers
+ jsonPath: .status.refCount
name: Refs
type: integer
- description: Age
@@ -802,6 +812,11 @@ spec:
It corresponds to the resource's generation, which is updated on mutation by the API Server
format: int64
type: integer
+ refCount:
+ description: RefCount is the number of VirtualMCPServers that reference
+ this workflow.
+ format: int32
+ type: integer
referencingVirtualServers:
description: |-
ReferencingVirtualServers lists VirtualMCPServer resources that reference this workflow
@@ -810,6 +825,11 @@ spec:
type: string
type: array
x-kubernetes-list-type: set
+ stepCount:
+ description: StepCount is the number of steps in the composite tool
+ workflow.
+ format: int32
+ type: integer
validationErrors:
description: ValidationErrors contains validation error messages if
ValidationStatus is Invalid
diff --git a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpcompositetooldefinitions.yaml b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpcompositetooldefinitions.yaml
index 39fcdea5fc..0fc1bc32f2 100644
--- a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpcompositetooldefinitions.yaml
+++ b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpcompositetooldefinitions.yaml
@@ -28,15 +28,15 @@ spec:
name: Workflow
type: string
- description: Number of steps
- jsonPath: .spec.steps[*]
+ jsonPath: .status.stepCount
name: Steps
type: integer
- description: Validation status
jsonPath: .status.validationStatus
name: Status
type: string
- - description: Refs
- jsonPath: .status.referencingVirtualServers[*]
+ - description: Referencing servers
+ jsonPath: .status.refCount
name: Refs
type: integer
- description: Age
@@ -398,6 +398,11 @@ spec:
It corresponds to the resource's generation, which is updated on mutation by the API Server
format: int64
type: integer
+ refCount:
+ description: RefCount is the number of VirtualMCPServers that reference
+ this workflow.
+ format: int32
+ type: integer
referencingVirtualServers:
description: |-
ReferencingVirtualServers lists VirtualMCPServer resources that reference this workflow
@@ -406,6 +411,11 @@ spec:
type: string
type: array
x-kubernetes-list-type: set
+ stepCount:
+ description: StepCount is the number of steps in the composite tool
+ workflow.
+ format: int32
+ type: integer
validationErrors:
description: ValidationErrors contains validation error messages if
ValidationStatus is Invalid
@@ -435,15 +445,15 @@ spec:
name: Workflow
type: string
- description: Number of steps
- jsonPath: .spec.steps[*]
+ jsonPath: .status.stepCount
name: Steps
type: integer
- description: Validation status
jsonPath: .status.validationStatus
name: Status
type: string
- - description: Refs
- jsonPath: .status.referencingVirtualServers[*]
+ - description: Referencing servers
+ jsonPath: .status.refCount
name: Refs
type: integer
- description: Age
@@ -805,6 +815,11 @@ spec:
It corresponds to the resource's generation, which is updated on mutation by the API Server
format: int64
type: integer
+ refCount:
+ description: RefCount is the number of VirtualMCPServers that reference
+ this workflow.
+ format: int32
+ type: integer
referencingVirtualServers:
description: |-
ReferencingVirtualServers lists VirtualMCPServer resources that reference this workflow
@@ -813,6 +828,11 @@ spec:
type: string
type: array
x-kubernetes-list-type: set
+ stepCount:
+ description: StepCount is the number of steps in the composite tool
+ workflow.
+ format: int32
+ type: integer
validationErrors:
description: ValidationErrors contains validation error messages if
ValidationStatus is Invalid
diff --git a/deploy/charts/operator/templates/clusterrole/role.yaml b/deploy/charts/operator/templates/clusterrole/role.yaml
index a7a8b3f6e4..488ccef1f3 100644
--- a/deploy/charts/operator/templates/clusterrole/role.yaml
+++ b/deploy/charts/operator/templates/clusterrole/role.yaml
@@ -144,6 +144,7 @@ rules:
- mcptelemetryconfigs/status
- mcptoolconfigs/status
- mcpwebhookconfigs/status
+ - virtualmcpcompositetooldefinitions/status
- virtualmcpservers/status
verbs:
- get
diff --git a/docs/operator/crd-api.md b/docs/operator/crd-api.md
index b8f04ff351..34423b9b37 100644
--- a/docs/operator/crd-api.md
+++ b/docs/operator/crd-api.md
@@ -3545,6 +3545,8 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
+| `stepCount` _integer_ | StepCount is the number of steps in the composite tool workflow. | | Optional: \{\}
|
+| `refCount` _integer_ | RefCount is the number of VirtualMCPServers that reference this workflow. | | Optional: \{\}
|
| `validationStatus` _[api.v1beta1.ValidationStatus](#apiv1beta1validationstatus)_ | ValidationStatus indicates the validation state of the workflow
- Valid: Workflow structure is valid
- Invalid: Workflow has validation errors | | Enum: [Valid Invalid Unknown]
Optional: \{\}
|
| `validationErrors` _string array_ | ValidationErrors contains validation error messages if ValidationStatus is Invalid | | Optional: \{\}
|
| `referencingVirtualServers` _string array_ | ReferencingVirtualServers lists VirtualMCPServer resources that reference this workflow
This helps track which servers need to be reconciled when this workflow changes | | Optional: \{\}
|
diff --git a/test/e2e/chainsaw/operator/multi-tenancy/setup/assert-rbac-clusterrole.yaml b/test/e2e/chainsaw/operator/multi-tenancy/setup/assert-rbac-clusterrole.yaml
index a7a8b3f6e4..488ccef1f3 100644
--- a/test/e2e/chainsaw/operator/multi-tenancy/setup/assert-rbac-clusterrole.yaml
+++ b/test/e2e/chainsaw/operator/multi-tenancy/setup/assert-rbac-clusterrole.yaml
@@ -144,6 +144,7 @@ rules:
- mcptelemetryconfigs/status
- mcptoolconfigs/status
- mcpwebhookconfigs/status
+ - virtualmcpcompositetooldefinitions/status
- virtualmcpservers/status
verbs:
- get
diff --git a/test/e2e/chainsaw/operator/single-tenancy/setup/assert-rbac-clusterrole.yaml b/test/e2e/chainsaw/operator/single-tenancy/setup/assert-rbac-clusterrole.yaml
index a7a8b3f6e4..488ccef1f3 100644
--- a/test/e2e/chainsaw/operator/single-tenancy/setup/assert-rbac-clusterrole.yaml
+++ b/test/e2e/chainsaw/operator/single-tenancy/setup/assert-rbac-clusterrole.yaml
@@ -144,6 +144,7 @@ rules:
- mcptelemetryconfigs/status
- mcptoolconfigs/status
- mcpwebhookconfigs/status
+ - virtualmcpcompositetooldefinitions/status
- virtualmcpservers/status
verbs:
- get