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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,9 @@ Get a Kubernetes Pod in the current or provided namespace with the provided name

List all the Kubernetes pods in the current cluster from all namespaces

**Parameters:** None
**Parameters:**
- `labelSelector` (`string`, optional)
- Kubernetes label selector (e.g., 'app=myapp,env=prod' or 'app in (myapp,yourapp)'). Use this option to filter the pods by label

### `pods_list_in_namespace`

Expand All @@ -250,6 +252,8 @@ List all the Kubernetes pods in the specified namespace in the current cluster
**Parameters:**
- `namespace` (`string`, required)
- Namespace to list pods from
- `labelSelector` (`string`, optional)
- Kubernetes label selector (e.g., 'app=myapp,env=prod' or 'app in (myapp,yourapp)'). Use this option to filter the pods by label

### `pods_log`

Expand Down Expand Up @@ -343,6 +347,8 @@ List Kubernetes resources and objects in the current cluster
- Namespace to retrieve the namespaced resources from
- Ignored for cluster-scoped resources
- Lists resources from all namespaces if not provided
- `labelSelector` (`string`, optional)
- Kubernetes label selector (e.g., 'app=myapp,env=prod' or 'app in (myapp,yourapp)'). Use this option to filter the pods by label.

## 🧑‍💻 Development <a id="development"></a>

Expand Down
2 changes: 1 addition & 1 deletion pkg/kubernetes/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
func (k *Kubernetes) EventsList(ctx context.Context, namespace string) (string, error) {
unstructuredList, err := k.resourcesList(ctx, &schema.GroupVersionKind{
Group: "", Version: "v1", Kind: "Event",
}, namespace)
}, namespace, "")
if err != nil {
return "", err
}
Expand Down
9 changes: 5 additions & 4 deletions pkg/kubernetes/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"

"github.com/manusa/kubernetes-mcp-server/pkg/version"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -17,16 +18,16 @@ import (
"k8s.io/client-go/tools/remotecommand"
)

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

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

