From c63cfc010da204eb7b241f02e39b543c9833b503 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Mon, 17 Nov 2025 07:08:43 -0500 Subject: [PATCH 1/4] refactor: add extension config Signed-off-by: JM Huibonhoa --- cmd/main.go | 221 +------------------ pkg/app/app.go | 307 +++++++++++++++++++++++++++ pkg/cli/internal/commands/deploy.go | 32 ++- pkg/cli/internal/commands/run.go | 10 +- pkg/cli/internal/commands/secrets.go | 2 + pkg/cli/internal/themes/logo.go | 4 +- pkg/cli/root.go | 2 +- 7 files changed, 352 insertions(+), 226 deletions(-) create mode 100644 pkg/app/app.go diff --git a/cmd/main.go b/cmd/main.go index b338bb0..6143d21 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,229 +17,18 @@ limitations under the License. package main import ( - "crypto/tls" - "flag" - "os" - "path/filepath" - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/certwatcher" - "sigs.k8s.io/controller-runtime/pkg/healthz" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/controller-runtime/pkg/metrics/filters" - metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - "sigs.k8s.io/controller-runtime/pkg/webhook" - - kagentdevv1alpha1 "github.com/kagent-dev/kmcp/api/v1alpha1" - "github.com/kagent-dev/kmcp/pkg/controller" + "github.com/kagent-dev/kmcp/pkg/app" // +kubebuilder:scaffold:imports ) -var ( - scheme = runtime.NewScheme() - setupLog = ctrl.Log.WithName("setup") -) - -func init() { - utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - - utilruntime.Must(kagentdevv1alpha1.AddToScheme(scheme)) - // +kubebuilder:scaffold:scheme -} - -// nolint:gocyclo func main() { - var metricsAddr string - var metricsCertPath, metricsCertName, metricsCertKey string - var webhookCertPath, webhookCertName, webhookCertKey string - var enableLeaderElection bool - var probeAddr string - var secureMetrics bool - var enableHTTP2 bool - var tlsOpts []func(*tls.Config) - flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ - "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") - flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") - flag.BoolVar(&enableLeaderElection, "leader-elect", false, - "Enable leader election for controller manager. "+ - "Enabling this will ensure there is only one active controller manager.") - flag.BoolVar(&secureMetrics, "metrics-secure", true, - "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") - flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") - flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") - flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") - flag.StringVar(&metricsCertPath, "metrics-cert-path", "", - "The directory that contains the metrics server certificate.") - flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") - flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") - flag.BoolVar(&enableHTTP2, "enable-http2", false, - "If set, HTTP/2 will be enabled for the metrics and webhook servers") - opts := zap.Options{ - Development: true, - } - opts.BindFlags(flag.CommandLine) - flag.Parse() - - ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - - // if the enable-http2 flag is false (the default), http/2 should be disabled - // due to its vulnerabilities. More specifically, disabling http/2 will - // prevent from being vulnerable to the HTTP/2 Stream Cancellation and - // Rapid Reset CVEs. For more information see: - // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 - // - https://github.com/advisories/GHSA-4374-p667-p6c8 - disableHTTP2 := func(c *tls.Config) { - setupLog.Info("disabling http/2") - c.NextProtos = []string{"http/1.1"} - } - - if !enableHTTP2 { - tlsOpts = append(tlsOpts, disableHTTP2) - } - - // Create watchers for metrics and webhooks certificates - var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher - - // Initial webhook TLS options - webhookTLSOpts := tlsOpts - - if len(webhookCertPath) > 0 { - setupLog.Info("Initializing webhook certificate watcher using provided certificates", - "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) - - var err error - webhookCertWatcher, err = certwatcher.New( - filepath.Join(webhookCertPath, webhookCertName), - filepath.Join(webhookCertPath, webhookCertKey), - ) - if err != nil { - setupLog.Error(err, "Failed to initialize webhook certificate watcher") - os.Exit(1) - } - - webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { - config.GetCertificate = webhookCertWatcher.GetCertificate - }) - } - - webhookServer := webhook.NewServer(webhook.Options{ - TLSOpts: webhookTLSOpts, - }) - - // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. - // More info: - // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/metrics/server - // - https://book.kubebuilder.io/reference/metrics.html - metricsServerOptions := metricsserver.Options{ - BindAddress: metricsAddr, - SecureServing: secureMetrics, - TLSOpts: tlsOpts, - } - - if secureMetrics { - // FilterProvider is used to protect the metrics endpoint with authn/authz. - // These configurations ensure that only authorized users and service accounts - // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: - // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/metrics/filters#WithAuthenticationAndAuthorization - metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization - } - - // If the certificate is not specified, controller-runtime will automatically - // generate self-signed certificates for the metrics server. While convenient for development and testing, - // this setup is not recommended for production. - // - // TODO(user): If you enable certManager, uncomment the following lines: - // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates - // managed by cert-manager for the metrics server. - // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. - if len(metricsCertPath) > 0 { - setupLog.Info("Initializing metrics certificate watcher using provided certificates", - "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) - - var err error - metricsCertWatcher, err = certwatcher.New( - filepath.Join(metricsCertPath, metricsCertName), - filepath.Join(metricsCertPath, metricsCertKey), - ) - if err != nil { - setupLog.Error(err, "to initialize metrics certificate watcher", "error", err) - os.Exit(1) - } - - metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) { - config.GetCertificate = metricsCertWatcher.GetCertificate - }) - } - - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ - Scheme: scheme, - Metrics: metricsServerOptions, - WebhookServer: webhookServer, - HealthProbeBindAddress: probeAddr, - LeaderElection: enableLeaderElection, - LeaderElectionID: "90217b08.kagent.dev", - // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily - // when the Manager ends. This requires the binary to immediately end when the - // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly - // speeds up voluntary leader transitions as the new leader don't have to wait - // LeaseDuration time first. - // - // In the default scaffold provided, the program ends immediately after - // the manager stops, so would be fine to enable this option. However, - // if you are doing or is intended to do any operation such as perform cleanups - // after the manager stops then its usage might be unsafe. - // LeaderElectionReleaseOnCancel: true, + app.Start(func(_ app.BootstrapConfig) (*app.ExtensionConfig, error) { + return &app.ExtensionConfig{ + Plugins: nil, + }, nil }) - if err != nil { - setupLog.Error(err, "unable to start manager") - os.Exit(1) - } - - if err = (&controller.MCPServerReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Plugins: nil, // Plugins are for 3rd party extensions to open source. - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "MCPServer") - os.Exit(1) - } - // +kubebuilder:scaffold:builder - - if metricsCertWatcher != nil { - setupLog.Info("Adding metrics certificate watcher to manager") - if err := mgr.Add(metricsCertWatcher); err != nil { - setupLog.Error(err, "unable to add metrics certificate watcher to manager") - os.Exit(1) - } - } - - if webhookCertWatcher != nil { - setupLog.Info("Adding webhook certificate watcher to manager") - if err := mgr.Add(webhookCertWatcher); err != nil { - setupLog.Error(err, "unable to add webhook certificate watcher to manager") - os.Exit(1) - } - } - - if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { - setupLog.Error(err, "unable to set up health check") - os.Exit(1) - } - if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { - setupLog.Error(err, "unable to set up ready check") - os.Exit(1) - } - - setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { - setupLog.Error(err, "problem running manager") - os.Exit(1) - } } diff --git a/pkg/app/app.go b/pkg/app/app.go new file mode 100644 index 0000000..576317b --- /dev/null +++ b/pkg/app/app.go @@ -0,0 +1,307 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package app + +import ( + "context" + "crypto/tls" + "flag" + "os" + "path/filepath" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/certwatcher" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + kagentdevv1alpha1 "github.com/kagent-dev/kmcp/api/v1alpha1" + "github.com/kagent-dev/kmcp/pkg/controller" + "github.com/kagent-dev/kmcp/pkg/controller/transportadapter" + // +kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(kagentdevv1alpha1.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme + + ctrl.SetLogger(zap.New(zap.UseDevMode(true))) +} + +type Config struct { + Metrics struct { + Addr string + CertPath string + CertName string + CertKey string + } + Webhook struct { + CertPath string + CertName string + CertKey string + } + LeaderElection bool + ProbeAddr string + SecureMetrics bool + EnableHTTP2 bool +} + +func (cfg *Config) SetFlags(commandLine *flag.FlagSet) { + commandLine.StringVar(&cfg.Metrics.Addr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ + "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") + commandLine.StringVar(&cfg.ProbeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + commandLine.BoolVar(&cfg.LeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + commandLine.BoolVar(&cfg.SecureMetrics, "metrics-secure", true, + "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") + commandLine.StringVar(&cfg.Metrics.CertPath, "metrics-cert-path", "", + "The directory that contains the metrics server certificate.") + commandLine.StringVar( + &cfg.Metrics.CertName, + "metrics-cert-name", + "tls.crt", + "The name of the metrics server certificate file.", + ) + commandLine.StringVar(&cfg.Metrics.CertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") + commandLine.StringVar( + &cfg.Webhook.CertPath, + "webhook-cert-path", + "", + "The directory that contains the webhook certificate.", + ) + commandLine.StringVar( + &cfg.Webhook.CertName, + "webhook-cert-name", + "tls.crt", + "The name of the webhook certificate file.", + ) + commandLine.StringVar(&cfg.Webhook.CertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") + commandLine.BoolVar(&cfg.EnableHTTP2, "enable-http2", false, + "If set, HTTP/2 will be enabled for the metrics and webhook servers") +} + +type BootstrapConfig struct { + Ctx context.Context + Manager manager.Manager +} + +type ExtensionConfig struct { + Plugins []transportadapter.TranslatorPlugin +} + +type GetExtensionConfig func(bootstrap BootstrapConfig) (*ExtensionConfig, error) + +// nolint:gocyclo +func Start(getExtensionConfig GetExtensionConfig) { + var cfg Config + var tlsOpts []func(*tls.Config) + + // TODO setup signal handlers + ctx := context.Background() + + cfg.SetFlags(flag.CommandLine) + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + // if the enable-http2 flag is false (the default), http/2 should be disabled + // due to its vulnerabilities. More specifically, disabling http/2 will + // prevent from being vulnerable to the HTTP/2 Stream Cancellation and + // Rapid Reset CVEs. For more information see: + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + if !cfg.EnableHTTP2 { + tlsOpts = append(tlsOpts, disableHTTP2) + } + + // Create watchers for metrics and webhooks certificates + var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher + + // Initial webhook TLS options + webhookTLSOpts := tlsOpts + + if len(cfg.Webhook.CertPath) > 0 { + //nolint:lll + setupLog.Info("Initializing webhook certificate watcher using provided certificates", + "webhook-cert-path", cfg.Webhook.CertPath, "webhook-cert-name", cfg.Webhook.CertName, "webhook-cert-key", cfg.Webhook.CertKey) + + var err error + webhookCertWatcher, err = certwatcher.New( + filepath.Join(cfg.Webhook.CertPath, cfg.Webhook.CertName), + filepath.Join(cfg.Webhook.CertPath, cfg.Webhook.CertKey), + ) + if err != nil { + setupLog.Error(err, "Failed to initialize webhook certificate watcher") + os.Exit(1) + } + + webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { + config.GetCertificate = webhookCertWatcher.GetCertificate + }) + } + + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: webhookTLSOpts, + }) + + // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. + // More info: + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/metrics/server + // - https://book.kubebuilder.io/reference/metrics.html + metricsServerOptions := metricsserver.Options{ + BindAddress: cfg.Metrics.Addr, + SecureServing: cfg.SecureMetrics, + TLSOpts: tlsOpts, + } + + if cfg.SecureMetrics { + // FilterProvider is used to protect the metrics endpoint with authn/authz. + // These configurations ensure that only authorized users and service accounts + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/metrics/filters#WithAuthenticationAndAuthorization + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization + } + + // If the certificate is not specified, controller-runtime will automatically + // generate self-signed certificates for the metrics server. While convenient for development and testing, + // this setup is not recommended for production. + // + // TODO(user): If you enable certManager, uncomment the following lines: + // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates + // managed by cert-manager for the metrics server. + // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. + if len(cfg.Metrics.CertPath) > 0 { + //nolint:lll + setupLog.Info("Initializing metrics certificate watcher using provided certificates", + "metrics-cert-path", cfg.Metrics.CertPath, "metrics-cert-name", cfg.Metrics.CertName, "metrics-cert-key", cfg.Metrics.CertKey) + + var err error + metricsCertWatcher, err = certwatcher.New( + filepath.Join(cfg.Metrics.CertPath, cfg.Metrics.CertName), + filepath.Join(cfg.Metrics.CertPath, cfg.Metrics.CertKey), + ) + if err != nil { + setupLog.Error(err, "to initialize metrics certificate watcher", "error", err) + os.Exit(1) + } + + metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) { + config.GetCertificate = metricsCertWatcher.GetCertificate + }) + } + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsServerOptions, + WebhookServer: webhookServer, + HealthProbeBindAddress: cfg.ProbeAddr, + LeaderElection: cfg.LeaderElection, + LeaderElectionID: "90217b08.kagent.dev", + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + extensionCfg, err := getExtensionConfig(BootstrapConfig{ + Ctx: ctx, + Manager: mgr, + }) + if err != nil { + setupLog.Error(err, "unable to get extension config") + os.Exit(1) + } + + if err = (&controller.MCPServerReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Plugins: extensionCfg.Plugins, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "MCPServer") + os.Exit(1) + } + // +kubebuilder:scaffold:builder + + if metricsCertWatcher != nil { + setupLog.Info("Adding metrics certificate watcher to manager") + if err := mgr.Add(metricsCertWatcher); err != nil { + setupLog.Error(err, "unable to add metrics certificate watcher to manager") + os.Exit(1) + } + } + + if webhookCertWatcher != nil { + setupLog.Info("Adding webhook certificate watcher to manager") + if err := mgr.Add(webhookCertWatcher); err != nil { + setupLog.Error(err, "unable to add webhook certificate watcher to manager") + os.Exit(1) + } + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/pkg/cli/internal/commands/deploy.go b/pkg/cli/internal/commands/deploy.go index 3beeeca..bdc1673 100644 --- a/pkg/cli/internal/commands/deploy.go +++ b/pkg/cli/internal/commands/deploy.go @@ -112,6 +112,8 @@ func init() { } // package subcommand - supports both npm and uvx +// +//nolint:lll // Long example lines in command description var packageDeployCmd = &cobra.Command{ Use: "package", Short: "Deploy an MCP server using a package manager (npx, uvx)", @@ -137,17 +139,32 @@ func init() { // package subcommand flags packageDeployCmd.Flags().StringVar(&packageName, "deployment-name", "", "Name for the deployment (required)") packageDeployCmd.Flags().StringVar(&packageManager, "manager", "", "Package manager to use (npx or uvx) (required)") - packageDeployCmd.Flags().StringSliceVar(&packageSecrets, "secrets", []string{}, "List of Kubernetes secret names to mount") + packageDeployCmd.Flags().StringSliceVar( + &packageSecrets, + "secrets", + []string{}, + "List of Kubernetes secret names to mount", + ) // Add common deployment flags - packageDeployCmd.Flags().StringSliceVar(&deployArgs, "args", []string{}, "Arguments to pass to the package manager (e.g., package names) (required)") + packageDeployCmd.Flags().StringSliceVar( + &deployArgs, + "args", + []string{}, + "Arguments to pass to the package manager (e.g., package names) (required)", + ) packageDeployCmd.Flags().StringSliceVar(&deployEnv, "env", []string{}, "Environment variables (KEY=VALUE)") packageDeployCmd.Flags().BoolVar(&deployDryRun, "dry-run", false, "Generate manifest without applying to cluster") packageDeployCmd.Flags().StringVarP(&deployNamespace, "namespace", "n", "", "Kubernetes namespace") packageDeployCmd.Flags().StringVar(&deployImage, "image", "", "Docker image to deploy (overrides default)") packageDeployCmd.Flags().StringVar(&deployTransport, "transport", "", "Transport type (stdio, http)") packageDeployCmd.Flags().IntVar(&deployPort, "port", 0, "Container port (default: 3000)") - packageDeployCmd.Flags().BoolVar(&deployNoInspector, "no-inspector", false, "Do not start the MCP inspector after deployment") + packageDeployCmd.Flags().BoolVar( + &deployNoInspector, + "no-inspector", + false, + "Do not start the MCP inspector after deployment", + ) packageDeployCmd.Flags().StringVarP(&deployOutput, "output", "o", "", "Output file for the generated YAML") // Mark required flags @@ -156,7 +173,7 @@ func init() { _ = packageDeployCmd.MarkFlagRequired("args") } -func runPackageDeploy(_ *cobra.Command, args []string) error { +func runPackageDeploy(_ *cobra.Command, _ []string) error { // Validate package manager if packageManager != "npx" && packageManager != "uvx" { return fmt.Errorf("unsupported package manager: %s (must be 'npx' or 'uvx')", packageManager) @@ -530,7 +547,12 @@ func getDefaultArgs(framework string, targetPort int) []string { return []string{"dist/index.js"} case manifest.FrameworkJava: if deployTransport == transportHTTP { - return []string{"-jar", "app.jar", "--transport", "http", "--host", "0.0.0.0", "--port", fmt.Sprintf("%d", targetPort)} + return []string{ + "-jar", "app.jar", + "--transport", "http", + "--host", "0.0.0.0", + "--port", fmt.Sprintf("%d", targetPort), + } } return []string{"-jar", "app.jar"} default: diff --git a/pkg/cli/internal/commands/run.go b/pkg/cli/internal/commands/run.go index 0dcf251..fd83b18 100644 --- a/pkg/cli/internal/commands/run.go +++ b/pkg/cli/internal/commands/run.go @@ -244,7 +244,10 @@ func runTypeScript(projectDir string, manifest *manifest.ProjectManifest) error // Check if npm is available if _, err := exec.LookPath("npm"); err != nil { npmInstallURL := "https://docs.npmjs.com/downloading-and-installing-node-js-and-npm" - return fmt.Errorf("npm is required to run TypeScript projects locally. Please install Node.js and npm: %s", npmInstallURL) + return fmt.Errorf( + "npm is required to run TypeScript projects locally. Please install Node.js and npm: %s", + npmInstallURL, + ) } // Install dependencies first @@ -317,7 +320,10 @@ func runJava(projectDir string, manifest *manifest.ProjectManifest) error { if noInspector { // Run the server directly if runTransport == transportHTTP { - fmt.Printf("Running server directly: mvn exec:java -Dexec.mainClass=\"com.example.Main\" --transport http --host 0.0.0.0 --port 3000\n") + fmt.Printf( + "Running server directly: mvn exec:java -Dexec.mainClass=\"com.example.Main\" " + + "--transport http --host 0.0.0.0 --port 3000\n", + ) fmt.Printf("Server is running on http://localhost:3000\n") fmt.Printf("Health check: http://localhost:3000/health\n") fmt.Printf("MCP endpoint: http://localhost:3000/mcp\n") diff --git a/pkg/cli/internal/commands/secrets.go b/pkg/cli/internal/commands/secrets.go index 6ef21c8..f000a92 100644 --- a/pkg/cli/internal/commands/secrets.go +++ b/pkg/cli/internal/commands/secrets.go @@ -30,6 +30,8 @@ var ( ) // syncCmd creates or updates a Kubernetes secret from an environment file +// +//nolint:lll // Long example lines in command description var syncCmd = &cobra.Command{ Use: "sync [environment]", Short: "Sync secrets to a Kubernetes environment from a local .env file", diff --git a/pkg/cli/internal/themes/logo.go b/pkg/cli/internal/themes/logo.go index 757fd3a..6f4fffb 100644 --- a/pkg/cli/internal/themes/logo.go +++ b/pkg/cli/internal/themes/logo.go @@ -7,10 +7,10 @@ import ( ) //go:embed kmcp-ascii.txt -var kmcpLogoAscii string +var kmcpLogoASCII string func KmcpLogo() string { - return kmcpLogoAscii + return kmcpLogoASCII } func ColoredKmcpLogo() string { diff --git a/pkg/cli/root.go b/pkg/cli/root.go index fa2a762..8684586 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -27,7 +27,7 @@ KMCP is a CLI tool for building and managing Model Context Protocol (MCP) server It provides a unified development experience for creating, building, and deploying MCP servers locally and to Kubernetes clusters.`, themes.ColoredKmcpLogo()), Version: version, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, } From 7d73c37d0044325324f0a9ec219f21adde8cd693 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Mon, 17 Nov 2025 07:49:17 -0500 Subject: [PATCH 2/4] refactor: simplify extension config and add scheme registration Signed-off-by: JM Huibonhoa --- cmd/main.go | 5 +++-- pkg/app/app.go | 41 +++++++++++++++++++++-------------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 6143d21..0fd2a92 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -26,9 +26,10 @@ import ( ) func main() { - app.Start(func(_ app.BootstrapConfig) (*app.ExtensionConfig, error) { + app.Start(func() (*app.ExtensionConfig, error) { return &app.ExtensionConfig{ - Plugins: nil, + Plugins: nil, + RegisterSchemes: nil, }, nil }) } diff --git a/pkg/app/app.go b/pkg/app/app.go index 576317b..52e3dc6 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -17,7 +17,6 @@ limitations under the License. package app import ( - "context" "crypto/tls" "flag" "os" @@ -34,7 +33,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/certwatcher" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -112,25 +110,21 @@ func (cfg *Config) SetFlags(commandLine *flag.FlagSet) { "If set, HTTP/2 will be enabled for the metrics and webhook servers") } -type BootstrapConfig struct { - Ctx context.Context - Manager manager.Manager -} - type ExtensionConfig struct { + // Plugins are translator plugins for extending MCPServer translation behavior Plugins []transportadapter.TranslatorPlugin + // RegisterSchemes is an optional function to register additional API types to the runtime scheme. + // This is called before the manager is created, allowing extensions to add their own CRDs. + RegisterSchemes func(*runtime.Scheme) error } -type GetExtensionConfig func(bootstrap BootstrapConfig) (*ExtensionConfig, error) +type GetExtensionConfig func() (*ExtensionConfig, error) // nolint:gocyclo func Start(getExtensionConfig GetExtensionConfig) { var cfg Config var tlsOpts []func(*tls.Config) - // TODO setup signal handlers - ctx := context.Background() - cfg.SetFlags(flag.CommandLine) opts := zap.Options{ Development: true, @@ -231,6 +225,22 @@ func Start(getExtensionConfig GetExtensionConfig) { }) } + // Get extension config before creating the manager + // Schemes must be registered before the manager is created + extensionCfg, err := getExtensionConfig() + if err != nil { + setupLog.Error(err, "unable to get extension config") + os.Exit(1) + } + + // Register extension schemes if provided + if extensionCfg.RegisterSchemes != nil { + if err := extensionCfg.RegisterSchemes(scheme); err != nil { + setupLog.Error(err, "unable to register extension schemes") + os.Exit(1) + } + } + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, @@ -255,15 +265,6 @@ func Start(getExtensionConfig GetExtensionConfig) { os.Exit(1) } - extensionCfg, err := getExtensionConfig(BootstrapConfig{ - Ctx: ctx, - Manager: mgr, - }) - if err != nil { - setupLog.Error(err, "unable to get extension config") - os.Exit(1) - } - if err = (&controller.MCPServerReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), From 71ba148453df9047d86d8d67cda6b2b7e2ff5697 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Mon, 17 Nov 2025 08:11:39 -0500 Subject: [PATCH 3/4] refactor: use factory pattern for plugin creation to use created scheme and client Signed-off-by: JM Huibonhoa --- cmd/main.go | 2 +- pkg/app/app.go | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 0fd2a92..b216a6c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,7 +28,7 @@ import ( func main() { app.Start(func() (*app.ExtensionConfig, error) { return &app.ExtensionConfig{ - Plugins: nil, + PluginFactories: nil, RegisterSchemes: nil, }, nil }) diff --git a/pkg/app/app.go b/pkg/app/app.go index 52e3dc6..b08b336 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -31,6 +31,7 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/certwatcher" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" @@ -110,9 +111,15 @@ func (cfg *Config) SetFlags(commandLine *flag.FlagSet) { "If set, HTTP/2 will be enabled for the metrics and webhook servers") } +// PluginFactory creates a TranslatorPlugin when provided with the client and scheme. +// This allows plugins to be initialized after the manager is created, giving them access to +// the Kubernetes client and scheme. Plugins should create their own logger. +type PluginFactory func(client.Client, *runtime.Scheme) transportadapter.TranslatorPlugin + type ExtensionConfig struct { - // Plugins are translator plugins for extending MCPServer translation behavior - Plugins []transportadapter.TranslatorPlugin + // PluginFactories are factories that create translator plugins for extending MCPServer translation behavior. + // These factories are called after the manager is created, allowing plugins to access the client and scheme. + PluginFactories []PluginFactory // RegisterSchemes is an optional function to register additional API types to the runtime scheme. // This is called before the manager is created, allowing extensions to add their own CRDs. RegisterSchemes func(*runtime.Scheme) error @@ -265,10 +272,16 @@ func Start(getExtensionConfig GetExtensionConfig) { os.Exit(1) } + var plugins []transportadapter.TranslatorPlugin + for _, factory := range extensionCfg.PluginFactories { + plugin := factory(mgr.GetClient(), mgr.GetScheme()) + plugins = append(plugins, plugin) + } + if err = (&controller.MCPServerReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - Plugins: extensionCfg.Plugins, + Plugins: plugins, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "MCPServer") os.Exit(1) From 8c8092730367e504ec74e27672292ce2f4ac99b0 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Mon, 17 Nov 2025 08:17:57 -0500 Subject: [PATCH 4/4] style: lint fix Signed-off-by: JM Huibonhoa --- pkg/app/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index b08b336..015a7d5 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -272,7 +272,7 @@ func Start(getExtensionConfig GetExtensionConfig) { os.Exit(1) } - var plugins []transportadapter.TranslatorPlugin + plugins := make([]transportadapter.TranslatorPlugin, 0, len(extensionCfg.PluginFactories)) for _, factory := range extensionCfg.PluginFactories { plugin := factory(mgr.GetClient(), mgr.GetScheme()) plugins = append(plugins, plugin)