Skip to content

Commit b4928f8

Browse files
committed
refactor(helm): adapt Helm contribution to project structure
1 parent 34eabde commit b4928f8

File tree

12 files changed

+232
-203
lines changed

12 files changed

+232
-203
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ A powerful and flexible Kubernetes [Model Context Protocol (MCP)](https://blog.m
2828
- **✅ Namespaces**: List Kubernetes Namespaces.
2929
- **✅ Events**: View Kubernetes events in all namespaces or in a specific namespace.
3030
- **✅ Projects**: List OpenShift Projects.
31+
- **☸️ Helm**:
32+
- **List** Helm releases in all namespaces or in a specific namespace.
3133

3234
Unlike other Kubernetes MCP server implementations, this **IS NOT** just a wrapper around `kubectl` or `helm` command-line tools.
3335

go.mod

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ go 1.24.1
44

55
require (
66
github.com/fsnotify/fsnotify v1.9.0
7-
github.com/mark3labs/mcp-go v0.21.1
7+
github.com/mark3labs/mcp-go v0.26.0
88
github.com/spf13/afero v1.14.0
99
github.com/spf13/cobra v1.9.1
1010
github.com/spf13/viper v1.20.1
11-
golang.org/x/net v0.39.0
12-
gopkg.in/yaml.v3 v3.0.1
11+
golang.org/x/net v0.40.0
1312
helm.sh/helm/v3 v3.17.3
14-
k8s.io/api v0.32.3
15-
k8s.io/apiextensions-apiserver v0.32.3
16-
k8s.io/apimachinery v0.32.3
17-
k8s.io/client-go v0.32.3
13+
k8s.io/api v0.33.0
14+
k8s.io/apiextensions-apiserver v0.33.0
15+
k8s.io/apimachinery v0.33.0
16+
k8s.io/cli-runtime v0.32.2
17+
k8s.io/client-go v0.33.0
1818
k8s.io/klog/v2 v2.130.1
1919
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
2020
sigs.k8s.io/controller-runtime v0.20.4
@@ -67,15 +67,13 @@ require (
6767
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
6868
github.com/gobwas/glob v0.2.3 // indirect
6969
github.com/gogo/protobuf v1.3.2 // indirect
70-
github.com/golang/protobuf v1.5.4 // indirect
7170
github.com/google/btree v1.1.3 // indirect
72-
github.com/google/gnostic-models v0.6.8 // indirect
73-
github.com/google/go-cmp v0.6.0 // indirect
74-
github.com/google/gofuzz v1.2.0 // indirect
71+
github.com/google/gnostic-models v0.6.9 // indirect
72+
github.com/google/go-cmp v0.7.0 // indirect
7573
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
7674
github.com/google/uuid v1.6.0 // indirect
7775
github.com/gorilla/mux v1.8.0 // indirect
78-
github.com/gorilla/websocket v1.5.0 // indirect
76+
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
7977
github.com/gosuri/uitable v0.0.4 // indirect
8078
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
8179
github.com/hashicorp/errwrap v1.1.0 // indirect
@@ -85,7 +83,7 @@ require (
8583
github.com/jmoiron/sqlx v1.4.0 // indirect
8684
github.com/josharian/intern v1.0.0 // indirect
8785
github.com/json-iterator/go v1.1.12 // indirect
88-
github.com/klauspost/compress v1.16.7 // indirect
86+
github.com/klauspost/compress v1.18.0 // indirect
8987
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
9088
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
9189
github.com/lib/pq v1.10.9 // indirect
@@ -110,9 +108,9 @@ require (
110108
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
111109
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
112110
github.com/pkg/errors v0.9.1 // indirect
113-
github.com/prometheus/client_golang v1.19.1 // indirect
111+
github.com/prometheus/client_golang v1.22.0 // indirect
114112
github.com/prometheus/client_model v0.6.1 // indirect
115-
github.com/prometheus/common v0.55.0 // indirect
113+
github.com/prometheus/common v0.62.0 // indirect
116114
github.com/prometheus/procfs v0.15.1 // indirect
117115
github.com/rubenv/sql-migrate v1.7.1 // indirect
118116
github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -129,31 +127,33 @@ require (
129127
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
130128
github.com/xlab/treeprint v1.2.0 // indirect
131129
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
132-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
133-
go.opentelemetry.io/otel v1.28.0 // indirect
134-
go.opentelemetry.io/otel/metric v1.28.0 // indirect
135-
go.opentelemetry.io/otel/trace v1.28.0 // indirect
130+
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
131+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
132+
go.opentelemetry.io/otel v1.33.0 // indirect
133+
go.opentelemetry.io/otel/metric v1.33.0 // indirect
134+
go.opentelemetry.io/otel/trace v1.33.0 // indirect
136135
go.uber.org/multierr v1.11.0 // indirect
137-
golang.org/x/crypto v0.37.0 // indirect
138-
golang.org/x/oauth2 v0.25.0 // indirect
139-
golang.org/x/sync v0.13.0 // indirect
140-
golang.org/x/sys v0.32.0 // indirect
141-
golang.org/x/term v0.31.0 // indirect
142-
golang.org/x/text v0.24.0 // indirect
143-
golang.org/x/time v0.8.0 // indirect
144-
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
145-
google.golang.org/grpc v1.65.0 // indirect
146-
google.golang.org/protobuf v1.36.1 // indirect
136+
golang.org/x/crypto v0.38.0 // indirect
137+
golang.org/x/oauth2 v0.27.0 // indirect
138+
golang.org/x/sync v0.14.0 // indirect
139+
golang.org/x/sys v0.33.0 // indirect
140+
golang.org/x/term v0.32.0 // indirect
141+
golang.org/x/text v0.25.0 // indirect
142+
golang.org/x/time v0.9.0 // indirect
143+
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
144+
google.golang.org/grpc v1.68.1 // indirect
145+
google.golang.org/protobuf v1.36.5 // indirect
147146
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
148147
gopkg.in/inf.v0 v0.9.1 // indirect
149-
k8s.io/apiserver v0.32.3 // indirect
150-
k8s.io/cli-runtime v0.32.2 // indirect
151-
k8s.io/component-base v0.32.3 // indirect
152-
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
148+
gopkg.in/yaml.v3 v3.0.1 // indirect
149+
k8s.io/apiserver v0.33.0 // indirect
150+
k8s.io/component-base v0.33.0 // indirect
151+
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
153152
k8s.io/kubectl v0.32.2 // indirect
154153
oras.land/oras-go v1.2.5 // indirect
155154
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
156155
sigs.k8s.io/kustomize/api v0.18.0 // indirect
157156
sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect
158-
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
157+
sigs.k8s.io/randfill v1.0.0 // indirect
158+
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
159159
)

pkg/helm/helm.go

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,52 @@
11
package helm
22

33
import (
4-
"context"
5-
"log"
6-
74
"helm.sh/helm/v3/pkg/action"
85
"helm.sh/helm/v3/pkg/cli"
9-
"helm.sh/helm/v3/pkg/release"
6+
"k8s.io/cli-runtime/pkg/genericclioptions"
7+
"log"
8+
"sigs.k8s.io/yaml"
109
)
1110

12-
// Helm provides methods to interact with Helm releases
13-
// Mirrors the abstraction style of pkg/kubernetes
11+
type Kubernetes interface {
12+
genericclioptions.RESTClientGetter
13+
NamespaceOrDefault(namespace string) string
14+
}
1415

1516
type Helm struct {
16-
settings *cli.EnvSettings
17+
kubernetes Kubernetes
1718
}
1819

19-
// NewHelm creates a new Helm instance using kubeconfig, context, and namespace settings
20-
func NewHelm(kubeconfig, kubeContext, namespace string) *Helm {
20+
// NewHelm creates a new Helm instance
21+
func NewHelm(kubernetes Kubernetes, namespace string) *Helm {
2122
settings := cli.New()
22-
if kubeconfig != "" {
23-
settings.KubeConfig = kubeconfig
24-
}
25-
if kubeContext != "" {
26-
settings.KubeContext = kubeContext
27-
}
2823
if namespace != "" {
2924
settings.SetNamespace(namespace)
3025
}
31-
return &Helm{settings: settings}
26+
return &Helm{kubernetes: kubernetes}
3227
}
3328

34-
// ReleasesList lists Helm releases in a specific namespace (or all namespaces if namespace is empty)
35-
func (h *Helm) ReleasesList(ctx context.Context, namespace string) ([]*release.Release, error) {
36-
// If no namespace is given, use the default from kubeconfig
37-
if namespace == "" {
38-
namespace = h.settings.Namespace()
39-
}
29+
// ReleasesList lists all the releases for the specified namespace (or current namespace if). Or allNamespaces is true, it lists all releases across all namespaces.
30+
func (h *Helm) ReleasesList(namespace string, allNamespaces bool) (string, error) {
4031
cfg := new(action.Configuration)
41-
if err := cfg.Init(h.settings.RESTClientGetter(), namespace, "", log.Printf); err != nil {
42-
return nil, err
32+
applicableNamespace := ""
33+
if !allNamespaces {
34+
applicableNamespace = h.kubernetes.NamespaceOrDefault(namespace)
35+
}
36+
if err := cfg.Init(h.kubernetes, applicableNamespace, "", log.Printf); err != nil {
37+
return "", err
4338
}
4439
list := action.NewList(cfg)
45-
// To list across all namespaces, set AllNamespaces to true
46-
if namespace == "" || namespace == "all" {
47-
list.AllNamespaces = true
40+
list.AllNamespaces = allNamespaces
41+
releases, err := list.Run()
42+
if err != nil {
43+
return "", err
44+
} else if len(releases) == 0 {
45+
return "No Helm releases found", nil
46+
}
47+
ret, err := yaml.Marshal(releases)
48+
if err != nil {
49+
return "", err
4850
}
49-
return list.Run()
51+
return string(ret), nil
5052
}

pkg/helm/helm_test.go

Lines changed: 0 additions & 40 deletions
This file was deleted.

pkg/kubernetes/configuration.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,30 @@ func (k *Kubernetes) IsInCluster() bool {
5252
return err == nil && cfg != nil
5353
}
5454

55+
func (k *Kubernetes) configuredNamespace() string {
56+
if ns, _, nsErr := k.clientCmdConfig.Namespace(); nsErr == nil {
57+
return ns
58+
}
59+
return ""
60+
}
61+
62+
func (k *Kubernetes) NamespaceOrDefault(namespace string) string {
63+
if namespace == "" {
64+
return k.configuredNamespace()
65+
}
66+
return namespace
67+
}
68+
69+
// ToRESTConfig returns the rest.Config object (genericclioptions.RESTClientGetter)
70+
func (k *Kubernetes) ToRESTConfig() (*rest.Config, error) {
71+
return k.cfg, nil
72+
}
73+
74+
// ToRawKubeConfigLoader returns the clientcmd.ClientConfig object (genericclioptions.RESTClientGetter)
75+
func (k *Kubernetes) ToRawKubeConfigLoader() clientcmd.ClientConfig {
76+
return k.clientCmdConfig
77+
}
78+
5579
func (k *Kubernetes) ConfigurationView(minify bool) (string, error) {
5680
var cfg clientcmdapi.Config
5781
var err error

pkg/kubernetes/kubernetes.go

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package kubernetes
22

33
import (
44
"github.com/fsnotify/fsnotify"
5+
"github.com/manusa/kubernetes-mcp-server/pkg/helm"
56
v1 "k8s.io/api/core/v1"
7+
"k8s.io/apimachinery/pkg/api/meta"
68
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
79
"k8s.io/apimachinery/pkg/runtime"
810
"k8s.io/client-go/discovery"
@@ -26,9 +28,10 @@ type Kubernetes struct {
2628
scheme *runtime.Scheme
2729
parameterCodec runtime.ParameterCodec
2830
clientSet kubernetes.Interface
29-
discoveryClient *discovery.DiscoveryClient
31+
discoveryClient discovery.CachedDiscoveryInterface
3032
deferredDiscoveryRESTMapper *restmapper.DeferredDiscoveryRESTMapper
3133
dynamicClient *dynamic.DynamicClient
34+
Helm *helm.Helm
3235
}
3336

3437
func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
@@ -43,10 +46,11 @@ func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
4346
if err != nil {
4447
return nil, err
4548
}
46-
k8s.discoveryClient, err = discovery.NewDiscoveryClientForConfig(k8s.cfg)
49+
discoveryClient, err := discovery.NewDiscoveryClientForConfig(k8s.cfg)
4750
if err != nil {
4851
return nil, err
4952
}
53+
k8s.discoveryClient = memory.NewMemCacheClient(discoveryClient)
5054
k8s.deferredDiscoveryRESTMapper = restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(k8s.discoveryClient))
5155
k8s.dynamicClient, err = dynamic.NewForConfig(k8s.cfg)
5256
if err != nil {
@@ -57,6 +61,7 @@ func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
5761
return nil, err
5862
}
5963
k8s.parameterCodec = runtime.NewParameterCodec(k8s.scheme)
64+
k8s.Helm = helm.NewHelm(k8s, "TODO")
6065
return k8s, nil
6166
}
6267

@@ -102,6 +107,14 @@ func (k *Kubernetes) Close() {
102107
}
103108
}
104109

110+
func (k *Kubernetes) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
111+
return k.discoveryClient, nil
112+
}
113+
114+
func (k *Kubernetes) ToRESTMapper() (meta.RESTMapper, error) {
115+
return k.deferredDiscoveryRESTMapper, nil
116+
}
117+
105118
func marshal(v any) (string, error) {
106119
switch t := v.(type) {
107120
case []unstructured.Unstructured:
@@ -123,37 +136,3 @@ func marshal(v any) (string, error) {
123136
}
124137
return string(ret), nil
125138
}
126-
127-
// KubeconfigPath returns the kubeconfig path used by this Kubernetes client
128-
func (k *Kubernetes) KubeconfigPath() string {
129-
return k.Kubeconfig
130-
}
131-
132-
// CurrentContext returns the current context from the kubeconfig
133-
func (k *Kubernetes) CurrentContext() string {
134-
if k.clientCmdConfig == nil {
135-
return ""
136-
}
137-
if rawConfig, err := k.clientCmdConfig.RawConfig(); err == nil {
138-
return rawConfig.CurrentContext
139-
}
140-
return ""
141-
}
142-
143-
// ConfiguredNamespace returns the namespace configured in the kubeconfig/context
144-
func (k *Kubernetes) ConfiguredNamespace() string {
145-
if k.clientCmdConfig == nil {
146-
return ""
147-
}
148-
if ns, _, nsErr := k.clientCmdConfig.Namespace(); nsErr == nil {
149-
return ns
150-
}
151-
return ""
152-
}
153-
154-
func (k *Kubernetes) namespaceOrDefault(namespace string) string {
155-
if namespace == "" {
156-
return k.ConfiguredNamespace()
157-
}
158-
return namespace
159-
}

0 commit comments

Comments
 (0)