Skip to content

Commit 7e10e82

Browse files
authored
feat(output): table output to minimize resource list verbosity
A new configuration options is available: `--list-output` There are two modes available: - `yaml`: current default (will be changed in subsequent PR), which returns a multi-document YAML - `table`: returns a plain-text table as created by the kube-api server when requested with `Accept: application/json;as=Table;v=v1;g=meta.k8s.io` Additional logic has been added to the table format to include the apiVersion and kind. This is not returned by the server, kubectl doesn't include this either. However, this is extremely handy for the LLM when using the generic resource tools.
1 parent 155fe68 commit 7e10e82

19 files changed

+437
-152
lines changed

pkg/kubernetes-mcp-server/cmd/root.go

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ Kubernetes Model Context Protocol (MCP) server
4747
fmt.Printf("Invalid profile name: %s, valid names are: %s\n", viper.GetString("profile"), strings.Join(mcp.ProfileNames, ", "))
4848
os.Exit(1)
4949
}
50-
o := output.FromString(viper.GetString("output"))
51-
if o == nil {
52-
fmt.Printf("Invalid output name: %s, valid names are: %s\n", viper.GetString("output"), strings.Join(output.Names, ", "))
50+
listOutput := output.FromString(viper.GetString("list-output"))
51+
if listOutput == nil {
52+
fmt.Printf("Invalid output name: %s, valid names are: %s\n", viper.GetString("list-output"), strings.Join(output.Names, ", "))
5353
os.Exit(1)
5454
}
5555
klog.V(1).Info("Starting kubernetes-mcp-server")
5656
klog.V(1).Infof(" - Profile: %s", profile.GetName())
57-
klog.V(1).Infof(" - Output: %s", o.GetName())
57+
klog.V(1).Infof(" - ListOutput: %s", listOutput.GetName())
5858
klog.V(1).Infof(" - Read-only mode: %t", viper.GetBool("read-only"))
5959
klog.V(1).Infof(" - Disable destructive tools: %t", viper.GetBool("disable-destructive"))
6060
if viper.GetBool("version") {
@@ -63,7 +63,7 @@ Kubernetes Model Context Protocol (MCP) server
6363
}
6464
mcpServer, err := mcp.NewSever(mcp.Configuration{
6565
Profile: profile,
66-
Output: o,
66+
ListOutput: listOutput,
6767
ReadOnly: viper.GetBool("read-only"),
6868
DisableDestructive: viper.GetBool("disable-destructive"),
6969
Kubeconfig: viper.GetString("kubeconfig"),
@@ -109,26 +109,6 @@ func initLogging() {
109109
klog.SetLoggerWithOptions(logger)
110110
}
111111

112-
type profileFlag struct {
113-
mcp.Profile
114-
}
115-
116-
func (p *profileFlag) String() string {
117-
return p.GetName()
118-
}
119-
120-
func (p *profileFlag) Set(v string) error {
121-
p.Profile = mcp.ProfileFromString(v)
122-
if p.Profile != nil {
123-
return nil
124-
}
125-
return fmt.Errorf("invalid profile name: %s, valid names are: %s", v, mcp.ProfileNames)
126-
}
127-
128-
func (p *profileFlag) Type() string {
129-
return "profile"
130-
}
131-
132112
// flagInit initializes the flags for the root command.
133113
// Exposed for testing purposes.
134114
func flagInit() {
@@ -137,8 +117,8 @@ func flagInit() {
137117
rootCmd.Flags().IntP("sse-port", "", 0, "Start a SSE server on the specified port")
138118
rootCmd.Flags().StringP("sse-base-url", "", "", "SSE public base URL to use when sending the endpoint message (e.g. https://example.com)")
139119
rootCmd.Flags().StringP("kubeconfig", "", "", "Path to the kubeconfig file to use for authentication")
140-
rootCmd.Flags().String("profile", "full", "MCP profile to use (one of: "+strings.Join(mcp.ProfileNames, ", ")+") default is full")
141-
rootCmd.Flags().String("output", "yaml", "Output format for resources (one of: "+strings.Join(output.Names, ", ")+") default is yaml")
120+
rootCmd.Flags().String("profile", "full", "MCP profile to use (one of: "+strings.Join(mcp.ProfileNames, ", ")+")")
121+
rootCmd.Flags().String("list-output", "yaml", "Output format for resource lists (one of: "+strings.Join(output.Names, ", ")+")")
142122
rootCmd.Flags().Bool("read-only", false, "If true, only tools annotated with readOnlyHint=true are exposed")
143123
rootCmd.Flags().Bool("disable-destructive", false, "If true, tools annotated with destructiveHint=true are disabled")
144124
_ = viper.BindPFlags(rootCmd.Flags())

pkg/kubernetes-mcp-server/cmd/root_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ func TestProfile(t *testing.T) {
4242
})
4343
}
4444

45-
func TestOutput(t *testing.T) {
45+
func TestListOutput(t *testing.T) {
4646
t.Run("available", func(t *testing.T) {
4747
rootCmd.SetArgs([]string{"--help"})
4848
rootCmd.ResetFlags()
4949
flagInit()
5050
out, err := captureOutput(rootCmd.Execute)
51-
if !strings.Contains(out, "Output format for resources (one of: yaml)") {
51+
if !strings.Contains(out, "Output format for resource lists (one of: yaml, table)") {
5252
t.Fatalf("Expected all available outputs, got %s %v", out, err)
5353
}
5454
})
@@ -57,8 +57,8 @@ func TestOutput(t *testing.T) {
5757
rootCmd.ResetFlags()
5858
flagInit()
5959
out, err := captureOutput(rootCmd.Execute)
60-
if !strings.Contains(out, "- Output: yaml") {
61-
t.Fatalf("Expected output 'yaml', got %s %v", out, err)
60+
if !strings.Contains(out, "- ListOutput: yaml") {
61+
t.Fatalf("Expected list-output 'yaml', got %s %v", out, err)
6262
}
6363
})
6464
}

pkg/kubernetes/configuration.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package kubernetes
22

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

80-
func (k *Kubernetes) ConfigurationView(minify bool) (string, error) {
80+
func (k *Kubernetes) ConfigurationView(minify bool) (runtime.Object, error) {
8181
var cfg clientcmdapi.Config
8282
var err error
8383
if k.IsInCluster() {
@@ -95,20 +95,16 @@ func (k *Kubernetes) ConfigurationView(minify bool) (string, error) {
9595
}
9696
cfg.CurrentContext = "context"
9797
} else if cfg, err = k.clientCmdConfig.RawConfig(); err != nil {
98-
return "", err
98+
return nil, err
9999
}
100100
if minify {
101101
if err = clientcmdapi.MinifyConfig(&cfg); err != nil {
102-
return "", err
102+
return nil, err
103103
}
104104
}
105105
if err = clientcmdapi.FlattenConfig(&cfg); err != nil {
106106
// ignore error
107107
//return "", err
108108
}
109-
convertedObj, err := latest.Scheme.ConvertToVersion(&cfg, latest.ExternalVersion)
110-
if err != nil {
111-
return "", err
112-
}
113-
return output.MarshalYaml(convertedObj)
109+
return latest.Scheme.ConvertToVersion(&cfg, latest.ExternalVersion)
114110
}

pkg/kubernetes/events.go

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,29 @@ package kubernetes
22

33
import (
44
"context"
5-
"fmt"
6-
"github.com/manusa/kubernetes-mcp-server/pkg/output"
75
v1 "k8s.io/api/core/v1"
6+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
87
"k8s.io/apimachinery/pkg/runtime"
98
"k8s.io/apimachinery/pkg/runtime/schema"
109
"strings"
1110
)
1211

13-
func (k *Kubernetes) EventsList(ctx context.Context, namespace string) (string, error) {
14-
unstructuredList, err := k.resourcesList(ctx, &schema.GroupVersionKind{
12+
func (k *Kubernetes) EventsList(ctx context.Context, namespace string) ([]map[string]any, error) {
13+
var eventMap []map[string]any
14+
raw, err := k.ResourcesList(ctx, &schema.GroupVersionKind{
1515
Group: "", Version: "v1", Kind: "Event",
16-
}, namespace, "")
16+
}, namespace, ResourceListOptions{})
1717
if err != nil {
18-
return "", err
18+
return eventMap, err
1919
}
20+
unstructuredList := raw.(*unstructured.UnstructuredList)
2021
if len(unstructuredList.Items) == 0 {
21-
return "No events found", nil
22+
return eventMap, nil
2223
}
23-
var eventMap []map[string]any
2424
for _, item := range unstructuredList.Items {
2525
event := &v1.Event{}
2626
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, event); err != nil {
27-
return "", err
27+
return eventMap, err
2828
}
2929
timestamp := event.EventTime.Time
3030
if timestamp.IsZero() && event.Series != nil {
@@ -47,9 +47,5 @@ func (k *Kubernetes) EventsList(ctx context.Context, namespace string) (string,
4747
"Message": strings.TrimSpace(event.Message),
4848
})
4949
}
50-
yamlEvents, err := output.MarshalYaml(eventMap)
51-
if err != nil {
52-
return "", err
53-
}
54-
return fmt.Sprintf("The following events (YAML format) were found:\n%s", yamlEvents), nil
50+
return eventMap, nil
5551
}

pkg/kubernetes/namespaces.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ package kubernetes
22

33
import (
44
"context"
5-
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
5+
"k8s.io/apimachinery/pkg/runtime"
66
"k8s.io/apimachinery/pkg/runtime/schema"
77
)
88

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

15-
func (k *Kubernetes) ProjectsList(ctx context.Context) ([]unstructured.Unstructured, error) {
15+
func (k *Kubernetes) ProjectsList(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) {
1616
return k.ResourcesList(ctx, &schema.GroupVersionKind{
1717
Group: "project.openshift.io", Version: "v1", Kind: "Project",
18-
}, "")
18+
}, "", options)
1919
}

pkg/kubernetes/pods.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@ import (
1818
"k8s.io/client-go/tools/remotecommand"
1919
)
2020

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

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

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

98-
func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string, port int32) (string, error) {
98+
func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string, port int32) ([]*unstructured.Unstructured, error) {
9999
if name == "" {
100100
name = version.BinaryName + "-run-" + rand.String(5)
101101
}
@@ -164,11 +164,11 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
164164
for _, obj := range resources {
165165
m, err := converter.ToUnstructured(obj)
166166
if err != nil {
167-
return "", err
167+
return nil, err
168168
}
169169
u := &unstructured.Unstructured{}
170170
if err = converter.FromUnstructured(m, u); err != nil {
171-
return "", err
171+
return nil, err
172172
}
173173
toCreate = append(toCreate, u)
174174
}

0 commit comments

Comments
 (0)