Skip to content

Commit 2994699

Browse files
authored
feat: add label selectors to listing tools
This PR introduces the ability to filter Kubernetes resources by label using a labelSelector parameter for the following tools: * pods_list * pods_list_in_namespace * resources_list This enhancement allows users to retrieve a more specific set of resources based on their labels, improving the flexibility and utility of these tools. The labelSelector parameter accepts standard Kubernetes label selector syntax, such as app=myapp,env=prod or app in (myapp,yourapp). Signed-off-by: Eran Cohen <[email protected]>
1 parent ba2b072 commit 2994699

File tree

9 files changed

+292
-49
lines changed

9 files changed

+292
-49
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,9 @@ Get a Kubernetes Pod in the current or provided namespace with the provided name
261261

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

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

266268
### `pods_list_in_namespace`
267269

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

274278
### `pods_log`
275279

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

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

pkg/kubernetes/events.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
func (k *Kubernetes) EventsList(ctx context.Context, namespace string) (string, error) {
1313
unstructuredList, err := k.resourcesList(ctx, &schema.GroupVersionKind{
1414
Group: "", Version: "v1", Kind: "Event",
15-
}, namespace)
15+
}, namespace, "")
1616
if err != nil {
1717
return "", err
1818
}

pkg/kubernetes/pods.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7+
78
"github.com/manusa/kubernetes-mcp-server/pkg/version"
89
v1 "k8s.io/api/core/v1"
910
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -17,16 +18,16 @@ import (
1718
"k8s.io/client-go/tools/remotecommand"
1819
)
1920

20-
func (k *Kubernetes) PodsListInAllNamespaces(ctx context.Context) (string, error) {
21+
func (k *Kubernetes) PodsListInAllNamespaces(ctx context.Context, labelSelector string) (string, error) {
2122
return k.ResourcesList(ctx, &schema.GroupVersionKind{
2223
Group: "", Version: "v1", Kind: "Pod",
23-
}, "")
24+
}, "", labelSelector)
2425
}
2526

26-
func (k *Kubernetes) PodsListInNamespace(ctx context.Context, namespace string) (string, error) {
27+
func (k *Kubernetes) PodsListInNamespace(ctx context.Context, namespace string, labelSelector string) (string, error) {
2728
return k.ResourcesList(ctx, &schema.GroupVersionKind{
2829
Group: "", Version: "v1", Kind: "Pod",
29-
}, namespace)
30+
}, namespace, labelSelector)
3031
}
3132

