diff --git a/pkg/mcp/testdata/toolsets-core-tools.json b/pkg/mcp/testdata/toolsets-core-tools.json index 4244a9534..2d43cc2d1 100644 --- a/pkg/mcp/testdata/toolsets-core-tools.json +++ b/pkg/mcp/testdata/toolsets-core-tools.json @@ -456,6 +456,10 @@ "description": "apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)", "type": "string" }, + "clean_metadata": { + "description": "If true, strips verbose metadata (managedFields, resourceVersion, uid, generation, last-applied-configuration) from the output. Useful for diagnostics where only spec/status matter.", + "type": "boolean" + }, "kind": { "description": "kind of the resource (examples of valid kind are: Pod, Service, Deployment, Ingress)", "type": "string" @@ -493,6 +497,10 @@ "description": "apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)", "type": "string" }, + "clean_metadata": { + "description": "If true, strips verbose metadata (managedFields, resourceVersion, uid, generation, last-applied-configuration) from the output. Useful for diagnostics where only spec/status matter.", + "type": "boolean" + }, "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]+)+$", diff --git a/pkg/mcp/testdata/toolsets-full-tools-multicluster-enum.json b/pkg/mcp/testdata/toolsets-full-tools-multicluster-enum.json index 75a1f2eef..9bf5e75be 100644 --- a/pkg/mcp/testdata/toolsets-full-tools-multicluster-enum.json +++ b/pkg/mcp/testdata/toolsets-full-tools-multicluster-enum.json @@ -723,6 +723,10 @@ "description": "apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)", "type": "string" }, + "clean_metadata": { + "description": "If true, strips verbose metadata (managedFields, resourceVersion, uid, generation, last-applied-configuration) from the output. Useful for diagnostics where only spec/status matter.", + "type": "boolean" + }, "context": { "description": "Optional parameter selecting which context to run the tool in. Defaults to fake-context if not set", "enum": [ @@ -768,6 +772,10 @@ "description": "apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)", "type": "string" }, + "clean_metadata": { + "description": "If true, strips verbose metadata (managedFields, resourceVersion, uid, generation, last-applied-configuration) from the output. Useful for diagnostics where only spec/status matter.", + "type": "boolean" + }, "context": { "description": "Optional parameter selecting which context to run the tool in. Defaults to fake-context if not set", "enum": [ diff --git a/pkg/mcp/testdata/toolsets-full-tools-multicluster.json b/pkg/mcp/testdata/toolsets-full-tools-multicluster.json index 849703a67..ad96d98f7 100644 --- a/pkg/mcp/testdata/toolsets-full-tools-multicluster.json +++ b/pkg/mcp/testdata/toolsets-full-tools-multicluster.json @@ -651,6 +651,10 @@ "description": "apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)", "type": "string" }, + "clean_metadata": { + "description": "If true, strips verbose metadata (managedFields, resourceVersion, uid, generation, last-applied-configuration) from the output. Useful for diagnostics where only spec/status matter.", + "type": "boolean" + }, "context": { "description": "Optional parameter selecting which context to run the tool in. Defaults to fake-context if not set", "type": "string" @@ -692,6 +696,10 @@ "description": "apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)", "type": "string" }, + "clean_metadata": { + "description": "If true, strips verbose metadata (managedFields, resourceVersion, uid, generation, last-applied-configuration) from the output. Useful for diagnostics where only spec/status matter.", + "type": "boolean" + }, "context": { "description": "Optional parameter selecting which context to run the tool in. Defaults to fake-context if not set", "type": "string" diff --git a/pkg/mcp/testdata/toolsets-full-tools-openshift.json b/pkg/mcp/testdata/toolsets-full-tools-openshift.json index 9dbd1e09d..9e0d497e3 100644 --- a/pkg/mcp/testdata/toolsets-full-tools-openshift.json +++ b/pkg/mcp/testdata/toolsets-full-tools-openshift.json @@ -577,6 +577,10 @@ "description": "apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)", "type": "string" }, + "clean_metadata": { + "description": "If true, strips verbose metadata (managedFields, resourceVersion, uid, generation, last-applied-configuration) from the output. Useful for diagnostics where only spec/status matter.", + "type": "boolean" + }, "kind": { "description": "kind of the resource (examples of valid kind are: Pod, Service, Deployment, Ingress)", "type": "string" @@ -614,6 +618,10 @@ "description": "apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)", "type": "string" }, + "clean_metadata": { + "description": "If true, strips verbose metadata (managedFields, resourceVersion, uid, generation, last-applied-configuration) from the output. Useful for diagnostics where only spec/status matter.", + "type": "boolean" + }, "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]+)+$", diff --git a/pkg/mcp/testdata/toolsets-full-tools.json b/pkg/mcp/testdata/toolsets-full-tools.json index 8db8967b1..4a80a1e44 100644 --- a/pkg/mcp/testdata/toolsets-full-tools.json +++ b/pkg/mcp/testdata/toolsets-full-tools.json @@ -562,6 +562,10 @@ "description": "apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)", "type": "string" }, + "clean_metadata": { + "description": "If true, strips verbose metadata (managedFields, resourceVersion, uid, generation, last-applied-configuration) from the output. Useful for diagnostics where only spec/status matter.", + "type": "boolean" + }, "kind": { "description": "kind of the resource (examples of valid kind are: Pod, Service, Deployment, Ingress)", "type": "string" @@ -599,6 +603,10 @@ "description": "apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)", "type": "string" }, + "clean_metadata": { + "description": "If true, strips verbose metadata (managedFields, resourceVersion, uid, generation, last-applied-configuration) from the output. Useful for diagnostics where only spec/status matter.", + "type": "boolean" + }, "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]+)+$", diff --git a/pkg/output/output.go b/pkg/output/output.go index c558ae9df..892a01f81 100644 --- a/pkg/output/output.go +++ b/pkg/output/output.go @@ -95,22 +95,24 @@ func (p *table) PrintObj(obj runtime.Unstructured) (string, error) { return buf.String(), err } -func MarshalYaml(v any) (string, error) { +func MarshalYaml(v any, opts ...MarshalOption) (string, error) { + var cfg marshalConfig + for _, o := range opts { + o(&cfg) + } + if cfg.clean { + switch t := v.(type) { + case *unstructured.UnstructuredList: + for i := range t.Items { + cleanMetadata(&t.Items[i]) + } + case *unstructured.Unstructured: + cleanMetadata(t) + } + } switch t := v.(type) { - //case unstructured.UnstructuredList: - // for i := range t.Items { - // t.Items[i].SetManagedFields(nil) - // } - // v = t.Items case *unstructured.UnstructuredList: - for i := range t.Items { - t.Items[i].SetManagedFields(nil) - } v = t.Items - //case unstructured.Unstructured: - // t.SetManagedFields(nil) - case *unstructured.Unstructured: - t.SetManagedFields(nil) } ret, err := yml.Marshal(v) if err != nil { @@ -119,6 +121,37 @@ func MarshalYaml(v any) (string, error) { return string(ret), nil } +type marshalConfig struct { + clean bool +} + +// MarshalOption configures MarshalYaml behaviour. +type MarshalOption func(*marshalConfig) + +// WithCleanMetadata strips verbose metadata (managedFields, resourceVersion, uid, etc.) +// that provides no diagnostic value to the LLM. +func WithCleanMetadata() MarshalOption { + return func(c *marshalConfig) { c.clean = true } +} + +// cleanMetadata strips verbose metadata that provides no diagnostic value to the LLM. +func cleanMetadata(obj *unstructured.Unstructured) { + obj.SetManagedFields(nil) + obj.SetResourceVersion("") + obj.SetUID("") + obj.SetGeneration(0) + + annotations := obj.GetAnnotations() + if annotations != nil { + delete(annotations, "kubectl.kubernetes.io/last-applied-configuration") + if len(annotations) == 0 { + obj.SetAnnotations(nil) + } else { + obj.SetAnnotations(annotations) + } + } +} + func init() { Names = make([]string, 0) for _, output := range Outputs { diff --git a/pkg/toolsets/core/resources.go b/pkg/toolsets/core/resources.go index cfefdd396..ec29c6921 100644 --- a/pkg/toolsets/core/resources.go +++ b/pkg/toolsets/core/resources.go @@ -39,6 +39,10 @@ func initResources(o api.Openshift) []api.ServerTool { Type: "string", 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", }, + "clean_metadata": { + Type: "boolean", + Description: "If true, strips verbose metadata (managedFields, resourceVersion, uid, generation, last-applied-configuration) from the output. Useful for diagnostics where only spec/status matter.", + }, "labelSelector": { Type: "string", 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 resources by label", @@ -77,6 +81,10 @@ func initResources(o api.Openshift) []api.ServerTool { Type: "string", 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", }, + "clean_metadata": { + Type: "boolean", + Description: "If true, strips verbose metadata (managedFields, resourceVersion, uid, generation, last-applied-configuration) from the output. Useful for diagnostics where only spec/status matter.", + }, "name": { Type: "string", Description: "Name of the resource", @@ -225,7 +233,14 @@ func resourcesList(params api.ToolHandlerParams) (*api.ToolCallResult, error) { if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to list resources: %w", err)), nil } - return api.NewToolCallResult(params.ListOutput.PrintObj(ret)), nil + if params.ListOutput.AsTable() { + return api.NewToolCallResult(params.ListOutput.PrintObj(ret)), nil + } + var marshalOpts []output.MarshalOption + if clean, _ := params.GetArguments()["clean_metadata"].(bool); clean { + marshalOpts = append(marshalOpts, output.WithCleanMetadata()) + } + return api.NewToolCallResult(output.MarshalYaml(ret, marshalOpts...)), nil } func resourcesGet(params api.ToolHandlerParams) (*api.ToolCallResult, error) { @@ -256,7 +271,11 @@ func resourcesGet(params api.ToolHandlerParams) (*api.ToolCallResult, error) { if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to get resource: %w", err)), nil } - return api.NewToolCallResult(output.MarshalYaml(ret)), nil + var marshalOpts []output.MarshalOption + if clean, _ := params.GetArguments()["clean_metadata"].(bool); clean { + marshalOpts = append(marshalOpts, output.WithCleanMetadata()) + } + return api.NewToolCallResult(output.MarshalYaml(ret, marshalOpts...)), nil } func resourcesCreateOrUpdate(params api.ToolHandlerParams) (*api.ToolCallResult, error) {