Skip to content

feat(output): table output to minimize resource list verbosity #111

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 7 additions & 27 deletions pkg/kubernetes-mcp-server/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ Kubernetes Model Context Protocol (MCP) server
fmt.Printf("Invalid profile name: %s, valid names are: %s\n", viper.GetString("profile"), strings.Join(mcp.ProfileNames, ", "))
os.Exit(1)
}
o := output.FromString(viper.GetString("output"))
if o == nil {
fmt.Printf("Invalid output name: %s, valid names are: %s\n", viper.GetString("output"), strings.Join(output.Names, ", "))
listOutput := output.FromString(viper.GetString("list-output"))
if listOutput == nil {
fmt.Printf("Invalid output name: %s, valid names are: %s\n", viper.GetString("list-output"), strings.Join(output.Names, ", "))
os.Exit(1)
}
klog.V(1).Info("Starting kubernetes-mcp-server")
klog.V(1).Infof(" - Profile: %s", profile.GetName())
klog.V(1).Infof(" - Output: %s", o.GetName())
klog.V(1).Infof(" - ListOutput: %s", listOutput.GetName())
klog.V(1).Infof(" - Read-only mode: %t", viper.GetBool("read-only"))
klog.V(1).Infof(" - Disable destructive tools: %t", viper.GetBool("disable-destructive"))
if viper.GetBool("version") {
Expand All @@ -63,7 +63,7 @@ Kubernetes Model Context Protocol (MCP) server
}
mcpServer, err := mcp.NewSever(mcp.Configuration{
Profile: profile,
Output: o,
ListOutput: listOutput,
ReadOnly: viper.GetBool("read-only"),
DisableDestructive: viper.GetBool("disable-destructive"),
Kubeconfig: viper.GetString("kubeconfig"),
Expand Down Expand Up @@ -109,26 +109,6 @@ func initLogging() {
klog.SetLoggerWithOptions(logger)
}

type profileFlag struct {
mcp.Profile
}

func (p *profileFlag) String() string {
return p.GetName()
}

func (p *profileFlag) Set(v string) error {
p.Profile = mcp.ProfileFromString(v)
if p.Profile != nil {
return nil
}
return fmt.Errorf("invalid profile name: %s, valid names are: %s", v, mcp.ProfileNames)
}

func (p *profileFlag) Type() string {
return "profile"
}

// flagInit initializes the flags for the root command.
// Exposed for testing purposes.
func flagInit() {
Expand All @@ -137,8 +117,8 @@ func flagInit() {
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")
rootCmd.Flags().String("profile", "full", "MCP profile to use (one of: "+strings.Join(mcp.ProfileNames, ", ")+") default is full")
rootCmd.Flags().String("output", "yaml", "Output format for resources (one of: "+strings.Join(output.Names, ", ")+") default is yaml")
rootCmd.Flags().String("profile", "full", "MCP profile to use (one of: "+strings.Join(mcp.ProfileNames, ", ")+")")
rootCmd.Flags().String("list-output", "yaml", "Output format for resource lists (one of: "+strings.Join(output.Names, ", ")+")")
rootCmd.Flags().Bool("read-only", false, "If true, only tools annotated with readOnlyHint=true are exposed")
rootCmd.Flags().Bool("disable-destructive", false, "If true, tools annotated with destructiveHint=true are disabled")
_ = viper.BindPFlags(rootCmd.Flags())
Expand Down
8 changes: 4 additions & 4 deletions pkg/kubernetes-mcp-server/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ func TestProfile(t *testing.T) {
})
}

func TestOutput(t *testing.T) {
func TestListOutput(t *testing.T) {
t.Run("available", func(t *testing.T) {
rootCmd.SetArgs([]string{"--help"})
rootCmd.ResetFlags()
flagInit()
out, err := captureOutput(rootCmd.Execute)
if !strings.Contains(out, "Output format for resources (one of: yaml)") {
if !strings.Contains(out, "Output format for resource lists (one of: yaml, table)") {
t.Fatalf("Expected all available outputs, got %s %v", out, err)
}
})
Expand All @@ -57,8 +57,8 @@ func TestOutput(t *testing.T) {
rootCmd.ResetFlags()
flagInit()
out, err := captureOutput(rootCmd.Execute)
if !strings.Contains(out, "- Output: yaml") {
t.Fatalf("Expected output 'yaml', got %s %v", out, err)
if !strings.Contains(out, "- ListOutput: yaml") {
t.Fatalf("Expected list-output 'yaml', got %s %v", out, err)
}
})
}
Expand Down
14 changes: 5 additions & 9 deletions pkg/kubernetes/configuration.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package kubernetes

import (
"github.com/manusa/kubernetes-mcp-server/pkg/output"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
Expand Down Expand Up @@ -77,7 +77,7 @@ func (k *Kubernetes) ToRawKubeConfigLoader() clientcmd.ClientConfig {
return k.clientCmdConfig
}

func (k *Kubernetes) ConfigurationView(minify bool) (string, error) {
func (k *Kubernetes) ConfigurationView(minify bool) (runtime.Object, error) {
var cfg clientcmdapi.Config
var err error
if k.IsInCluster() {
Expand All @@ -95,20 +95,16 @@ func (k *Kubernetes) ConfigurationView(minify bool) (string, error) {
}
cfg.CurrentContext = "context"
} else if cfg, err = k.clientCmdConfig.RawConfig(); err != nil {
return "", err
return nil, err
}
if minify {
if err = clientcmdapi.MinifyConfig(&cfg); err != nil {
return "", err
return nil, err
}
}
if err = clientcmdapi.FlattenConfig(&cfg); err != nil {
// ignore error
//return "", err
}
convertedObj, err := latest.Scheme.ConvertToVersion(&cfg, latest.ExternalVersion)
if err != nil {
return "", err
}
return output.MarshalYaml(convertedObj)
return latest.Scheme.ConvertToVersion(&cfg, latest.ExternalVersion)
}
24 changes: 10 additions & 14 deletions pkg/kubernetes/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,29 @@ package kubernetes

import (
"context"
"fmt"
"github.com/manusa/kubernetes-mcp-server/pkg/output"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"strings"
)

func (k *Kubernetes) EventsList(ctx context.Context, namespace string) (string, error) {
unstructuredList, err := k.resourcesList(ctx, &schema.GroupVersionKind{
func (k *Kubernetes) EventsList(ctx context.Context, namespace string) ([]map[string]any, error) {
var eventMap []map[string]any
raw, err := k.ResourcesList(ctx, &schema.GroupVersionKind{
Group: "", Version: "v1", Kind: "Event",
}, namespace, "")
}, namespace, ResourceListOptions{})
if err != nil {
return "", err
return eventMap, err
}
unstructuredList := raw.(*unstructured.UnstructuredList)
if len(unstructuredList.Items) == 0 {
return "No events found", nil
return eventMap, nil
}
var eventMap []map[string]any
for _, item := range unstructuredList.Items {
event := &v1.Event{}
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, event); err != nil {
return "", err
return eventMap, err
}
timestamp := event.EventTime.Time
if timestamp.IsZero() && event.Series != nil {
Expand All @@ -47,9 +47,5 @@ func (k *Kubernetes) EventsList(ctx context.Context, namespace string) (string,
"Message": strings.TrimSpace(event.Message),
})
}
yamlEvents, err := output.MarshalYaml(eventMap)
if err != nil {
return "", err
}
return fmt.Sprintf("The following events (YAML format) were found:\n%s", yamlEvents), nil
return eventMap, nil
}
10 changes: 5 additions & 5 deletions pkg/kubernetes/namespaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ package kubernetes

import (
"context"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)

func (k *Kubernetes) NamespacesList(ctx context.Context) ([]unstructured.Unstructured, error) {
func (k *Kubernetes) NamespacesList(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) {
return k.ResourcesList(ctx, &schema.GroupVersionKind{
Group: "", Version: "v1", Kind: "Namespace",
}, "")
}, "", options)
}

func (k *Kubernetes) ProjectsList(ctx context.Context) ([]unstructured.Unstructured, error) {
func (k *Kubernetes) ProjectsList(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) {
return k.ResourcesList(ctx, &schema.GroupVersionKind{
Group: "project.openshift.io", Version: "v1", Kind: "Project",
}, "")
}, "", options)
}
14 changes: 7 additions & 7 deletions pkg/kubernetes/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ import (
"k8s.io/client-go/tools/remotecommand"
)

func (k *Kubernetes) PodsListInAllNamespaces(ctx context.Context, labelSelector string) ([]unstructured.Unstructured, error) {
func (k *Kubernetes) PodsListInAllNamespaces(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) {
return k.ResourcesList(ctx, &schema.GroupVersionKind{
Group: "", Version: "v1", Kind: "Pod",
}, "", labelSelector)
}, "", options)
}

func (k *Kubernetes) PodsListInNamespace(ctx context.Context, namespace string, labelSelector string) ([]unstructured.Unstructured, error) {
func (k *Kubernetes) PodsListInNamespace(ctx context.Context, namespace string, options ResourceListOptions) (runtime.Unstructured, error) {
return k.ResourcesList(ctx, &schema.GroupVersionKind{
Group: "", Version: "v1", Kind: "Pod",
}, namespace, labelSelector)
}, namespace, options)
}

func (k *Kubernetes) PodsGet(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) {
Expand Down Expand Up @@ -95,7 +95,7 @@ func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name, container str
return string(rawData), nil
}

func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string, port int32) (string, error) {
func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string, port int32) ([]*unstructured.Unstructured, error) {
if name == "" {
name = version.BinaryName + "-run-" + rand.String(5)
}
Expand Down Expand Up @@ -164,11 +164,11 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
for _, obj := range resources {
m, err := converter.ToUnstructured(obj)
if err != nil {
return "", err
return nil, err
}
u := &unstructured.Unstructured{}
if err = converter.FromUnstructured(m, u); err != nil {
return "", err
return nil, err
}
toCreate = append(toCreate, u)
}
Expand Down
Loading
Loading