Skip to content
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ A powerful and flexible Kubernetes [Model Context Protocol (MCP)](https://blog.m
- **Get** a pod by name from the specified namespace.
- **Delete** a pod by name from the specified namespace.
- **Show logs** for a pod by name from the specified namespace.
- **Top** gets resource usage metrics for all pods or a specific pod in the specified namespace.
- **Exec** into a pod and run a command.
- **Run** a container image in a pod and optionally expose it.
- **✅ Namespaces**: List Kubernetes Namespaces.
Expand Down Expand Up @@ -314,6 +315,23 @@ Run a Kubernetes Pod in the current or provided namespace with the provided cont
- TCP/IP port to expose from the Pod container
- No port exposed if not provided

### `pods_top`

Lists the resource consumption (CPU and memory) as recorded by the Kubernetes Metrics Server for the specified Kubernetes Pods in the all namespaces, the provided namespace, or the current namespace

**Parameters:**
- `all_namespaces` (`boolean`, optional, default: `true`)
- If `true`, lists resource consumption for Pods in all namespaces
- If `false`, lists resource consumption for Pods in the configured or provided namespace
- `namespace` (`string`, optional)
- Namespace to list the Pod resources from
- If not provided, will list Pods from the configured namespace (in case all_namespaces is false)
- `name` (`string`, optional)
- Name of the Pod to get resource consumption from
- If not provided, will list resource consumption for all Pods in the applicable namespace(s)
- `label_selector` (`string`, optional)
- Kubernetes label selector (e.g. 'app=myapp,env=prod' or 'app in (myapp,yourapp)'), use this option when you want to filter the pods by label (Optional, only applicable when name is not provided)

### `projects_list`

List all the OpenShift projects in the current cluster
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ require (
k8s.io/cli-runtime v0.33.1
k8s.io/client-go v0.33.1
k8s.io/klog/v2 v2.130.1
k8s.io/kubectl v0.33.0
k8s.io/metrics v0.33.0
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
sigs.k8s.io/controller-runtime v0.21.0
sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250211091558-894df3a7e664
Expand Down Expand Up @@ -126,7 +128,6 @@ require (
k8s.io/apiserver v0.33.1 // indirect
k8s.io/component-base v0.33.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/kubectl v0.33.0 // indirect
oras.land/oras-go/v2 v2.5.0 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/kustomize/api v0.19.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,8 @@ k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUy
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/kubectl v0.33.0 h1:HiRb1yqibBSCqic4pRZP+viiOBAnIdwYDpzUFejs07g=
k8s.io/kubectl v0.33.0/go.mod h1:gAlGBuS1Jq1fYZ9AjGWbI/5Vk3M/VW2DK4g10Fpyn/0=
k8s.io/metrics v0.33.0 h1:sKe5sC9qb1RakMhs8LWYNuN2ne6OTCWexj8Jos3rO2Y=
k8s.io/metrics v0.33.0/go.mod h1:XewckTFXmE2AJiP7PT3EXaY7hi7bler3t2ZLyOdQYzU=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c=
Expand Down
43 changes: 43 additions & 0 deletions pkg/kubernetes/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package kubernetes
import (
"bytes"
"context"
"errors"
"fmt"
"k8s.io/metrics/pkg/apis/metrics"
metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
metricsclientset "k8s.io/metrics/pkg/client/clientset/versioned"

"github.com/manusa/kubernetes-mcp-server/pkg/version"
v1 "k8s.io/api/core/v1"
Expand All @@ -18,6 +22,13 @@ import (
"k8s.io/client-go/tools/remotecommand"
)

type PodsTopOptions struct {
metav1.ListOptions
AllNamespaces bool
Namespace string
Name string
}

func (k *Kubernetes) PodsListInAllNamespaces(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) {
return k.ResourcesList(ctx, &schema.GroupVersionKind{
Group: "", Version: "v1", Kind: "Pod",
Expand Down Expand Up @@ -175,6 +186,38 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
return k.resourcesCreateOrUpdate(ctx, toCreate)
}

func (k *Kubernetes) PodsTop(ctx context.Context, options PodsTopOptions) (*metrics.PodMetricsList, error) {
// TODO, maybe move to mcp Tools setup and omit in case metrics aren't available in the target cluster
if !k.supportsGroupVersion(metrics.GroupName + "/" + metricsv1beta1api.SchemeGroupVersion.Version) {
return nil, errors.New("metrics API is not available")
}
namespace := options.Namespace
if options.AllNamespaces && namespace == "" {
namespace = ""
} else {
namespace = k.NamespaceOrDefault(namespace)
}
metricsClient, err := metricsclientset.NewForConfig(k.cfg)
if err != nil {
return nil, fmt.Errorf("failed to create metrics client: %w", err)
}
versionedMetrics := &metricsv1beta1api.PodMetricsList{}
if options.Name != "" {
m, err := metricsClient.MetricsV1beta1().PodMetricses(namespace).Get(ctx, options.Name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get metrics for pod %s/%s: %w", namespace, options.Name, err)
}
versionedMetrics.Items = []metricsv1beta1api.PodMetrics{*m}
} else {
versionedMetrics, err = metricsClient.MetricsV1beta1().PodMetricses(namespace).List(ctx, options.ListOptions)
if err != nil {
return nil, fmt.Errorf("failed to list pod metrics in namespace %s: %w", namespace, err)
}
}
convertedMetrics := &metrics.PodMetricsList{}
return convertedMetrics, metricsv1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, convertedMetrics, nil)
}

func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error) {
namespace = k.NamespaceOrDefault(namespace)
pod, err := k.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
Expand Down
44 changes: 43 additions & 1 deletion pkg/mcp/pods.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package mcp

import (
"bytes"
"context"
"errors"
"fmt"
"github.com/manusa/kubernetes-mcp-server/pkg/kubernetes"
"github.com/manusa/kubernetes-mcp-server/pkg/output"
"k8s.io/kubectl/pkg/metricsutil"

"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
Expand Down Expand Up @@ -53,6 +55,19 @@ func (s *Server) initPods() []server.ServerTool {
mcp.WithIdempotentHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.podsDelete},
{Tool: mcp.NewTool("pods_top",
mcp.WithDescription("List the resource consumption (CPU and memory) as recorded by the Kubernetes Metrics Server for the specified Kubernetes Pods in the all namespaces, the provided namespace, or the current namespace"),
mcp.WithBoolean("all_namespaces", mcp.Description("If true, list the resource consumption for all Pods in all namespaces. If false, list the resource consumption for Pods in the provided namespace or the current namespace"), mcp.DefaultBool(true)),
mcp.WithString("namespace", mcp.Description("Namespace to get the Pods resource consumption from (Optional, current namespace if not provided and all_namespaces is false)")),
mcp.WithString("name", mcp.Description("Name of the Pod to get the resource consumption from (Optional, all Pods in the namespace if not provided)")),
mcp.WithString("label_selector", mcp.Description("Kubernetes label selector (e.g. 'app=myapp,env=prod' or 'app in (myapp,yourapp)'), use this option when you want to filter the pods by label (Optional, only applicable when name is not provided)"), mcp.Pattern("([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]")),
// Tool annotations
mcp.WithTitleAnnotation("Pods: Top"),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithDestructiveHintAnnotation(false),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(true),
), Handler: s.podsTop},
{Tool: mcp.NewTool("pods_exec",
mcp.WithDescription("Execute a command in a Kubernetes Pod in the current or provided namespace with the provided name and command"),
mcp.WithString("namespace", mcp.Description("Namespace of the Pod where the command will be executed")),
Expand Down Expand Up @@ -125,10 +140,10 @@ func (s *Server) podsListInNamespace(ctx context.Context, ctr mcp.CallToolReques
if ns == nil {
return NewTextResult("", errors.New("failed to list pods in namespace, missing argument namespace")), nil
}
labelSelector := ctr.GetArguments()["labelSelector"]
resourceListOptions := kubernetes.ResourceListOptions{
AsTable: s.configuration.ListOutput.AsTable(),
}
labelSelector := ctr.GetArguments()["labelSelector"]
if labelSelector != nil {
resourceListOptions.ListOptions.LabelSelector = labelSelector.(string)
}
Expand Down Expand Up @@ -171,6 +186,33 @@ func (s *Server) podsDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.
return NewTextResult(ret, err), nil
}

func (s *Server) podsTop(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
podsTopOptions := kubernetes.PodsTopOptions{AllNamespaces: true}
if v, ok := ctr.GetArguments()["namespace"].(string); ok {
podsTopOptions.Namespace = v
}
if v, ok := ctr.GetArguments()["all_namespaces"].(bool); ok {
podsTopOptions.AllNamespaces = v
}
if v, ok := ctr.GetArguments()["name"].(string); ok {
podsTopOptions.Name = v
}
if v, ok := ctr.GetArguments()["label_selector"].(string); ok {
podsTopOptions.LabelSelector = v
}
ret, err := s.k.Derived(ctx).PodsTop(ctx, podsTopOptions)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to get pods top: %v", err)), nil
}
buf := new(bytes.Buffer)
printer := metricsutil.NewTopCmdPrinter(buf)
err = printer.PrintPodMetrics(ret.Items, true, true, false, "", true)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to get pods top: %v", err)), nil
}
return NewTextResult(buf.String(), nil), nil
}

func (s *Server) podsExec(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
ns := ctr.GetArguments()["namespace"]
if ns == nil {
Expand Down
1 change: 0 additions & 1 deletion pkg/mcp/pods_exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,5 @@ func TestPodsExec(t *testing.T) {
t.Errorf("expected container name not found %v", podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text)
}
})

})
}
Loading
Loading