3233
func (k *Kubernetes) PodsGet(ctx context.Context, namespace, name string) (string, error) {

pkg/kubernetes/resources.go

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

33
import (
44
"context"
5+
"regexp"
6+
"strings"
7+
58
"github.com/manusa/kubernetes-mcp-server/pkg/version"
69
authv1 "k8s.io/api/authorization/v1"
710
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
811
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
912
"k8s.io/apimachinery/pkg/runtime/schema"
1013
"k8s.io/apimachinery/pkg/util/yaml"
11-
"regexp"
12-
"strings"
1314
)
1415

1516
const (
@@ -19,8 +20,12 @@ const (
1920
AppKubernetesPartOf = "app.kubernetes.io/part-of"
2021
)
2122

22-
func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string) (string, error) {
23-
rl, err := k.resourcesList(ctx, gvk, namespace)
23+
func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string, labelSelector ...string) (string, error) {
24+
var selector string
25+
if len(labelSelector) > 0 {
26+
selector = labelSelector[0]
27+
}
28+
rl, err := k.resourcesList(ctx, gvk, namespace, selector)
2429
if err != nil {
2530
return "", err
2631
}
@@ -69,7 +74,7 @@ func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersi
6974
return k.dynamicClient.Resource(*gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
7075
}
7176

72-
func (k *Kubernetes) resourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string) (*unstructured.UnstructuredList, error) {
77+
func (k *Kubernetes) resourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string, labelSelector string) (*unstructured.UnstructuredList, error) {
7378
gvr, err := k.resourceFor(gvk)
7479
if err != nil {
7580
return nil, err
@@ -79,7 +84,9 @@ func (k *Kubernetes) resourcesList(ctx context.Context, gvk *schema.GroupVersion
7984
if isNamespaced && !k.canIUse(ctx, gvr, namespace, "list") && namespace == "" {
8085
namespace = k.configuredNamespace()
8186
}
82-
return k.dynamicClient.Resource(*gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
87+
return k.dynamicClient.Resource(*gvr).Namespace(namespace).List(ctx, metav1.ListOptions{
88+
LabelSelector: labelSelector,
89+
})
8390
}
8491

8592
func (k *Kubernetes) resourcesCreateOrUpdate(ctx context.Context, resources []*unstructured.Unstructured) (string, error) {

pkg/mcp/common_test.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,9 @@ func (c *mcpContext) crdWaitUntilReady(name string) {
298298
watcher, err := c.newApiExtensionsClient().CustomResourceDefinitions().Watch(c.ctx, metav1.ListOptions{
299299
FieldSelector: "metadata.name=" + name,
300300
})
301+
if err != nil {
302+
panic(fmt.Errorf("failed to watch CRD %v", err))
303+
}
301304
_, err = toolswatch.UntilWithoutRetry(c.ctx, watcher, func(event watch.Event) (bool, error) {
302305
for _, c := range event.Object.(*apiextensionsv1spec.CustomResourceDefinition).Status.Conditions {
303306
if c.Type == apiextensionsv1spec.Established && c.Status == apiextensionsv1spec.ConditionTrue {
@@ -347,17 +350,45 @@ func createTestData(ctx context.Context) {
347350
_, _ = kubernetesAdmin.CoreV1().Namespaces().
348351
Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-to-delete"}}, metav1.CreateOptions{})
349352
_, _ = kubernetesAdmin.CoreV1().Pods("default").Create(ctx, &corev1.Pod{
350-
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-default"},
351-
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
353+
ObjectMeta: metav1.ObjectMeta{
354+
Name: "a-pod-in-default",
355+
Labels: map[string]string{"app": "nginx"},
356+
},
357+
Spec: corev1.PodSpec{
358+
Containers: []corev1.Container{
359+
{
360+
Name: "nginx",
361+
Image: "nginx",
362+
},
363+
},
364+
},
352365
}, metav1.CreateOptions{})
353366
// Pods for listing
354367
_, _ = kubernetesAdmin.CoreV1().Pods("ns-1").Create(ctx, &corev1.Pod{
355-
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-ns-1"},
356-
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
368+
ObjectMeta: metav1.ObjectMeta{
369+
Name: "a-pod-in-ns-1",
370+
},
371+
Spec: corev1.PodSpec{
372+
Containers: []corev1.Container{
373+
{
374+
Name: "nginx",
375+
Image: "nginx",
376+
},
377+
},
378+
},
357379
}, metav1.CreateOptions{})
358380
_, _ = kubernetesAdmin.CoreV1().Pods("ns-2").Create(ctx, &corev1.Pod{
359-
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-ns-2"},
360-
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
381+
ObjectMeta: metav1.ObjectMeta{
382+
Name: "a-pod-in-ns-2",
383+
},
384+
Spec: corev1.PodSpec{
385+
Containers: []corev1.Container{
386+
{
387+
Name: "nginx",
388+
Image: "nginx",
389+
},
390+
},
391+
},
361392
}, metav1.CreateOptions{})
362393
_, _ = kubernetesAdmin.CoreV1().ConfigMaps("default").
363394
Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "a-configmap-to-delete"}}, metav1.CreateOptions{})

pkg/mcp/pods.go

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,33 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
78
"github.com/mark3labs/mcp-go/mcp"
89
"github.com/mark3labs/mcp-go/server"
910
)
1011

1112
func (s *Server) initPods() []server.ServerTool {
1213
return []server.ServerTool{
13-
{mcp.NewTool("pods_list",
14+
{Tool: mcp.NewTool("pods_list",
1415
mcp.WithDescription("List all the Kubernetes pods in the current cluster from all namespaces"),
15-
), s.podsListInAllNamespaces},
16-
{mcp.NewTool("pods_list_in_namespace",
16+
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]")),
17+
), Handler: s.podsListInAllNamespaces},
18+
{Tool: mcp.NewTool("pods_list_in_namespace",
1719
mcp.WithDescription("List all the Kubernetes pods in the specified namespace in the current cluster"),
1820
mcp.WithString("namespace", mcp.Description("Namespace to list pods from"), mcp.Required()),
19-
), s.podsListInNamespace},
20-
{mcp.NewTool("pods_get",
21+
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]")),
22+
), Handler: s.podsListInNamespace},
23+
{Tool: mcp.NewTool("pods_get",
2124
mcp.WithDescription("Get a Kubernetes Pod in the current or provided namespace with the provided name"),
2225
mcp.WithString("namespace", mcp.Description("Namespace to get the Pod from")),
2326
mcp.WithString("name", mcp.Description("Name of the Pod"), mcp.Required()),
24-
), s.podsGet},
25-
{mcp.NewTool("pods_delete",
27+
), Handler: s.podsGet},
28+
{Tool: mcp.NewTool("pods_delete",
2629
mcp.WithDescription("Delete a Kubernetes Pod in the current or provided namespace with the provided name"),
2730
mcp.WithString("namespace", mcp.Description("Namespace to delete the Pod from")),
2831
mcp.WithString("name", mcp.Description("Name of the Pod to delete"), mcp.Required()),
29-
), s.podsDelete},
30-
{mcp.NewTool("pods_exec",
32+
), Handler: s.podsDelete},
33+
{Tool: mcp.NewTool("pods_exec",
3134
mcp.WithDescription("Execute a command in a Kubernetes Pod in the current or provided namespace with the provided name and command"),
3235
mcp.WithString("namespace", mcp.Description("Namespace of the Pod where the command will be executed")),
3336
mcp.WithString("name", mcp.Description("Name of the Pod where the command will be executed"), mcp.Required()),
@@ -45,25 +48,31 @@ func (s *Server) initPods() []server.ServerTool {
4548
mcp.Required(),
4649
),
4750
mcp.WithString("container", mcp.Description("Name of the Pod container where the command will be executed (Optional)")),
48-
), s.podsExec},
49-
{mcp.NewTool("pods_log",
51+
), Handler: s.podsExec},
52+
{Tool: mcp.NewTool("pods_log",
5053
mcp.WithDescription("Get the logs of a Kubernetes Pod in the current or provided namespace with the provided name"),
5154
mcp.WithString("namespace", mcp.Description("Namespace to get the Pod logs from")),
5255
mcp.WithString("name", mcp.Description("Name of the Pod to get the logs from"), mcp.Required()),
5356
mcp.WithString("container", mcp.Description("Name of the Pod container to get the logs from (Optional)")),
54-
), s.podsLog},
55-
{mcp.NewTool("pods_run",
57+
), Handler: s.podsLog},
58+
{Tool: mcp.NewTool("pods_run",
5659
mcp.WithDescription("Run a Kubernetes Pod in the current or provided namespace with the provided container image and optional name"),
5760
mcp.WithString("namespace", mcp.Description("Namespace to run the Pod in")),
5861
mcp.WithString("name", mcp.Description("Name of the Pod (Optional, random name if not provided)")),
5962
mcp.WithString("image", mcp.Description("Container Image to run in the Pod"), mcp.Required()),
6063
mcp.WithNumber("port", mcp.Description("TCP/IP port to expose from the Pod container (Optional, no port exposed if not provided)")),
61-
), s.podsRun},
64+
), Handler: s.podsRun},
6265
}
6366
}
6467

65-
func (s *Server) podsListInAllNamespaces(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
66-
ret, err := s.k.PodsListInAllNamespaces(ctx)
68+
func (s *Server) podsListInAllNamespaces(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
69+
labelSelector := ctr.Params.Arguments["labelSelector"]
70+
var selector string
71+
if labelSelector != nil {
72+
selector = labelSelector.(string)
73+
}
74+
75+
ret, err := s.k.PodsListInAllNamespaces(ctx, selector)
6776
if err != nil {
6877
return NewTextResult("", fmt.Errorf("failed to list pods in all namespaces: %v", err)), nil
6978
}
@@ -75,7 +84,12 @@ func (s *Server) podsListInNamespace(ctx context.Context, ctr mcp.CallToolReques
7584
if ns == nil {
7685
return NewTextResult("", errors.New("failed to list pods in namespace, missing argument namespace")), nil
7786
}
78-
ret, err := s.k.PodsListInNamespace(ctx, ns.(string))
87+
labelSelector := ctr.Params.Arguments["labelSelector"]
88+
var selector string
89+
if labelSelector != nil {
90+
selector = labelSelector.(string)
91+
}
92+
ret, err := s.k.PodsListInNamespace(ctx, ns.(string), selector)
7993
if err != nil {
8094
return NewTextResult("", fmt.Errorf("failed to list pods in namespace %s: %v", ns, err)), nil
8195
}

0 commit comments

Comments
 (0)