diff --git a/README.md b/README.md index cde0df99..f05d6d07 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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` @@ -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 diff --git a/pkg/kubernetes/events.go b/pkg/kubernetes/events.go index 26f0843b..a8db246e 100644 --- a/pkg/kubernetes/events.go +++ b/pkg/kubernetes/events.go @@ -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 } diff --git a/pkg/kubernetes/pods.go b/pkg/kubernetes/pods.go index c2002d56..0e485076 100644 --- a/pkg/kubernetes/pods.go +++ b/pkg/kubernetes/pods.go @@ -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" @@ -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) { diff --git a/pkg/kubernetes/resources.go b/pkg/kubernetes/resources.go index 24a189d0..603c57a6 100644 --- a/pkg/kubernetes/resources.go +++ b/pkg/kubernetes/resources.go @@ -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 ( @@ -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 } @@ -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 @@ -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) { diff --git a/pkg/mcp/common_test.go b/pkg/mcp/common_test.go index b6c5d0b8..058a6d08 100644 --- a/pkg/mcp/common_test.go +++ b/pkg/mcp/common_test.go @@ -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 { @@ -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{}) diff --git a/pkg/mcp/pods.go b/pkg/mcp/pods.go index c75e5dc5..1eb358c0 100644 --- a/pkg/mcp/pods.go +++ b/pkg/mcp/pods.go @@ -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()), @@ -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 } @@ -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 } diff --git a/pkg/mcp/pods_test.go b/pkg/mcp/pods_test.go index 5c2898ec..d5ff6074 100644 --- a/pkg/mcp/pods_test.go +++ b/pkg/mcp/pods_test.go @@ -1,6 +1,9 @@ package mcp import ( + "strings" + "testing" + "github.com/mark3labs/mcp-go/mcp" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -9,8 +12,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "sigs.k8s.io/yaml" - "strings" - "testing" ) func TestPodsListInAllNamespaces(t *testing.T) { @@ -744,3 +745,109 @@ func TestPodsRunInOpenShift(t *testing.T) { }) }) } + +func TestPodsListWithLabelSelector(t *testing.T) { + testCase(t, func(c *mcpContext) { + c.withEnvTest() + kc := c.newKubernetesClient() + // Create pods with labels + _, _ = kc.CoreV1().Pods("default").Create(c.ctx, &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-with-labels", + Labels: map[string]string{"app": "test", "env": "dev"}, + }, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, metav1.CreateOptions{}) + _, _ = kc.CoreV1().Pods("ns-1").Create(c.ctx, &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "another-pod-with-labels", + Labels: map[string]string{"app": "test", "env": "prod"}, + }, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, metav1.CreateOptions{}) + + // Test pods_list with label selector + t.Run("pods_list with label selector returns filtered pods", func(t *testing.T) { + toolResult, err := c.callTool("pods_list", map[string]interface{}{ + "labelSelector": "app=test", + }) + if err != nil { + t.Fatalf("call tool failed %v", err) + return + } + if toolResult.IsError { + t.Fatalf("call tool failed") + return + } + var decoded []unstructured.Unstructured + err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decoded) + if err != nil { + t.Fatalf("invalid tool result content %v", err) + return + } + if len(decoded) != 2 { + t.Fatalf("invalid pods count, expected 2, got %v", len(decoded)) + return + } + }) + + // Test pods_list_in_namespace with label selector + t.Run("pods_list_in_namespace with label selector returns filtered pods", func(t *testing.T) { + toolResult, err := c.callTool("pods_list_in_namespace", map[string]interface{}{ + "namespace": "ns-1", + "labelSelector": "env=prod", + }) + if err != nil { + t.Fatalf("call tool failed %v", err) + return + } + if toolResult.IsError { + t.Fatalf("call tool failed") + return + } + var decoded []unstructured.Unstructured + err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decoded) + if err != nil { + t.Fatalf("invalid tool result content %v", err) + return + } + if len(decoded) != 1 { + t.Fatalf("invalid pods count, expected 1, got %v", len(decoded)) + return + } + if decoded[0].GetName() != "another-pod-with-labels" { + t.Fatalf("invalid pod name, expected another-pod-with-labels, got %v", decoded[0].GetName()) + return + } + }) + + // Test multiple label selectors + t.Run("pods_list with multiple label selectors returns filtered pods", func(t *testing.T) { + toolResult, err := c.callTool("pods_list", map[string]interface{}{ + "labelSelector": "app=test,env=prod", + }) + if err != nil { + t.Fatalf("call tool failed %v", err) + return + } + if toolResult.IsError { + t.Fatalf("call tool failed") + return + } + var decoded []unstructured.Unstructured + err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decoded) + if err != nil { + t.Fatalf("invalid tool result content %v", err) + return + } + if len(decoded) != 1 { + t.Fatalf("invalid pods count, expected 1, got %v", len(decoded)) + return + } + if decoded[0].GetName() != "another-pod-with-labels" { + t.Fatalf("invalid pod name, expected another-pod-with-labels, got %v", decoded[0].GetName()) + return + } + }) + }) +} diff --git a/pkg/mcp/resources.go b/pkg/mcp/resources.go index 9e072d78..746d8034 100644 --- a/pkg/mcp/resources.go +++ b/pkg/mcp/resources.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "k8s.io/apimachinery/pkg/runtime/schema" @@ -16,8 +17,8 @@ func (s *Server) initResources() []server.ServerTool { } commonApiVersion = fmt.Sprintf("(common apiVersion and kind include: %s)", commonApiVersion) return []server.ServerTool{ - {mcp.NewTool("resources_list", - mcp.WithDescription("List Kubernetes resources and objects in the current cluster by providing their apiVersion and kind and optionally the namespace\n"+ + {Tool: mcp.NewTool("resources_list", + mcp.WithDescription("List Kubernetes resources and objects in the current cluster by providing their apiVersion and kind and optionally the namespace and label selector\n"+ commonApiVersion), mcp.WithString("apiVersion", mcp.Description("apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"), @@ -28,8 +29,11 @@ func (s *Server) initResources() []server.ServerTool { mcp.Required(), ), mcp.WithString("namespace", - mcp.Description("Optional Namespace to retrieve the namespaced resources from (ignored in case of cluster scoped resources). If not provided, will list resources from all namespaces"))), s.resourcesList}, - {mcp.NewTool("resources_get", + mcp.Description("Optional Namespace to retrieve the namespaced resources from (ignored in case of cluster scoped resources). If not provided, will list resources from all namespaces")), + 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.resourcesList}, + {Tool: mcp.NewTool("resources_get", mcp.WithDescription("Get a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n"+ commonApiVersion), mcp.WithString("apiVersion", @@ -44,16 +48,16 @@ func (s *Server) initResources() []server.ServerTool { mcp.Description("Optional Namespace to retrieve the namespaced resource from (ignored in case of cluster scoped resources). If not provided, will get resource from configured namespace"), ), mcp.WithString("name", mcp.Description("Name of the resource"), mcp.Required()), - ), s.resourcesGet}, - {mcp.NewTool("resources_create_or_update", + ), Handler: s.resourcesGet}, + {Tool: mcp.NewTool("resources_create_or_update", mcp.WithDescription("Create or update a Kubernetes resource in the current cluster by providing a YAML or JSON representation of the resource\n"+ commonApiVersion), mcp.WithString("resource", mcp.Description("A JSON or YAML containing a representation of the Kubernetes resource. Should include top-level fields such as apiVersion,kind,metadata, and spec"), mcp.Required(), ), - ), s.resourcesCreateOrUpdate}, - {mcp.NewTool("resources_delete", + ), Handler: s.resourcesCreateOrUpdate}, + {Tool: mcp.NewTool("resources_delete", mcp.WithDescription("Delete a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n"+ commonApiVersion), mcp.WithString("apiVersion", @@ -68,7 +72,7 @@ func (s *Server) initResources() []server.ServerTool { mcp.Description("Optional Namespace to delete the namespaced resource from (ignored in case of cluster scoped resources). If not provided, will delete resource from configured namespace"), ), mcp.WithString("name", mcp.Description("Name of the resource"), mcp.Required()), - ), s.resourcesDelete}, + ), Handler: s.resourcesDelete}, } } @@ -77,11 +81,15 @@ func (s *Server) resourcesList(ctx context.Context, ctr mcp.CallToolRequest) (*m if namespace == nil { namespace = "" } + labelSelector := ctr.Params.Arguments["labelSelector"] + if labelSelector == nil { + labelSelector = "" + } gvk, err := parseGroupVersionKind(ctr.Params.Arguments) if err != nil { return NewTextResult("", fmt.Errorf("failed to list resources, %s", err)), nil } - ret, err := s.k.ResourcesList(ctx, gvk, namespace.(string)) + ret, err := s.k.ResourcesList(ctx, gvk, namespace.(string), labelSelector.(string)) if err != nil { return NewTextResult("", fmt.Errorf("failed to list resources: %v", err)), nil } diff --git a/pkg/mcp/resources_test.go b/pkg/mcp/resources_test.go index 551507d9..59fdc48a 100644 --- a/pkg/mcp/resources_test.go +++ b/pkg/mcp/resources_test.go @@ -1,14 +1,15 @@ package mcp import ( + "strings" + "testing" + "github.com/mark3labs/mcp-go/mcp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "sigs.k8s.io/yaml" - "strings" - "testing" ) func TestResourcesList(t *testing.T) { @@ -83,6 +84,74 @@ func TestResourcesList(t *testing.T) { return } }) + + // Test label selector functionality + t.Run("resources_list with label selector returns filtered pods", func(t *testing.T) { + + // List pods with label selector + result, err := c.callTool("resources_list", map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "namespace": "default", + "labelSelector": "app=nginx", + }) + + if err != nil { + t.Fatalf("call tool failed %v", err) + return + } + if result.IsError { + t.Fatalf("call tool failed") + return + } + + var decodedPods []unstructured.Unstructured + err = yaml.Unmarshal([]byte(result.Content[0].(mcp.TextContent).Text), &decodedPods) + if err != nil { + t.Fatalf("invalid tool result content %v", err) + return + } + + // Verify only the pod with matching label is returned + if len(decodedPods) != 1 { + t.Fatalf("expected 1 pod, got %d", len(decodedPods)) + return + } + + if decodedPods[0].GetName() != "a-pod-in-default" { + t.Fatalf("expected pod-with-label, got %s", decodedPods[0].GetName()) + return + } + + // Test that multiple label selectors work + result, err = c.callTool("resources_list", map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "namespace": "default", + "labelSelector": "test-label=test-value,another=value", + }) + + if err != nil { + t.Fatalf("call tool failed %v", err) + return + } + if result.IsError { + t.Fatalf("call tool failed") + return + } + + err = yaml.Unmarshal([]byte(result.Content[0].(mcp.TextContent).Text), &decodedPods) + if err != nil { + t.Fatalf("invalid tool result content %v", err) + return + } + + // Verify no pods match multiple label selector + if len(decodedPods) != 0 { + t.Fatalf("expected 0 pods, got %d", len(decodedPods)) + return + } + }) }) }