func (k *Kubernetes) PodsGet(ctx context.Context, namespace, name string) (string, error) {
Expand Down
19 changes: 13 additions & 6 deletions pkg/kubernetes/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ package kubernetes

import (
"context"
"regexp"
"strings"

"github.com/manusa/kubernetes-mcp-server/pkg/version"
authv1 "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/yaml"
"regexp"
"strings"
)

const (
Expand All @@ -19,8 +20,12 @@ const (
AppKubernetesPartOf = "app.kubernetes.io/part-of"
)

func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string) (string, error) {
rl, err := k.resourcesList(ctx, gvk, namespace)
func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string, labelSelector ...string) (string, error) {
var selector string
if len(labelSelector) > 0 {
selector = labelSelector[0]
}
rl, err := k.resourcesList(ctx, gvk, namespace, selector)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -69,7 +74,7 @@ func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersi
return k.dynamicClient.Resource(*gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
}

func (k *Kubernetes) resourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string) (*unstructured.UnstructuredList, error) {
func (k *Kubernetes) resourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string, labelSelector string) (*unstructured.UnstructuredList, error) {
gvr, err := k.resourceFor(gvk)
if err != nil {
return nil, err
Expand All @@ -79,7 +84,9 @@ func (k *Kubernetes) resourcesList(ctx context.Context, gvk *schema.GroupVersion
if isNamespaced && !k.canIUse(ctx, gvr, namespace, "list") && namespace == "" {
namespace = k.configuredNamespace()
}
return k.dynamicClient.Resource(*gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
return k.dynamicClient.Resource(*gvr).Namespace(namespace).List(ctx, metav1.ListOptions{
LabelSelector: labelSelector,
})
}

func (k *Kubernetes) resourcesCreateOrUpdate(ctx context.Context, resources []*unstructured.Unstructured) (string, error) {
Expand Down
43 changes: 37 additions & 6 deletions pkg/mcp/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ func (c *mcpContext) crdWaitUntilReady(name string) {
watcher, err := c.newApiExtensionsClient().CustomResourceDefinitions().Watch(c.ctx, metav1.ListOptions{
FieldSelector: "metadata.name=" + name,
})
if err != nil {
panic(fmt.Errorf("failed to watch CRD %v", err))
}
_, err = toolswatch.UntilWithoutRetry(c.ctx, watcher, func(event watch.Event) (bool, error) {
for _, c := range event.Object.(*apiextensionsv1spec.CustomResourceDefinition).Status.Conditions {
if c.Type == apiextensionsv1spec.Established && c.Status == apiextensionsv1spec.ConditionTrue {
Expand Down Expand Up @@ -311,17 +314,45 @@ func createTestData(ctx context.Context) {
_, _ = kubernetesAdmin.CoreV1().Namespaces().
Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-to-delete"}}, metav1.CreateOptions{})
_, _ = kubernetesAdmin.CoreV1().Pods("default").Create(ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-default"},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
ObjectMeta: metav1.ObjectMeta{
Name: "a-pod-in-default",
Labels: map[string]string{"app": "nginx"},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: "nginx",
},
},
},
}, metav1.CreateOptions{})
// Pods for listing
_, _ = kubernetesAdmin.CoreV1().Pods("ns-1").Create(ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-ns-1"},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
ObjectMeta: metav1.ObjectMeta{
Name: "a-pod-in-ns-1",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: "nginx",
},
},
},
}, metav1.CreateOptions{})
_, _ = kubernetesAdmin.CoreV1().Pods("ns-2").Create(ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-ns-2"},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
ObjectMeta: metav1.ObjectMeta{
Name: "a-pod-in-ns-2",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: "nginx",
},
},
},
}, metav1.CreateOptions{})
_, _ = kubernetesAdmin.CoreV1().ConfigMaps("default").
Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "a-configmap-to-delete"}}, metav1.CreateOptions{})
Expand Down
48 changes: 31 additions & 17 deletions pkg/mcp/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,33 @@ import (
"context"
"errors"
"fmt"

"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)

func (s *Server) initPods() []server.ServerTool {
return []server.ServerTool{
{mcp.NewTool("pods_list",
{Tool: mcp.NewTool("pods_list",
mcp.WithDescription("List all the Kubernetes pods in the current cluster from all namespaces"),
), s.podsListInAllNamespaces},
{mcp.NewTool("pods_list_in_namespace",
mcp.WithString("labelSelector", mcp.Description("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"), mcp.Pattern("([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]")),
), Handler: s.podsListInAllNamespaces},
{Tool: mcp.NewTool("pods_list_in_namespace",
mcp.WithDescription("List all the Kubernetes pods in the specified namespace in the current cluster"),
mcp.WithString("namespace", mcp.Description("Namespace to list pods from"), mcp.Required()),
), s.podsListInNamespace},
{mcp.NewTool("pods_get",
mcp.WithString("labelSelector", mcp.Description("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"), mcp.Pattern("([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]")),
), Handler: s.podsListInNamespace},
{Tool: mcp.NewTool("pods_get",
mcp.WithDescription("Get a Kubernetes Pod in the current or provided namespace with the provided name"),
mcp.WithString("namespace", mcp.Description("Namespace to get the Pod from")),
mcp.WithString("name", mcp.Description("Name of the Pod"), mcp.Required()),
), s.podsGet},
{mcp.NewTool("pods_delete",
), Handler: s.podsGet},
{Tool: mcp.NewTool("pods_delete",
mcp.WithDescription("Delete a Kubernetes Pod in the current or provided namespace with the provided name"),
mcp.WithString("namespace", mcp.Description("Namespace to delete the Pod from")),
mcp.WithString("name", mcp.Description("Name of the Pod to delete"), mcp.Required()),
), s.podsDelete},
{mcp.NewTool("pods_exec",
), Handler: s.podsDelete},
{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")),
mcp.WithString("name", mcp.Description("Name of the Pod where the command will be executed"), mcp.Required()),
Expand All @@ -45,25 +48,31 @@ func (s *Server) initPods() []server.ServerTool {
mcp.Required(),
),
mcp.WithString("container", mcp.Description("Name of the Pod container where the command will be executed (Optional)")),
), s.podsExec},
{mcp.NewTool("pods_log",
), Handler: s.podsExec},
{Tool: mcp.NewTool("pods_log",
mcp.WithDescription("Get the logs of a Kubernetes Pod in the current or provided namespace with the provided name"),
mcp.WithString("namespace", mcp.Description("Namespace to get the Pod logs from")),
mcp.WithString("name", mcp.Description("Name of the Pod to get the logs from"), mcp.Required()),
mcp.WithString("container", mcp.Description("Name of the Pod container to get the logs from (Optional)")),
), s.podsLog},
{mcp.NewTool("pods_run",
), Handler: s.podsLog},
{Tool: mcp.NewTool("pods_run",
mcp.WithDescription("Run a Kubernetes Pod in the current or provided namespace with the provided container image and optional name"),
mcp.WithString("namespace", mcp.Description("Namespace to run the Pod in")),
mcp.WithString("name", mcp.Description("Name of the Pod (Optional, random name if not provided)")),
mcp.WithString("image", mcp.Description("Container Image to run in the Pod"), mcp.Required()),
mcp.WithNumber("port", mcp.Description("TCP/IP port to expose from the Pod container (Optional, no port exposed if not provided)")),
), s.podsRun},
), Handler: s.podsRun},
}
}

func (s *Server) podsListInAllNamespaces(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
ret, err := s.k.PodsListInAllNamespaces(ctx)
func (s *Server) podsListInAllNamespaces(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
labelSelector := ctr.Params.Arguments["labelSelector"]
var selector string
if labelSelector != nil {
selector = labelSelector.(string)
}

ret, err := s.k.PodsListInAllNamespaces(ctx, selector)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to list pods in all namespaces: %v", err)), nil
}
Expand All @@ -75,7 +84,12 @@ 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
}
ret, err := s.k.PodsListInNamespace(ctx, ns.(string))
labelSelector := ctr.Params.Arguments["labelSelector"]
var selector string
if labelSelector != nil {
selector = labelSelector.(string)
}
ret, err := s.k.PodsListInNamespace(ctx, ns.(string), selector)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to list pods in namespace %s: %v", ns, err)), nil
}
Expand Down
Loading
Loading