mirror of
https://github.com/containers/kubernetes-mcp-server.git
synced 2025-10-23 01:22:57 +03:00
feat(kubernetes): added --kubeconfig flag option
This commit is contained in:
@@ -130,10 +130,11 @@ npx kubernetes-mcp-server@latest --help
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `--sse-port` | Starts the MCP server in Server-Sent Event (SSE) mode and listens on the specified port. |
|
||||
| `--log-level` | Sets the logging level (values [from 0-9](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md)). Similar to [kubectl logging levels](https://kubernetes.io/docs/reference/kubectl/quick-reference/#kubectl-output-verbosity-and-debugging). |
|
||||
| Option | Description |
|
||||
|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `--sse-port` | Starts the MCP server in Server-Sent Event (SSE) mode and listens on the specified port. |
|
||||
| `--log-level` | Sets the logging level (values [from 0-9](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md)). Similar to [kubectl logging levels](https://kubernetes.io/docs/reference/kubectl/quick-reference/#kubectl-output-verbosity-and-debugging). |
|
||||
| `--kubeconfig` | Path to the Kubernetes configuration file. If not provided, it will try to resolve the configuration (in-cluster, default location, etc.). |
|
||||
|
||||
## 🛠️ Tools <a id="tools"></a>
|
||||
|
||||
|
||||
@@ -45,7 +45,9 @@ Kubernetes Model Context Protocol (MCP) server
|
||||
fmt.Println(version.Version)
|
||||
return
|
||||
}
|
||||
mcpServer, err := mcp.NewSever()
|
||||
mcpServer, err := mcp.NewSever(mcp.Configuration{
|
||||
Kubeconfig: viper.GetString("kubeconfig"),
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to initialize MCP server: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -73,6 +75,7 @@ func init() {
|
||||
rootCmd.Flags().IntP("log-level", "", 0, "Set the log level (from 0 to 9)")
|
||||
rootCmd.Flags().IntP("sse-port", "", 0, "Start a SSE server on the specified port")
|
||||
rootCmd.Flags().StringP("sse-base-url", "", "", "SSE public base URL to use when sending the endpoint message (e.g. https://example.com)")
|
||||
rootCmd.Flags().StringP("kubeconfig", "", "", "Path to the kubeconfig file to use for authentication")
|
||||
_ = viper.BindPFlags(rootCmd.Flags())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,75 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
"k8s.io/client-go/tools/clientcmd/api/latest"
|
||||
)
|
||||
|
||||
func ConfigurationView(minify bool) (string, error) {
|
||||
// InClusterConfig is a variable that holds the function to get the in-cluster config
|
||||
// Exposed for testing
|
||||
var InClusterConfig = func() (*rest.Config, error) {
|
||||
// TODO use kubernetes.default.svc instead of resolved server
|
||||
// Currently running into: `http: server gave HTTP response to HTTPS client`
|
||||
inClusterConfig, err := rest.InClusterConfig()
|
||||
if inClusterConfig != nil {
|
||||
inClusterConfig.Host = "https://kubernetes.default.svc"
|
||||
}
|
||||
return inClusterConfig, err
|
||||
}
|
||||
|
||||
// resolveKubernetesConfigurations resolves the required kubernetes configurations and sets them in the Kubernetes struct
|
||||
func resolveKubernetesConfigurations(kubernetes *Kubernetes) error {
|
||||
// Always set clientCmdConfig
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
if kubernetes.Kubeconfig != "" {
|
||||
pathOptions.LoadingRules.ExplicitPath = kubernetes.Kubeconfig
|
||||
}
|
||||
kubernetes.clientCmdConfig = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
|
||||
&clientcmd.ClientConfigLoadingRules{ExplicitPath: pathOptions.GetDefaultFilename()},
|
||||
&clientcmd.ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: ""}})
|
||||
var err error
|
||||
if kubernetes.IsInCluster() {
|
||||
kubernetes.cfg, err = InClusterConfig()
|
||||
if err == nil && kubernetes.cfg != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Out of cluster
|
||||
kubernetes.cfg, err = kubernetes.clientCmdConfig.ClientConfig()
|
||||
if kubernetes.cfg != nil && kubernetes.cfg.UserAgent == "" {
|
||||
kubernetes.cfg.UserAgent = rest.DefaultKubernetesUserAgent()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (k *Kubernetes) IsInCluster() bool {
|
||||
if k.Kubeconfig != "" {
|
||||
return false
|
||||
}
|
||||
cfg, err := InClusterConfig()
|
||||
return err == nil && cfg != nil
|
||||
}
|
||||
|
||||
func (k *Kubernetes) ConfigurationView(minify bool) (string, error) {
|
||||
var cfg clientcmdapi.Config
|
||||
var err error
|
||||
inClusterConfig, err := InClusterConfig()
|
||||
if err == nil && inClusterConfig != nil {
|
||||
if k.IsInCluster() {
|
||||
cfg = *clientcmdapi.NewConfig()
|
||||
cfg.Clusters["cluster"] = &clientcmdapi.Cluster{
|
||||
Server: inClusterConfig.Host,
|
||||
InsecureSkipTLSVerify: inClusterConfig.Insecure,
|
||||
Server: k.cfg.Host,
|
||||
InsecureSkipTLSVerify: k.cfg.Insecure,
|
||||
}
|
||||
cfg.AuthInfos["user"] = &clientcmdapi.AuthInfo{
|
||||
Token: inClusterConfig.BearerToken,
|
||||
Token: k.cfg.BearerToken,
|
||||
}
|
||||
cfg.Contexts["context"] = &clientcmdapi.Context{
|
||||
Cluster: "cluster",
|
||||
AuthInfo: "user",
|
||||
}
|
||||
cfg.CurrentContext = "context"
|
||||
} else if cfg, err = resolveConfig().RawConfig(); err != nil {
|
||||
} else if cfg, err = k.clientCmdConfig.RawConfig(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if minify {
|
||||
|
||||
134
pkg/kubernetes/configuration_test.go
Normal file
134
pkg/kubernetes/configuration_test.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"k8s.io/client-go/rest"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKubernetes_IsInCluster(t *testing.T) {
|
||||
t.Run("with explicit kubeconfig", func(t *testing.T) {
|
||||
k := Kubernetes{
|
||||
Kubeconfig: "kubeconfig",
|
||||
}
|
||||
if k.IsInCluster() {
|
||||
t.Errorf("expected not in cluster, got in cluster")
|
||||
}
|
||||
})
|
||||
t.Run("with empty kubeconfig and in cluster", func(t *testing.T) {
|
||||
originalFunction := InClusterConfig
|
||||
InClusterConfig = func() (*rest.Config, error) {
|
||||
return &rest.Config{}, nil
|
||||
}
|
||||
defer func() {
|
||||
InClusterConfig = originalFunction
|
||||
}()
|
||||
k := Kubernetes{
|
||||
Kubeconfig: "",
|
||||
}
|
||||
if !k.IsInCluster() {
|
||||
t.Errorf("expected in cluster, got not in cluster")
|
||||
}
|
||||
})
|
||||
t.Run("with empty kubeconfig and not in cluster (empty)", func(t *testing.T) {
|
||||
originalFunction := InClusterConfig
|
||||
InClusterConfig = func() (*rest.Config, error) {
|
||||
return nil, nil
|
||||
}
|
||||
defer func() {
|
||||
InClusterConfig = originalFunction
|
||||
}()
|
||||
k := Kubernetes{
|
||||
Kubeconfig: "",
|
||||
}
|
||||
if k.IsInCluster() {
|
||||
t.Errorf("expected not in cluster, got in cluster")
|
||||
}
|
||||
})
|
||||
t.Run("with empty kubeconfig and not in cluster (error)", func(t *testing.T) {
|
||||
originalFunction := InClusterConfig
|
||||
InClusterConfig = func() (*rest.Config, error) {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
defer func() {
|
||||
InClusterConfig = originalFunction
|
||||
}()
|
||||
k := Kubernetes{
|
||||
Kubeconfig: "",
|
||||
}
|
||||
if k.IsInCluster() {
|
||||
t.Errorf("expected not in cluster, got in cluster")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestKubernetes_ResolveKubernetesConfigurations_Explicit(t *testing.T) {
|
||||
t.Run("with missing file", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
k := Kubernetes{Kubeconfig: path.Join(tempDir, "config")}
|
||||
err := resolveKubernetesConfigurations(&k)
|
||||
if err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
}
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Errorf("expected file not found error, got %v", err)
|
||||
}
|
||||
if !strings.HasSuffix(err.Error(), ": no such file or directory") {
|
||||
t.Errorf("expected file not found error, got %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("with empty file", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
kubeconfigPath := path.Join(tempDir, "config")
|
||||
if err := os.WriteFile(kubeconfigPath, []byte(""), 0644); err != nil {
|
||||
t.Fatalf("failed to create kubeconfig file: %v", err)
|
||||
}
|
||||
k := Kubernetes{Kubeconfig: kubeconfigPath}
|
||||
err := resolveKubernetesConfigurations(&k)
|
||||
if err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "no configuration has been provided") {
|
||||
t.Errorf("expected no kubeconfig error, got %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("with valid file", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
kubeconfigPath := path.Join(tempDir, "config")
|
||||
kubeconfigContent := `
|
||||
apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://example.com
|
||||
name: example-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: example-cluster
|
||||
user: example-user
|
||||
name: example-context
|
||||
current-context: example-context
|
||||
users:
|
||||
- name: example-user
|
||||
user:
|
||||
token: example-token
|
||||
`
|
||||
if err := os.WriteFile(kubeconfigPath, []byte(kubeconfigContent), 0644); err != nil {
|
||||
t.Fatalf("failed to create kubeconfig file: %v", err)
|
||||
}
|
||||
k := Kubernetes{Kubeconfig: kubeconfigPath}
|
||||
err := resolveKubernetesConfigurations(&k)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if k.cfg == nil {
|
||||
t.Errorf("expected non-nil config, got nil")
|
||||
}
|
||||
if k.cfg.Host != "https://example.com" {
|
||||
t.Errorf("expected host https://example.com, got %s", k.cfg.Host)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -12,27 +12,16 @@ import (
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/restmapper"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// InClusterConfig is a variable that holds the function to get the in-cluster config
|
||||
// Exposed for testing
|
||||
var InClusterConfig = func() (*rest.Config, error) {
|
||||
// TODO use kubernetes.default.svc instead of resolved server
|
||||
// Currently running into: `http: server gave HTTP response to HTTPS client`
|
||||
inClusterConfig, err := rest.InClusterConfig()
|
||||
if inClusterConfig != nil {
|
||||
inClusterConfig.Host = "https://kubernetes.default.svc"
|
||||
}
|
||||
return inClusterConfig, err
|
||||
}
|
||||
|
||||
type CloseWatchKubeConfig func() error
|
||||
|
||||
type Kubernetes struct {
|
||||
// Kubeconfig path override
|
||||
Kubeconfig string
|
||||
cfg *rest.Config
|
||||
kubeConfigFiles []string
|
||||
clientCmdConfig clientcmd.ClientConfig
|
||||
CloseWatchKubeConfig CloseWatchKubeConfig
|
||||
scheme *runtime.Scheme
|
||||
parameterCodec runtime.ParameterCodec
|
||||
@@ -42,14 +31,14 @@ type Kubernetes struct {
|
||||
dynamicClient *dynamic.DynamicClient
|
||||
}
|
||||
|
||||
func NewKubernetes() (*Kubernetes, error) {
|
||||
k8s := &Kubernetes{}
|
||||
var err error
|
||||
k8s.cfg, err = resolveClientConfig()
|
||||
if err != nil {
|
||||
func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
|
||||
k8s := &Kubernetes{
|
||||
Kubeconfig: kubeconfig,
|
||||
}
|
||||
if err := resolveKubernetesConfigurations(k8s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
k8s.kubeConfigFiles = resolveConfig().ConfigAccess().GetLoadingPrecedence()
|
||||
var err error
|
||||
k8s.clientSet, err = kubernetes.NewForConfig(k8s.cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -72,14 +61,18 @@ func NewKubernetes() (*Kubernetes, error) {
|
||||
}
|
||||
|
||||
func (k *Kubernetes) WatchKubeConfig(onKubeConfigChange func() error) {
|
||||
if len(k.kubeConfigFiles) == 0 {
|
||||
if k.clientCmdConfig == nil {
|
||||
return
|
||||
}
|
||||
kubeConfigFiles := k.clientCmdConfig.ConfigAccess().GetLoadingPrecedence()
|
||||
if len(kubeConfigFiles) == 0 {
|
||||
return
|
||||
}
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, file := range k.kubeConfigFiles {
|
||||
for _, file := range kubeConfigFiles {
|
||||
_ = watcher.Add(file)
|
||||
}
|
||||
go func() {
|
||||
@@ -131,35 +124,16 @@ func marshal(v any) (string, error) {
|
||||
return string(ret), nil
|
||||
}
|
||||
|
||||
func resolveConfig() clientcmd.ClientConfig {
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
|
||||
&clientcmd.ClientConfigLoadingRules{ExplicitPath: pathOptions.GetDefaultFilename()},
|
||||
&clientcmd.ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: ""}})
|
||||
}
|
||||
|
||||
func resolveClientConfig() (*rest.Config, error) {
|
||||
inClusterConfig, err := InClusterConfig()
|
||||
if err == nil && inClusterConfig != nil {
|
||||
return inClusterConfig, nil
|
||||
}
|
||||
cfg, err := resolveConfig().ClientConfig()
|
||||
if cfg != nil && cfg.UserAgent == "" {
|
||||
cfg.UserAgent = rest.DefaultKubernetesUserAgent()
|
||||
}
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func configuredNamespace() string {
|
||||
if ns, _, nsErr := resolveConfig().Namespace(); nsErr == nil {
|
||||
func (k *Kubernetes) configuredNamespace() string {
|
||||
if ns, _, nsErr := k.clientCmdConfig.Namespace(); nsErr == nil {
|
||||
return ns
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func namespaceOrDefault(namespace string) string {
|
||||
func (k *Kubernetes) namespaceOrDefault(namespace string) string {
|
||||
if namespace == "" {
|
||||
return configuredNamespace()
|
||||
return k.configuredNamespace()
|
||||
}
|
||||
return namespace
|
||||
}
|
||||
|
||||
@@ -32,11 +32,11 @@ func (k *Kubernetes) PodsListInNamespace(ctx context.Context, namespace string)
|
||||
func (k *Kubernetes) PodsGet(ctx context.Context, namespace, name string) (string, error) {
|
||||
return k.ResourcesGet(ctx, &schema.GroupVersionKind{
|
||||
Group: "", Version: "v1", Kind: "Pod",
|
||||
}, namespaceOrDefault(namespace), name)
|
||||
}, k.namespaceOrDefault(namespace), name)
|
||||
}
|
||||
|
||||
func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (string, error) {
|
||||
namespace = namespaceOrDefault(namespace)
|
||||
namespace = k.namespaceOrDefault(namespace)
|
||||
pod, err := k.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -79,7 +79,7 @@ func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (st
|
||||
|
||||
func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name, container string) (string, error) {
|
||||
tailLines := int64(256)
|
||||
req := k.clientSet.CoreV1().Pods(namespaceOrDefault(namespace)).GetLogs(name, &v1.PodLogOptions{
|
||||
req := k.clientSet.CoreV1().Pods(k.namespaceOrDefault(namespace)).GetLogs(name, &v1.PodLogOptions{
|
||||
TailLines: &tailLines,
|
||||
Container: container,
|
||||
})
|
||||
@@ -108,7 +108,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
|
||||
var resources []any
|
||||
pod := &v1.Pod{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespaceOrDefault(namespace), Labels: labels},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.namespaceOrDefault(namespace), Labels: labels},
|
||||
Spec: v1.PodSpec{Containers: []v1.Container{{
|
||||
Name: name,
|
||||
Image: image,
|
||||
@@ -120,7 +120,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
|
||||
pod.Spec.Containers[0].Ports = []v1.ContainerPort{{ContainerPort: port}}
|
||||
resources = append(resources, &v1.Service{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Service"},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespaceOrDefault(namespace), Labels: labels},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.namespaceOrDefault(namespace), Labels: labels},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: labels,
|
||||
Type: v1.ServiceTypeClusterIP,
|
||||
@@ -135,7 +135,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
|
||||
"kind": "Route",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": name,
|
||||
"namespace": namespaceOrDefault(namespace),
|
||||
"namespace": k.namespaceOrDefault(namespace),
|
||||
"labels": labels,
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
@@ -175,7 +175,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
|
||||
}
|
||||
|
||||
func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error) {
|
||||
namespace = namespaceOrDefault(namespace)
|
||||
namespace = k.namespaceOrDefault(namespace)
|
||||
pod, err := k.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -34,7 +34,7 @@ func (k *Kubernetes) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionK
|
||||
}
|
||||
// If it's a namespaced resource and namespace wasn't provided, try to use the default configured one
|
||||
if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced {
|
||||
namespace = namespaceOrDefault(namespace)
|
||||
namespace = k.namespaceOrDefault(namespace)
|
||||
}
|
||||
rg, err := k.dynamicClient.Resource(*gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
@@ -64,7 +64,7 @@ func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersi
|
||||
}
|
||||
// If it's a namespaced resource and namespace wasn't provided, try to use the default configured one
|
||||
if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced {
|
||||
namespace = namespaceOrDefault(namespace)
|
||||
namespace = k.namespaceOrDefault(namespace)
|
||||
}
|
||||
return k.dynamicClient.Resource(*gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (k *Kubernetes) resourcesList(ctx context.Context, gvk *schema.GroupVersion
|
||||
// Check if operation is allowed for all namespaces (applicable for namespaced resources)
|
||||
isNamespaced, _ := k.isNamespaced(gvk)
|
||||
if isNamespaced && !k.canIUse(ctx, gvr, namespace, "list") && namespace == "" {
|
||||
namespace = configuredNamespace()
|
||||
namespace = k.configuredNamespace()
|
||||
}
|
||||
return k.dynamicClient.Resource(*gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func (k *Kubernetes) resourcesCreateOrUpdate(ctx context.Context, resources []*u
|
||||
namespace := obj.GetNamespace()
|
||||
// If it's a namespaced resource and namespace wasn't provided, try to use the default configured one
|
||||
if namespaced, nsErr := k.isNamespaced(&gvk); nsErr == nil && namespaced {
|
||||
namespace = namespaceOrDefault(namespace)
|
||||
namespace = k.namespaceOrDefault(namespace)
|
||||
}
|
||||
resources[i], rErr = k.dynamicClient.Resource(*gvr).Namespace(namespace).Apply(ctx, obj.GetName(), obj, metav1.ApplyOptions{
|
||||
FieldManager: version.BinaryName,
|
||||
|
||||
@@ -105,7 +105,7 @@ func (c *mcpContext) beforeEach(t *testing.T) {
|
||||
c.ctx, c.cancel = context.WithCancel(context.Background())
|
||||
c.tempDir = t.TempDir()
|
||||
c.withKubeConfig(nil)
|
||||
if c.mcpServer, err = NewSever(); err != nil {
|
||||
if c.mcpServer, err = NewSever(Configuration{}); err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package mcp
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/manusa/kubernetes-mcp-server/pkg/kubernetes"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
@@ -16,18 +15,18 @@ func (s *Server) initConfiguration() []server.ServerTool {
|
||||
"If set to true, keeps only the current-context and the relevant pieces of the configuration for that context. "+
|
||||
"If set to false, all contexts, clusters, auth-infos, and users are returned in the configuration. "+
|
||||
"(Optional, default true)")),
|
||||
), configurationView},
|
||||
), s.configurationView},
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
func configurationView(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
func (s *Server) configurationView(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
minify := true
|
||||
minified := ctr.Params.Arguments["minified"]
|
||||
if _, ok := minified.(bool); ok {
|
||||
minify = minified.(bool)
|
||||
}
|
||||
ret, err := kubernetes.ConfigurationView(minify)
|
||||
ret, err := s.k.ConfigurationView(minify)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to get configuration: %v", err)
|
||||
}
|
||||
|
||||
@@ -8,13 +8,19 @@ import (
|
||||
"slices"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
server *server.MCPServer
|
||||
k *kubernetes.Kubernetes
|
||||
type Configuration struct {
|
||||
Kubeconfig string
|
||||
}
|
||||
|
||||
func NewSever() (*Server, error) {
|
||||
type Server struct {
|
||||
configuration *Configuration
|
||||
server *server.MCPServer
|
||||
k *kubernetes.Kubernetes
|
||||
}
|
||||
|
||||
func NewSever(configuration Configuration) (*Server, error) {
|
||||
s := &Server{
|
||||
configuration: &configuration,
|
||||
server: server.NewMCPServer(
|
||||
version.BinaryName,
|
||||
version.Version,
|
||||
@@ -32,7 +38,7 @@ func NewSever() (*Server, error) {
|
||||
}
|
||||
|
||||
func (s *Server) reloadKubernetesClient() error {
|
||||
k, err := kubernetes.NewKubernetes()
|
||||
k, err := kubernetes.NewKubernetes(s.configuration.Kubeconfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func TestWatchKubeConfig(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
|
||||
t.Skip("Skipping test on non-linux platforms")
|
||||
}
|
||||
testCase(t, func(c *mcpContext) {
|
||||
|
||||
Reference in New Issue
Block a user