diff --git a/README.md b/README.md
index d098a12d0..489c1f32c 100644
--- a/README.md
+++ b/README.md
@@ -301,9 +301,11 @@ In case multi-cluster support is enabled (default) and you have access to multip
core
- **events_list** - List Kubernetes events (warnings, errors, state changes) for debugging and troubleshooting in the current cluster from all namespaces
+ - `fieldSelector` (`string`) - Optional Kubernetes field selector to filter events by field values (e.g. 'type=Warning', 'involvedObject.name=my-pod'). Supported fields: involvedObject.kind, involvedObject.name, involvedObject.namespace, involvedObject.uid, involvedObject.apiVersion, involvedObject.resourceVersion, involvedObject.fieldPath, reason, reportingComponent, source, type. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/
- `namespace` (`string`) - Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces
- **namespaces_list** - List all the Kubernetes namespaces in the current cluster
+ - `fieldSelector` (`string`) - Optional Kubernetes field selector to filter namespaces by field values (e.g. 'metadata.name=default', 'status.phase=Active'). Supported fields: metadata.name, status.phase. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/
- **projects_list** - List all the OpenShift projects in the current cluster
diff --git a/evals/tasks/core/filter-events-by-type/cleanup.sh b/evals/tasks/core/filter-events-by-type/cleanup.sh
new file mode 100755
index 000000000..6c9ee4969
--- /dev/null
+++ b/evals/tasks/core/filter-events-by-type/cleanup.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+kubectl delete namespace events-filter-test --ignore-not-found
diff --git a/evals/tasks/core/filter-events-by-type/filter-events-by-type.yaml b/evals/tasks/core/filter-events-by-type/filter-events-by-type.yaml
new file mode 100644
index 000000000..4b1833125
--- /dev/null
+++ b/evals/tasks/core/filter-events-by-type/filter-events-by-type.yaml
@@ -0,0 +1,28 @@
+kind: Task
+apiVersion: mcpchecker/v1alpha2
+metadata:
+ labels:
+ suite: core
+ name: filter-events-by-type
+ difficulty: easy
+spec:
+ setup:
+ - script:
+ file: setup.sh
+ verify:
+ - script:
+ env:
+ AGENTS_OUTPUT: "{agent.output}"
+ inline: |-
+ bad=$(echo "$AGENTS_OUTPUT" | grep 'Warning events' | grep 'bad-pod' | grep '.events-filter-test. namespace')
+ good=$(echo "$AGENTS_OUTPUT" | grep "good-pod")
+ if [ -n "$bad" ] && [ -z "$good" ]; then
+ exit 0
+ else
+ exit 1
+ fi
+ cleanup:
+ - script:
+ file: cleanup.sh
+ prompt:
+ inline: "List only the Warning events for the pod named 'bad-pod' in the 'events-filter-test' namespace"
diff --git a/evals/tasks/core/filter-events-by-type/setup.sh b/evals/tasks/core/filter-events-by-type/setup.sh
new file mode 100755
index 000000000..ef0fdbd97
--- /dev/null
+++ b/evals/tasks/core/filter-events-by-type/setup.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+NAMESPACE=events-filter-test
+
+kubectl delete namespace ${NAMESPACE} --ignore-not-found
+kubectl create namespace ${NAMESPACE}
+
+# Create a pod with a bad image to generate Warning events
+cat </dev/null | grep -q "."; then
+ exit 0
+ fi
+ sleep 2
+done
+
+echo "Warning events did not appear in time"
+exit 1
diff --git a/evals/tasks/core/filter-namespace-by-name/cleanup.sh b/evals/tasks/core/filter-namespace-by-name/cleanup.sh
new file mode 100755
index 000000000..02c3e487e
--- /dev/null
+++ b/evals/tasks/core/filter-namespace-by-name/cleanup.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+kubectl delete namespace ns-alpha ns-beta ns-gamma --ignore-not-found
diff --git a/evals/tasks/core/filter-namespace-by-name/filter-namespace-by-name.yaml b/evals/tasks/core/filter-namespace-by-name/filter-namespace-by-name.yaml
new file mode 100644
index 000000000..822447f8d
--- /dev/null
+++ b/evals/tasks/core/filter-namespace-by-name/filter-namespace-by-name.yaml
@@ -0,0 +1,22 @@
+kind: Task
+apiVersion: mcpchecker/v1alpha2
+metadata:
+ labels:
+ suite: core
+ name: filter-namespace-by-name
+ difficulty: easy
+spec:
+ setup:
+ - script:
+ file: setup.sh
+ verify:
+ - script:
+ env:
+ AGENTS_OUTPUT: "{agent.output}"
+ inline: |-
+ [ X"$(echo "$AGENTS_OUTPUT" | grep -v 'ns-beta')" = X"" ] && exit 0 || exit 1
+ cleanup:
+ - script:
+ file: cleanup.sh
+ prompt:
+ inline: "Using a field selector, list only the namespace 'ns-beta' from the cluster"
diff --git a/evals/tasks/core/filter-namespace-by-name/setup.sh b/evals/tasks/core/filter-namespace-by-name/setup.sh
new file mode 100755
index 000000000..351c21c61
--- /dev/null
+++ b/evals/tasks/core/filter-namespace-by-name/setup.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+kubectl delete namespace ns-alpha ns-beta ns-gamma --ignore-not-found
+kubectl create namespace ns-alpha
+kubectl create namespace ns-beta
+kubectl create namespace ns-gamma
diff --git a/pkg/kubernetes/events.go b/pkg/kubernetes/events.go
index 73133087e..c66fe9215 100644
--- a/pkg/kubernetes/events.go
+++ b/pkg/kubernetes/events.go
@@ -11,11 +11,11 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
-func (c *Core) EventsList(ctx context.Context, namespace string) ([]map[string]any, error) {
+func (c *Core) EventsList(ctx context.Context, namespace string, options api.ListOptions) ([]map[string]any, error) {
var eventMap []map[string]any
raw, err := c.ResourcesList(ctx, &schema.GroupVersionKind{
Group: "", Version: "v1", Kind: "Event",
- }, namespace, api.ListOptions{})
+ }, namespace, options)
if err != nil {
return eventMap, err
}
diff --git a/pkg/mcp/events_test.go b/pkg/mcp/events_test.go
index dc64ab7d5..21e749482 100644
--- a/pkg/mcp/events_test.go
+++ b/pkg/mcp/events_test.go
@@ -166,6 +166,148 @@ func (s *EventsSuite) TestEventsListForbidden() {
})
}
+func (s *EventsSuite) TestEventsListWithFieldSelector() {
+ s.InitMcpClient()
+ client := kubernetes.NewForConfigOrDie(envTestRestConfig)
+
+ // Create events with different types to verify fieldSelector filtering
+ _, err := client.CoreV1().Events("default").Create(s.T().Context(), &v1.Event{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "warning-event",
+ },
+ InvolvedObject: v1.ObjectReference{
+ APIVersion: "v1",
+ Kind: "Pod",
+ Name: "crashing-pod",
+ Namespace: "default",
+ },
+ Type: "Warning",
+ Reason: "BackOff",
+ Message: "Back-off restarting failed container",
+ }, metav1.CreateOptions{})
+ s.Require().NoError(err, "failed to create warning event")
+ _, err = client.CoreV1().Events("default").Create(s.T().Context(), &v1.Event{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "normal-event",
+ },
+ InvolvedObject: v1.ObjectReference{
+ APIVersion: "v1",
+ Kind: "Pod",
+ Name: "healthy-pod",
+ Namespace: "default",
+ },
+ Type: "Normal",
+ Reason: "Started",
+ Message: "Started container successfully",
+ }, metav1.CreateOptions{})
+ s.Require().NoError(err, "failed to create normal event")
+
+ s.Run("events_list(fieldSelector=type=Warning) returns only warning events", func() {
+ toolResult, err := s.CallTool("events_list", map[string]interface{}{
+ "fieldSelector": "type=Warning",
+ })
+ s.Run("no error", func() {
+ s.Nil(err, "call tool failed %v", err)
+ s.False(toolResult.IsError, "call tool failed")
+ })
+ s.Run("has yaml comment indicating output format", func() {
+ s.True(strings.HasPrefix(toolResult.Content[0].(*mcp.TextContent).Text, "# The following events (YAML format) were found:\n"), "unexpected result %v", toolResult.Content[0].(*mcp.TextContent).Text)
+ })
+ var decoded []v1.Event
+ err = yaml.Unmarshal([]byte(toolResult.Content[0].(*mcp.TextContent).Text), &decoded)
+ s.Run("has yaml content", func() {
+ s.Nil(err, "unmarshal failed %v", err)
+ })
+ s.Run("returns only warning event", func() {
+ s.YAMLEq(""+
+ "- InvolvedObject:\n"+
+ " Kind: Pod\n"+
+ " Name: crashing-pod\n"+
+ " apiVersion: v1\n"+
+ " Message: Back-off restarting failed container\n"+
+ " Namespace: default\n"+
+ " Reason: BackOff\n"+
+ " Timestamp: 0001-01-01 00:00:00 +0000 UTC\n"+
+ " Type: Warning\n",
+ toolResult.Content[0].(*mcp.TextContent).Text,
+ "unexpected result %v", toolResult.Content[0].(*mcp.TextContent).Text)
+ })
+ })
+
+ s.Run("events_list(fieldSelector=involvedObject.name=healthy-pod) returns only matching event", func() {
+ toolResult, err := s.CallTool("events_list", map[string]interface{}{
+ "fieldSelector": "involvedObject.name=healthy-pod",
+ })
+ s.Run("no error", func() {
+ s.Nil(err, "call tool failed %v", err)
+ s.False(toolResult.IsError, "call tool failed")
+ })
+ var decoded []v1.Event
+ err = yaml.Unmarshal([]byte(toolResult.Content[0].(*mcp.TextContent).Text), &decoded)
+ s.Run("has yaml content", func() {
+ s.Nil(err, "unmarshal failed %v", err)
+ })
+ s.Run("returns only healthy-pod event", func() {
+ s.YAMLEq(""+
+ "- InvolvedObject:\n"+
+ " Kind: Pod\n"+
+ " Name: healthy-pod\n"+
+ " apiVersion: v1\n"+
+ " Message: Started container successfully\n"+
+ " Namespace: default\n"+
+ " Reason: Started\n"+
+ " Timestamp: 0001-01-01 00:00:00 +0000 UTC\n"+
+ " Type: Normal\n",
+ toolResult.Content[0].(*mcp.TextContent).Text,
+ "unexpected result %v", toolResult.Content[0].(*mcp.TextContent).Text)
+ })
+ })
+
+ s.Run("events_list(fieldSelector=type=Warning, namespace=default) combines fieldSelector and namespace", func() {
+ toolResult, err := s.CallTool("events_list", map[string]interface{}{
+ "namespace": "default",
+ "fieldSelector": "type=Warning",
+ })
+ s.Run("no error", func() {
+ s.Nil(err, "call tool failed %v", err)
+ s.False(toolResult.IsError, "call tool failed")
+ })
+ var decoded []v1.Event
+ err = yaml.Unmarshal([]byte(toolResult.Content[0].(*mcp.TextContent).Text), &decoded)
+ s.Run("has yaml content", func() {
+ s.Nil(err, "unmarshal failed %v", err)
+ })
+ s.Run("returns only warning event from default namespace", func() {
+ s.YAMLEq(""+
+ "- InvolvedObject:\n"+
+ " Kind: Pod\n"+
+ " Name: crashing-pod\n"+
+ " apiVersion: v1\n"+
+ " Message: Back-off restarting failed container\n"+
+ " Namespace: default\n"+
+ " Reason: BackOff\n"+
+ " Timestamp: 0001-01-01 00:00:00 +0000 UTC\n"+
+ " Type: Warning\n",
+ toolResult.Content[0].(*mcp.TextContent).Text,
+ "unexpected result %v", toolResult.Content[0].(*mcp.TextContent).Text)
+ })
+ })
+
+ s.Run("events_list(fieldSelector=type=Warning, namespace=ns-1) returns no events when none match", func() {
+ toolResult, err := s.CallTool("events_list", map[string]interface{}{
+ "namespace": "ns-1",
+ "fieldSelector": "type=Warning",
+ })
+ s.Run("no error", func() {
+ s.Nil(err, "call tool failed %v", err)
+ s.False(toolResult.IsError, "call tool failed")
+ })
+ s.Run("returns no events message", func() {
+ s.Equal("# No events found", toolResult.Content[0].(*mcp.TextContent).Text)
+ })
+ })
+}
+
func TestEvents(t *testing.T) {
suite.Run(t, new(EventsSuite))
}
diff --git a/pkg/mcp/namespaces_test.go b/pkg/mcp/namespaces_test.go
index 522bcc7d6..b540266c8 100644
--- a/pkg/mcp/namespaces_test.go
+++ b/pkg/mcp/namespaces_test.go
@@ -150,6 +150,37 @@ func (s *NamespacesSuite) TestNamespacesListAsTable() {
})
}
+func (s *NamespacesSuite) TestNamespacesListWithFieldSelector() {
+ s.InitMcpClient()
+
+ s.Run("namespaces_list(fieldSelector=metadata.name=ns-1) returns only matching namespace", func() {
+ toolResult, err := s.CallTool("namespaces_list", map[string]interface{}{
+ "fieldSelector": "metadata.name=ns-1",
+ })
+ s.Run("no error", func() {
+ s.Nil(err, "call tool failed %v", err)
+ s.False(toolResult.IsError, "call tool failed")
+ })
+ var decoded []unstructured.Unstructured
+ err = yaml.Unmarshal([]byte(toolResult.Content[0].(*mcp.TextContent).Text), &decoded)
+ s.Run("has yaml content", func() {
+ s.Nil(err, "invalid tool result content %v", err)
+ })
+ s.Run("returns exactly 1 namespace", func() {
+ s.Lenf(decoded, 1, "expected exactly 1 namespace, got %v", len(decoded))
+ })
+ s.Run("returns ns-1", func() {
+ s.Equalf("ns-1", decoded[0].GetName(), "expected ns-1, got %v", decoded[0].GetName())
+ })
+ s.Run("excludes other namespaces", func() {
+ for _, ns := range decoded {
+ s.NotEqualf("default", ns.GetName(), "default should have been filtered out by fieldSelector")
+ s.NotEqualf("ns-2", ns.GetName(), "ns-2 should have been filtered out by fieldSelector")
+ }
+ })
+ })
+}
+
func (s *NamespacesSuite) TestProjectsListInOpenShift() {
s.Require().NoError(EnvTestInOpenShift(s.T().Context()), "Expected to configure test for OpenShift")
s.T().Cleanup(func() {
diff --git a/pkg/mcp/testdata/toolsets-core-tools.json b/pkg/mcp/testdata/toolsets-core-tools.json
index 4244a9534..e763fddf4 100644
--- a/pkg/mcp/testdata/toolsets-core-tools.json
+++ b/pkg/mcp/testdata/toolsets-core-tools.json
@@ -9,6 +9,11 @@
"description": "List Kubernetes events (warnings, errors, state changes) for debugging and troubleshooting in the current cluster from all namespaces",
"inputSchema": {
"properties": {
+ "fieldSelector": {
+ "description": "Optional Kubernetes field selector to filter events by field values (e.g. 'type=Warning', 'involvedObject.name=my-pod'). Supported fields: involvedObject.kind, involvedObject.name, involvedObject.namespace, involvedObject.uid, involvedObject.apiVersion, involvedObject.resourceVersion, involvedObject.fieldPath, reason, reportingComponent, source, type. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
+ "type": "string"
+ },
"namespace": {
"description": "Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces",
"type": "string"
@@ -28,7 +33,13 @@
},
"description": "List all the Kubernetes namespaces in the current cluster",
"inputSchema": {
- "properties": {},
+ "properties": {
+ "fieldSelector": {
+ "description": "Optional Kubernetes field selector to filter namespaces by field values (e.g. 'metadata.name=default', 'status.phase=Active'). Supported fields: metadata.name, status.phase. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
+ "type": "string"
+ }
+ },
"type": "object"
},
"name": "namespaces_list",
@@ -221,7 +232,7 @@
"properties": {
"fieldSelector": {
"description": "Optional Kubernetes field selector to filter pods by field values (e.g. 'status.phase=Running', 'spec.nodeName=node1'). Supported fields: metadata.name, metadata.namespace, spec.nodeName, spec.restartPolicy, spec.schedulerName, spec.serviceAccountName, status.phase (Pending/Running/Succeeded/Failed/Unknown), status.podIP, status.nominatedNodeName. Note: CrashLoopBackOff is a container state, not a pod phase, so it cannot be filtered directly. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
- "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[.\\-A-Za-z0-9]+)+$",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
"type": "string"
},
"labelSelector": {
@@ -247,7 +258,7 @@
"properties": {
"fieldSelector": {
"description": "Optional Kubernetes field selector to filter pods by field values (e.g. 'status.phase=Running', 'spec.nodeName=node1'). Supported fields: metadata.name, metadata.namespace, spec.nodeName, spec.restartPolicy, spec.schedulerName, spec.serviceAccountName, status.phase (Pending/Running/Succeeded/Failed/Unknown), status.podIP, status.nominatedNodeName. Note: CrashLoopBackOff is a container state, not a pod phase, so it cannot be filtered directly. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
- "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[.\\-A-Za-z0-9]+)+$",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
"type": "string"
},
"labelSelector": {
@@ -495,7 +506,7 @@
},
"fieldSelector": {
"description": "Optional Kubernetes field selector to filter resources by field values (e.g. 'status.phase=Running', 'metadata.name=myresource'). Supported fields vary by resource type. For Pods: metadata.name, metadata.namespace, spec.nodeName, spec.restartPolicy, spec.schedulerName, spec.serviceAccountName, status.phase (Pending/Running/Succeeded/Failed/Unknown), status.podIP, status.nominatedNodeName. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
- "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[.\\-A-Za-z0-9]+)+$",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
"type": "string"
},
"kind": {
diff --git a/pkg/mcp/testdata/toolsets-full-tools-multicluster.json b/pkg/mcp/testdata/toolsets-full-tools-multicluster.json
index 1af5692a2..ecfe6fd4d 100644
--- a/pkg/mcp/testdata/toolsets-full-tools-multicluster.json
+++ b/pkg/mcp/testdata/toolsets-full-tools-multicluster.json
@@ -49,6 +49,11 @@
"description": "Optional parameter selecting which context to run the tool in. Defaults to fake-context if not set",
"type": "string"
},
+ "fieldSelector": {
+ "description": "Optional Kubernetes field selector to filter events by field values (e.g. 'type=Warning', 'involvedObject.name=my-pod'). Supported fields: involvedObject.kind, involvedObject.name, involvedObject.namespace, involvedObject.uid, involvedObject.apiVersion, involvedObject.resourceVersion, involvedObject.fieldPath, reason, reportingComponent, source, type. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
+ "type": "string"
+ },
"namespace": {
"description": "Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces",
"type": "string"
@@ -72,6 +77,11 @@
"context": {
"description": "Optional parameter selecting which context to run the tool in. Defaults to fake-context if not set",
"type": "string"
+ },
+ "fieldSelector": {
+ "description": "Optional Kubernetes field selector to filter namespaces by field values (e.g. 'metadata.name=default', 'status.phase=Active'). Supported fields: metadata.name, status.phase. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
+ "type": "string"
}
},
"type": "object"
@@ -294,7 +304,7 @@
},
"fieldSelector": {
"description": "Optional Kubernetes field selector to filter pods by field values (e.g. 'status.phase=Running', 'spec.nodeName=node1'). Supported fields: metadata.name, metadata.namespace, spec.nodeName, spec.restartPolicy, spec.schedulerName, spec.serviceAccountName, status.phase (Pending/Running/Succeeded/Failed/Unknown), status.podIP, status.nominatedNodeName. Note: CrashLoopBackOff is a container state, not a pod phase, so it cannot be filtered directly. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
- "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[.\\-A-Za-z0-9]+)+$",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
"type": "string"
},
"labelSelector": {
@@ -324,7 +334,7 @@
},
"fieldSelector": {
"description": "Optional Kubernetes field selector to filter pods by field values (e.g. 'status.phase=Running', 'spec.nodeName=node1'). Supported fields: metadata.name, metadata.namespace, spec.nodeName, spec.restartPolicy, spec.schedulerName, spec.serviceAccountName, status.phase (Pending/Running/Succeeded/Failed/Unknown), status.podIP, status.nominatedNodeName. Note: CrashLoopBackOff is a container state, not a pod phase, so it cannot be filtered directly. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
- "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[.\\-A-Za-z0-9]+)+$",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
"type": "string"
},
"labelSelector": {
@@ -600,7 +610,7 @@
},
"fieldSelector": {
"description": "Optional Kubernetes field selector to filter resources by field values (e.g. 'status.phase=Running', 'metadata.name=myresource'). Supported fields vary by resource type. For Pods: metadata.name, metadata.namespace, spec.nodeName, spec.restartPolicy, spec.schedulerName, spec.serviceAccountName, status.phase (Pending/Running/Succeeded/Failed/Unknown), status.podIP, status.nominatedNodeName. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
- "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[.\\-A-Za-z0-9]+)+$",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
"type": "string"
},
"kind": {
diff --git a/pkg/mcp/testdata/toolsets-full-tools-openshift.json b/pkg/mcp/testdata/toolsets-full-tools-openshift.json
index b3094c839..fffb273ba 100644
--- a/pkg/mcp/testdata/toolsets-full-tools-openshift.json
+++ b/pkg/mcp/testdata/toolsets-full-tools-openshift.json
@@ -29,6 +29,11 @@
"description": "List Kubernetes events (warnings, errors, state changes) for debugging and troubleshooting in the current cluster from all namespaces",
"inputSchema": {
"properties": {
+ "fieldSelector": {
+ "description": "Optional Kubernetes field selector to filter events by field values (e.g. 'type=Warning', 'involvedObject.name=my-pod'). Supported fields: involvedObject.kind, involvedObject.name, involvedObject.namespace, involvedObject.uid, involvedObject.apiVersion, involvedObject.resourceVersion, involvedObject.fieldPath, reason, reportingComponent, source, type. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
+ "type": "string"
+ },
"namespace": {
"description": "Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces",
"type": "string"
@@ -48,7 +53,13 @@
},
"description": "List all the Kubernetes namespaces in the current cluster",
"inputSchema": {
- "properties": {},
+ "properties": {
+ "fieldSelector": {
+ "description": "Optional Kubernetes field selector to filter namespaces by field values (e.g. 'metadata.name=default', 'status.phase=Active'). Supported fields: metadata.name, status.phase. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
+ "type": "string"
+ }
+ },
"type": "object"
},
"name": "namespaces_list",
@@ -241,7 +252,7 @@
"properties": {
"fieldSelector": {
"description": "Optional Kubernetes field selector to filter pods by field values (e.g. 'status.phase=Running', 'spec.nodeName=node1'). Supported fields: metadata.name, metadata.namespace, spec.nodeName, spec.restartPolicy, spec.schedulerName, spec.serviceAccountName, status.phase (Pending/Running/Succeeded/Failed/Unknown), status.podIP, status.nominatedNodeName. Note: CrashLoopBackOff is a container state, not a pod phase, so it cannot be filtered directly. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
- "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[.\\-A-Za-z0-9]+)+$",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
"type": "string"
},
"labelSelector": {
@@ -267,7 +278,7 @@
"properties": {
"fieldSelector": {
"description": "Optional Kubernetes field selector to filter pods by field values (e.g. 'status.phase=Running', 'spec.nodeName=node1'). Supported fields: metadata.name, metadata.namespace, spec.nodeName, spec.restartPolicy, spec.schedulerName, spec.serviceAccountName, status.phase (Pending/Running/Succeeded/Failed/Unknown), status.podIP, status.nominatedNodeName. Note: CrashLoopBackOff is a container state, not a pod phase, so it cannot be filtered directly. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
- "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[.\\-A-Za-z0-9]+)+$",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
"type": "string"
},
"labelSelector": {
@@ -530,7 +541,7 @@
},
"fieldSelector": {
"description": "Optional Kubernetes field selector to filter resources by field values (e.g. 'status.phase=Running', 'metadata.name=myresource'). Supported fields vary by resource type. For Pods: metadata.name, metadata.namespace, spec.nodeName, spec.restartPolicy, spec.schedulerName, spec.serviceAccountName, status.phase (Pending/Running/Succeeded/Failed/Unknown), status.podIP, status.nominatedNodeName. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
- "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[.\\-A-Za-z0-9]+)+$",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
"type": "string"
},
"kind": {
diff --git a/pkg/mcp/testdata/toolsets-full-tools.json b/pkg/mcp/testdata/toolsets-full-tools.json
index 69bcbd5c4..1ce4475a2 100644
--- a/pkg/mcp/testdata/toolsets-full-tools.json
+++ b/pkg/mcp/testdata/toolsets-full-tools.json
@@ -29,6 +29,11 @@
"description": "List Kubernetes events (warnings, errors, state changes) for debugging and troubleshooting in the current cluster from all namespaces",
"inputSchema": {
"properties": {
+ "fieldSelector": {
+ "description": "Optional Kubernetes field selector to filter events by field values (e.g. 'type=Warning', 'involvedObject.name=my-pod'). Supported fields: involvedObject.kind, involvedObject.name, involvedObject.namespace, involvedObject.uid, involvedObject.apiVersion, involvedObject.resourceVersion, involvedObject.fieldPath, reason, reportingComponent, source, type. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
+ "type": "string"
+ },
"namespace": {
"description": "Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces",
"type": "string"
@@ -48,7 +53,13 @@
},
"description": "List all the Kubernetes namespaces in the current cluster",
"inputSchema": {
- "properties": {},
+ "properties": {
+ "fieldSelector": {
+ "description": "Optional Kubernetes field selector to filter namespaces by field values (e.g. 'metadata.name=default', 'status.phase=Active'). Supported fields: metadata.name, status.phase. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
+ "type": "string"
+ }
+ },
"type": "object"
},
"name": "namespaces_list",
@@ -241,7 +252,7 @@
"properties": {
"fieldSelector": {
"description": "Optional Kubernetes field selector to filter pods by field values (e.g. 'status.phase=Running', 'spec.nodeName=node1'). Supported fields: metadata.name, metadata.namespace, spec.nodeName, spec.restartPolicy, spec.schedulerName, spec.serviceAccountName, status.phase (Pending/Running/Succeeded/Failed/Unknown), status.podIP, status.nominatedNodeName. Note: CrashLoopBackOff is a container state, not a pod phase, so it cannot be filtered directly. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
- "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[.\\-A-Za-z0-9]+)+$",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
"type": "string"
},
"labelSelector": {
@@ -267,7 +278,7 @@
"properties": {
"fieldSelector": {
"description": "Optional Kubernetes field selector to filter pods by field values (e.g. 'status.phase=Running', 'spec.nodeName=node1'). Supported fields: metadata.name, metadata.namespace, spec.nodeName, spec.restartPolicy, spec.schedulerName, spec.serviceAccountName, status.phase (Pending/Running/Succeeded/Failed/Unknown), status.podIP, status.nominatedNodeName. Note: CrashLoopBackOff is a container state, not a pod phase, so it cannot be filtered directly. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
- "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[.\\-A-Za-z0-9]+)+$",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
"type": "string"
},
"labelSelector": {
@@ -515,7 +526,7 @@
},
"fieldSelector": {
"description": "Optional Kubernetes field selector to filter resources by field values (e.g. 'status.phase=Running', 'metadata.name=myresource'). Supported fields vary by resource type. For Pods: metadata.name, metadata.namespace, spec.nodeName, spec.restartPolicy, spec.schedulerName, spec.serviceAccountName, status.phase (Pending/Running/Succeeded/Failed/Unknown), status.podIP, status.nominatedNodeName. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
- "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[.\\-A-Za-z0-9]+)+$",
+ "pattern": "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$",
"type": "string"
},
"kind": {
diff --git a/pkg/mcp/toolsets_test.go b/pkg/mcp/toolsets_test.go
index b83dbf278..0b0e1579c 100644
--- a/pkg/mcp/toolsets_test.go
+++ b/pkg/mcp/toolsets_test.go
@@ -200,26 +200,27 @@ func (s *ToolsetsSuite) TestGranularToolsetsTools() {
func (s *ToolsetsSuite) TestInputSchemaEdgeCases() {
//https://github.com/containers/kubernetes-mcp-server/issues/340
s.Run("InputSchema for no-arg tool is object with empty properties", func() {
+ s.Handle(test.NewInOpenShiftHandler())
s.InitMcpClient()
tools, err := s.ListTools()
s.Run("ListTools returns tools", func() {
s.NotNil(tools, "Expected tools from ListTools")
s.NoError(err, "Expected no error from ListTools")
})
- var namespacesList *mcp.Tool
+ var projectsList *mcp.Tool
for _, t := range tools.Tools {
- if t.Name == "namespaces_list" {
- namespacesList = t
+ if t.Name == "projects_list" {
+ projectsList = t
break
}
}
- s.Require().NotNil(namespacesList, "Expected namespaces_list from ListTools")
- schema, ok := namespacesList.InputSchema.(map[string]any)
+ s.Require().NotNil(projectsList, "Expected projects_list from ListTools")
+ schema, ok := projectsList.InputSchema.(map[string]any)
s.Require().True(ok, "Expected InputSchema to be map[string]any")
- s.NotNil(schema["properties"], "Expected namespaces_list.InputSchema.properties not to be nil")
+ s.NotNil(schema["properties"], "Expected projects_list.InputSchema.properties not to be nil")
properties, ok := schema["properties"].(map[string]any)
s.Require().True(ok, "Expected properties to be map[string]any")
- s.Empty(properties, "Expected namespaces_list.InputSchema.properties to be empty")
+ s.Empty(properties, "Expected projects_list.InputSchema.properties to be empty")
})
// https://github.com/containers/kubernetes-mcp-server/issues/717
// Verifies ALL tools have Properties initialized (not just cluster-aware ones)
diff --git a/pkg/toolsets/core/events.go b/pkg/toolsets/core/events.go
index c429f3a19..546c2ddac 100644
--- a/pkg/toolsets/core/events.go
+++ b/pkg/toolsets/core/events.go
@@ -23,6 +23,11 @@ func initEvents() []api.ServerTool {
Type: "string",
Description: "Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces",
},
+ "fieldSelector": {
+ Type: "string",
+ Description: "Optional Kubernetes field selector to filter events by field values (e.g. 'type=Warning', 'involvedObject.name=my-pod'). Supported fields: involvedObject.kind, involvedObject.name, involvedObject.namespace, involvedObject.uid, involvedObject.apiVersion, involvedObject.resourceVersion, involvedObject.fieldPath, reason, reportingComponent, source, type. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
+ Pattern: REGEX_FIELDSELECTOR,
+ },
},
},
Annotations: api.ToolAnnotations{
@@ -38,10 +43,12 @@ func initEvents() []api.ServerTool {
func eventsList(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
p := api.WrapParams(params)
namespace := p.OptionalString("namespace", "")
+ options := api.ListOptions{}
+ options.FieldSelector = p.OptionalString("fieldSelector", "")
if err := p.Err(); err != nil {
return api.NewToolCallResult("", fmt.Errorf("failed to list events in all namespaces: %w", err)), nil
}
- eventMap, err := kubernetes.NewCore(params).EventsList(params, namespace)
+ eventMap, err := kubernetes.NewCore(params).EventsList(params, namespace, options)
if err != nil {
return api.NewToolCallResult("", fmt.Errorf("failed to list events in all namespaces: %w", err)), nil
}
diff --git a/pkg/toolsets/core/namespaces.go b/pkg/toolsets/core/namespaces.go
index 1538cbe0e..9e6e9ce22 100644
--- a/pkg/toolsets/core/namespaces.go
+++ b/pkg/toolsets/core/namespaces.go
@@ -19,6 +19,13 @@ func initNamespaces(o api.Openshift) []api.ServerTool {
Description: "List all the Kubernetes namespaces in the current cluster",
InputSchema: &jsonschema.Schema{
Type: "object",
+ Properties: map[string]*jsonschema.Schema{
+ "fieldSelector": {
+ Type: "string",
+ Description: "Optional Kubernetes field selector to filter namespaces by field values (e.g. 'metadata.name=default', 'status.phase=Active'). Supported fields: metadata.name, status.phase. See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/",
+ Pattern: REGEX_FIELDSELECTOR,
+ },
+ },
},
Annotations: api.ToolAnnotations{
Title: "Namespaces: List",
@@ -49,7 +56,13 @@ func initNamespaces(o api.Openshift) []api.ServerTool {
}
func namespacesList(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
- ret, err := kubernetes.NewCore(params).NamespacesList(params, api.ListOptions{AsTable: params.ListOutput.AsTable()})
+ p := api.WrapParams(params)
+ options := api.ListOptions{AsTable: params.ListOutput.AsTable()}
+ options.FieldSelector = p.OptionalString("fieldSelector", "")
+ if err := p.Err(); err != nil {
+ return api.NewToolCallResult("", fmt.Errorf("failed to list namespaces: %w", err)), nil
+ }
+ ret, err := kubernetes.NewCore(params).NamespacesList(params, options)
if err != nil {
return api.NewToolCallResult("", fmt.Errorf("failed to list namespaces: %w", err)), nil
}
diff --git a/pkg/toolsets/core/regex_test.go b/pkg/toolsets/core/regex_test.go
index 3811a7214..7ad0e8d83 100644
--- a/pkg/toolsets/core/regex_test.go
+++ b/pkg/toolsets/core/regex_test.go
@@ -38,6 +38,8 @@ var validFieldSelectors = []struct{ selector string }{
{"status.phase=Running"},
{"metadata.namespace!=default"},
{"metadata.name=my-service"},
+ {"involvedObject.apiVersion=apps/v1"},
+ {"status.phase=Running,metadata.name=my-pod"},
}
func Test_FieldSelectorRegex_is_valid(t *testing.T) {
@@ -49,3 +51,24 @@ func Test_FieldSelectorRegex_is_valid(t *testing.T) {
})
}
}
+
+var invalidFieldSelectors = []struct{ selector string }{
+ {""},
+ {" "},
+ {"metadata.name"},
+ {"=my-service"},
+ {"metadata.name =my-service"},
+ {"metadata.name= my-service"},
+ {"metadata.name=my service"},
+ {"!!!"},
+}
+
+func Test_FieldSelectorRegex_is_invalid(t *testing.T) {
+ for _, tc := range invalidFieldSelectors {
+ t.Run(fmt.Sprint("Selector should be invalid: ", tc.selector), func(t *testing.T) {
+ if match, _ := regexp.MatchString(core.REGEX_FIELDSELECTOR, tc.selector); match {
+ t.Errorf("Pattern %s should not match invalid selector: %s", core.REGEX_FIELDSELECTOR, tc.selector)
+ }
+ })
+ }
+}
diff --git a/pkg/toolsets/core/toolset.go b/pkg/toolsets/core/toolset.go
index c8d9197d6..ec74b73f2 100644
--- a/pkg/toolsets/core/toolset.go
+++ b/pkg/toolsets/core/toolset.go
@@ -11,7 +11,7 @@ import (
const REGEX_LABELSELECTOR_VALID_CHARS = "^([/_.\\-A-Za-z0-9=, ()!])+$"
// Details: https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/
-const REGEX_FIELDSELECTOR = "^[.\\-A-Za-z0-9]+([=!,]{1,2}[.\\-A-Za-z0-9]+)+$"
+const REGEX_FIELDSELECTOR = "^[.\\-A-Za-z0-9]+([=!,]{1,2}[./\\-A-Za-z0-9]+)+$"
type Toolset struct{}
diff --git a/pkg/toolsets/kubevirt/vm_troubleshoot.go b/pkg/toolsets/kubevirt/vm_troubleshoot.go
index b9467bf8b..ef9ce1fa4 100644
--- a/pkg/toolsets/kubevirt/vm_troubleshoot.go
+++ b/pkg/toolsets/kubevirt/vm_troubleshoot.go
@@ -358,31 +358,40 @@ func fetchVirtLauncherPodLogs(ctx context.Context, client api.KubernetesClient,
// fetchEvents fetches events related to the VM and returns them formatted
func fetchEvents(ctx context.Context, client api.KubernetesClient, namespace, vmName string) string {
core := kubernetes.NewCore(client)
- eventMap, err := core.EventsList(ctx, namespace)
+
+ // Server-side filter: events whose involvedObject.name exactly matches the VM name
+ vmEvents, err := core.EventsList(ctx, namespace, api.ListOptions{
+ ListOptions: metav1.ListOptions{FieldSelector: "involvedObject.name=" + vmName},
+ })
if err != nil {
return fmt.Sprintf("### Events\n\n*Error listing events: %v*", err)
}
- if len(eventMap) == 0 {
- return "### Events\n\n*No events found in namespace*"
- }
-
- // Filter events related to the VM
var relatedEvents []map[string]any
- for _, event := range eventMap {
+ for _, event := range vmEvents {
involvedObj, ok := event["InvolvedObject"].(map[string]string)
if !ok {
continue
}
- objName := involvedObj["Name"]
objKind := involvedObj["Kind"]
-
// Include events for VM, VMI
- if objName == vmName && (objKind == "VirtualMachine" || objKind == "VirtualMachineInstance") {
+ if objKind == "VirtualMachine" || objKind == "VirtualMachineInstance" {
relatedEvents = append(relatedEvents, event)
- continue
}
+ }
+ // Prefix-based matches (virt-launcher pods, vmName-suffixed objects) can't be expressed
+ // as a field selector; list all events in the namespace and filter client-side.
+ allEvents, err := core.EventsList(ctx, namespace, api.ListOptions{})
+ if err != nil {
+ return fmt.Sprintf("### Events\n\n*Error listing events: %v*", err)
+ }
+ for _, event := range allEvents {
+ involvedObj, ok := event["InvolvedObject"].(map[string]string)
+ if !ok {
+ continue
+ }
+ objName := involvedObj["Name"]
// Include events for pods with VM name prefix
if strings.HasPrefix(objName, vmName+"-") || strings.HasPrefix(objName, "virt-launcher-"+vmName) {
relatedEvents = append(relatedEvents, event)