From a4ee01770b932c5b30e9002ae8424e4cde619d66 Mon Sep 17 00:00:00 2001 From: mlavacca Date: Fri, 8 May 2026 13:55:41 +0200 Subject: [PATCH 1/3] feat(mcpserver): provision ai-mcp-proxy plugin Signed-off-by: Mattia Lavacca --- controller/mcpserver/controller_rbac.go | 2 + controller/mcpserver/mcpserver_controller.go | 3 + controller/mcpserver/owned_kong_entities.go | 24 +++ controller/mcpserver/owned_plugins.go | 194 +++++++++++++++++++ 4 files changed, 223 insertions(+) create mode 100644 controller/mcpserver/owned_plugins.go diff --git a/controller/mcpserver/controller_rbac.go b/controller/mcpserver/controller_rbac.go index 6ac213bb62..62ace23341 100644 --- a/controller/mcpserver/controller_rbac.go +++ b/controller/mcpserver/controller_rbac.go @@ -12,3 +12,5 @@ package mcpserver // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get // +kubebuilder:rbac:groups=configuration.konghq.com,resources=kongservices,verbs=create;get;list;watch;update;patch;delete // +kubebuilder:rbac:groups=configuration.konghq.com,resources=kongroutes,verbs=create;get;list;watch;update;patch;delete +// +kubebuilder:rbac:groups=configuration.konghq.com,resources=kongplugins,verbs=create;get;list;watch;update;patch;delete +// +kubebuilder:rbac:groups=configuration.konghq.com,resources=kongpluginbindings,verbs=create;get;list;watch;update;patch;delete diff --git a/controller/mcpserver/mcpserver_controller.go b/controller/mcpserver/mcpserver_controller.go index 5202c87ffd..3d7a70e617 100644 --- a/controller/mcpserver/mcpserver_controller.go +++ b/controller/mcpserver/mcpserver_controller.go @@ -17,6 +17,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/source" + configurationv1 "github.com/kong/kong-operator/v2/api/configuration/v1" configurationv1alpha1 "github.com/kong/kong-operator/v2/api/configuration/v1alpha1" konnectv1alpha1 "github.com/kong/kong-operator/v2/api/konnect/v1alpha1" sdkops "github.com/kong/kong-operator/v2/controller/konnect/ops/sdk" @@ -74,6 +75,8 @@ func (r *MCPServerReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man Owns(&corev1.Service{}). Owns(&configurationv1alpha1.KongService{}). Owns(&configurationv1alpha1.KongRoute{}). + Owns(&configurationv1.KongPlugin{}). + Owns(&configurationv1alpha1.KongPluginBinding{}). WatchesRawSource( source.Channel( r.ReconcileEventCh, diff --git a/controller/mcpserver/owned_kong_entities.go b/controller/mcpserver/owned_kong_entities.go index 6c70ee427c..f5c7305c95 100644 --- a/controller/mcpserver/owned_kong_entities.go +++ b/controller/mcpserver/owned_kong_entities.go @@ -11,6 +11,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" commonv1alpha1 "github.com/kong/kong-operator/v2/api/common/v1alpha1" + configurationv1 "github.com/kong/kong-operator/v2/api/configuration/v1" configurationv1alpha1 "github.com/kong/kong-operator/v2/api/configuration/v1alpha1" konnectv1alpha1 "github.com/kong/kong-operator/v2/api/konnect/v1alpha1" sdkops "github.com/kong/kong-operator/v2/controller/konnect/ops/sdk" @@ -99,6 +100,17 @@ func (r *MCPServerReconciler) ensureKongEntities( return err } + // ------------------------------------------------------------------ + // Ensure KongPlugin and KongPluginBinding CRs + // ------------------------------------------------------------------ + if _, err := r.ensureKongPlugins(ctx, mcpServer); err != nil { + return err + } + + if err := r.ensureKongPluginBindings(ctx, mcpServer, desiredServiceNames); err != nil { + return err + } + return nil } @@ -284,6 +296,18 @@ func extractItems(list client.ObjectList) []client.Object { items[i] = &l.Items[i] } return items + case *configurationv1.KongPluginList: + items := make([]client.Object, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items + case *configurationv1alpha1.KongPluginBindingList: + items := make([]client.Object, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items } return nil } diff --git a/controller/mcpserver/owned_plugins.go b/controller/mcpserver/owned_plugins.go new file mode 100644 index 0000000000..8f6de3c171 --- /dev/null +++ b/controller/mcpserver/owned_plugins.go @@ -0,0 +1,194 @@ +package mcpserver + +import ( + "context" + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + configurationv1 "github.com/kong/kong-operator/v2/api/configuration/v1" + configurationv1alpha1 "github.com/kong/kong-operator/v2/api/configuration/v1alpha1" + konnectv1alpha1 "github.com/kong/kong-operator/v2/api/konnect/v1alpha1" + "github.com/kong/kong-operator/v2/controller/pkg/log" + "github.com/kong/kong-operator/v2/controller/pkg/op" + k8sutils "github.com/kong/kong-operator/v2/pkg/utils/kubernetes" + k8sresources "github.com/kong/kong-operator/v2/pkg/utils/kubernetes/resources" +) + +// builtinPlugin describes a Kong plugin that is automatically provisioned for +// every MCPServer deployment. +type builtinPlugin struct { + name string + config string +} + +var builtinPlugins = []builtinPlugin{ + { + name: "ai-mcp-proxy", + config: `{"mode":"passthrough-listener"}`, + }, +} + +// kongPluginName returns the deterministic name for the KongPlugin CR +// corresponding to the given builtin plugin and MCPServer. +func kongPluginName(mcpServer *konnectv1alpha1.MCPServer, plg builtinPlugin) string { + return fmt.Sprintf("%s-%s", generateWorkloadNN(mcpServer).Name, plg.name) +} + +// ensureKongPlugins creates KongPlugin CRs for every builtinPlugin and deletes +// any stale KongPlugins owned by the MCPServer that are no longer expected. +// It returns the set of plugin names that were ensured. +func (r *MCPServerReconciler) ensureKongPlugins( + ctx context.Context, + mcpServer *konnectv1alpha1.MCPServer, +) (map[string]struct{}, error) { + logger := log.GetLogger(ctx, "mcpserver", r.LoggingMode) + + desiredPluginNames := make(map[string]struct{}, len(builtinPlugins)) + for _, plg := range builtinPlugins { + res, nn, err := r.ensureKongPlugin(ctx, mcpServer, plg) + if err != nil { + return nil, err + } + if res != op.Noop { + log.Info(logger, fmt.Sprintf("%s KongPlugin for MCPServer", res), + "namespace", mcpServer.Namespace, "name", mcpServer.Name, "plugin", nn.Name) + } + desiredPluginNames[nn.Name] = struct{}{} + } + + if err := r.deleteStaleResources(ctx, mcpServer, &configurationv1.KongPluginList{}, desiredPluginNames); err != nil { + return nil, err + } + + return desiredPluginNames, nil +} + +func (r *MCPServerReconciler) ensureKongPlugin( + ctx context.Context, + mcpServer *konnectv1alpha1.MCPServer, + plg builtinPlugin, +) (op.Result, client.ObjectKey, error) { + desired := generateKongPlugin(mcpServer, plg) + nn := client.ObjectKeyFromObject(desired) + + k8sutils.SetOwnerForObject(desired, mcpServer) + k8sresources.LabelObjectAsMCPServerManaged(desired) + + existing := &configurationv1.KongPlugin{} + err := r.Get(ctx, nn, existing) + if err != nil { + if !apierrors.IsNotFound(err) { + return op.Noop, nn, fmt.Errorf("failed to get KongPlugin %s: %w", nn, err) + } + + if err := r.Create(ctx, desired); err != nil { + return op.Noop, nn, fmt.Errorf("failed to create KongPlugin %s: %w", nn, err) + } + return op.Created, nn, nil + } + + // TODO: enforce the KongPlugin Spec + + return op.Noop, nn, nil +} + +func generateKongPlugin(mcpServer *konnectv1alpha1.MCPServer, plg builtinPlugin) *configurationv1.KongPlugin { + return &configurationv1.KongPlugin{ + ObjectMeta: metav1.ObjectMeta{ + Name: kongPluginName(mcpServer, plg), + Namespace: mcpServer.Namespace, + }, + PluginName: plg.name, + Config: apiextensionsv1.JSON{ + Raw: []byte(plg.config), + }, + } +} + +// ensureKongPluginBindings creates a KongPluginBinding CR for every +// (KongService, KongPlugin) pair, binding the plugin to the service. Stale +// bindings owned by the MCPServer are deleted. +func (r *MCPServerReconciler) ensureKongPluginBindings( + ctx context.Context, + mcpServer *konnectv1alpha1.MCPServer, + serviceNames map[string]struct{}, +) error { + logger := log.GetLogger(ctx, "mcpserver", r.LoggingMode) + + desiredBindingNames := make(map[string]struct{}, len(builtinPlugins)*len(serviceNames)) + for _, plg := range builtinPlugins { + pluginName := kongPluginName(mcpServer, plg) + for svcName := range serviceNames { + res, nn, err := r.ensureKongPluginBinding(ctx, mcpServer, pluginName, svcName) + if err != nil { + return err + } + if res != op.Noop { + log.Info(logger, fmt.Sprintf("%s KongPluginBinding for MCPServer", res), + "namespace", mcpServer.Namespace, "name", mcpServer.Name, + "binding", nn.Name, "plugin", pluginName, "service", svcName) + } + desiredBindingNames[nn.Name] = struct{}{} + } + } + + return r.deleteStaleResources(ctx, mcpServer, &configurationv1alpha1.KongPluginBindingList{}, desiredBindingNames) +} + +func (r *MCPServerReconciler) ensureKongPluginBinding( + ctx context.Context, + mcpServer *konnectv1alpha1.MCPServer, + pluginName, serviceName string, +) (op.Result, client.ObjectKey, error) { + desired := generateKongPluginBinding(mcpServer, pluginName, serviceName) + nn := client.ObjectKeyFromObject(desired) + + k8sutils.SetOwnerForObject(desired, mcpServer) + k8sresources.LabelObjectAsMCPServerManaged(desired) + + existing := &configurationv1alpha1.KongPluginBinding{} + err := r.Get(ctx, nn, existing) + if err != nil { + if !apierrors.IsNotFound(err) { + return op.Noop, nn, fmt.Errorf("failed to get KongPluginBinding %s: %w", nn, err) + } + + if err := r.Create(ctx, desired); err != nil { + return op.Noop, nn, fmt.Errorf("failed to create KongPluginBinding %s: %w", nn, err) + } + return op.Created, nn, nil + } + + // TODO: enforce the KongPluginBinding Spec + + return op.Noop, nn, nil +} + +func generateKongPluginBinding( + mcpServer *konnectv1alpha1.MCPServer, + pluginName, serviceName string, +) *configurationv1alpha1.KongPluginBinding { + return &configurationv1alpha1.KongPluginBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: pluginName, + Namespace: mcpServer.Namespace, + }, + Spec: configurationv1alpha1.KongPluginBindingSpec{ + PluginReference: configurationv1alpha1.PluginRef{ + Name: pluginName, + }, + Targets: &configurationv1alpha1.KongPluginBindingTargets{ + ServiceReference: &configurationv1alpha1.TargetRefWithGroupKind{ + Name: serviceName, + Group: configurationv1.GroupVersion.Group, + Kind: "KongService", + }, + }, + ControlPlaneRef: mcpServer.Spec.ControlPlaneRef, + }, + } +} From dc1e28fee53e7d619be207edfef0a7a4d5389437 Mon Sep 17 00:00:00 2001 From: mlavacca Date: Fri, 8 May 2026 14:06:27 +0200 Subject: [PATCH 2/3] chore: add CHANGELOG entry Signed-off-by: Mattia Lavacca --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13c88ca374..f118f1ddca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -185,6 +185,9 @@ runner container images (`kong/mcp-server-init`, `kong/mcp-server-runner`) to the versions returned by Konnect. [#3943](https://github.com/Kong/kong-operator/pull/3943) +- `MCPServer`: provision the `ai-mcp-proxy` `KongPlugin` and attach it to the + owned `KongService` via a dedicated `KongPluginBinding`. + [#4188](https://github.com/Kong/kong-operator/pull/4188) ### Changed From 74b01fd8733e50c970f166156c07fcf9ab9ee517 Mon Sep 17 00:00:00 2001 From: mlavacca Date: Fri, 8 May 2026 16:06:36 +0200 Subject: [PATCH 3/3] chore: add issue link to TODO Signed-off-by: Mattia Lavacca --- controller/mcpserver/owned_kong_entities.go | 4 ++-- controller/mcpserver/owned_plugins.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/controller/mcpserver/owned_kong_entities.go b/controller/mcpserver/owned_kong_entities.go index f5c7305c95..c5ec8764cd 100644 --- a/controller/mcpserver/owned_kong_entities.go +++ b/controller/mcpserver/owned_kong_entities.go @@ -142,7 +142,7 @@ func (r *MCPServerReconciler) ensureKongService( return op.Created, nn, nil } - // TODO: enforce the KongService Spec + // TODO: enforce the KongService Spec https://github.com/Kong/kong-operator/issues/3979 return op.Noop, nn, nil } @@ -203,7 +203,7 @@ func (r *MCPServerReconciler) ensureKongRoute( return op.Created, nn, nil } - // TODO: enforce the KongRoute Spec + // TODO: enforce the KongRoute Spec https://github.com/Kong/kong-operator/issues/3979 return op.Noop, nn, nil } diff --git a/controller/mcpserver/owned_plugins.go b/controller/mcpserver/owned_plugins.go index 8f6de3c171..b061a45a93 100644 --- a/controller/mcpserver/owned_plugins.go +++ b/controller/mcpserver/owned_plugins.go @@ -91,7 +91,7 @@ func (r *MCPServerReconciler) ensureKongPlugin( return op.Created, nn, nil } - // TODO: enforce the KongPlugin Spec + // TODO: enforce the KongPlugin Spec https://github.com/Kong/kong-operator/issues/3979 return op.Noop, nn, nil } @@ -163,7 +163,7 @@ func (r *MCPServerReconciler) ensureKongPluginBinding( return op.Created, nn, nil } - // TODO: enforce the KongPluginBinding Spec + // TODO: enforce the KongPluginBinding Spec https://github.com/Kong/kong-operator/issues/3979 return op.Noop, nn, nil }