Skip to content

Commit 06d03a3

Browse files
authored
Merge pull request #12 from StacklokLabs/minimize-list-resources-output
Minimize list_resources output by returning only metadata
2 parents c60b134 + 0418a9b commit 06d03a3

File tree

2 files changed

+165
-3
lines changed

2 files changed

+165
-3
lines changed

pkg/mcp/list_resources.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88

99
"github.com/mark3labs/mcp-go/mcp"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1011
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1112
"k8s.io/apimachinery/pkg/runtime/schema"
1213
)
@@ -57,8 +58,47 @@ func (m *Implementation) HandleListResources(ctx context.Context, request mcp.Ca
5758
return mcp.NewToolResultErrorFromErr("Failed to list resources", err), nil
5859
}
5960

61+
// Convert to PartialObjectMetadataList
62+
metadataList := &metav1.PartialObjectMetadataList{
63+
TypeMeta: metav1.TypeMeta{
64+
Kind: "PartialObjectMetadataList",
65+
APIVersion: "meta.k8s.io/v1",
66+
},
67+
ListMeta: metav1.ListMeta{
68+
ResourceVersion: list.GetResourceVersion(),
69+
},
70+
Items: make([]metav1.PartialObjectMetadata, 0, len(list.Items)),
71+
}
72+
73+
// Extract metadata from each resource
74+
for _, item := range list.Items {
75+
// Get annotations and filter out the last-applied-configuration annotation
76+
annotations := item.GetAnnotations()
77+
if annotations != nil {
78+
delete(annotations, "kubectl.kubernetes.io/last-applied-configuration")
79+
}
80+
81+
metadata := metav1.PartialObjectMetadata{
82+
TypeMeta: metav1.TypeMeta{
83+
Kind: item.GetKind(),
84+
APIVersion: item.GetAPIVersion(),
85+
},
86+
ObjectMeta: metav1.ObjectMeta{
87+
Name: item.GetName(),
88+
Namespace: item.GetNamespace(),
89+
Labels: item.GetLabels(),
90+
Annotations: annotations,
91+
ResourceVersion: item.GetResourceVersion(),
92+
UID: item.GetUID(),
93+
CreationTimestamp: item.GetCreationTimestamp(),
94+
},
95+
}
96+
97+
metadataList.Items = append(metadataList.Items, metadata)
98+
}
99+
60100
// Convert to JSON
61-
result, err := json.Marshal(list)
101+
result, err := json.Marshal(metadataList)
62102
if err != nil {
63103
return mcp.NewToolResultErrorFromErr("Failed to marshal result", err), nil
64104
}

pkg/mcp/list_resources_test.go

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,13 @@ func TestHandleListResourcesClusteredSuccess(t *testing.T) {
8787
// Verify the result is successful
8888
assert.False(t, result.IsError, "Result should not be an error")
8989

90-
// Verify the result contains the resource name
90+
// Verify the result contains the resource name in a PartialObjectMetadataList
9191
textContent, ok := mcp.AsTextContent(result.Content[0])
9292
assert.True(t, ok, "Content should be TextContent")
9393
assert.Contains(t, textContent.Text, "test-cluster-role", "Result should contain the resource name")
94+
assert.Contains(t, textContent.Text, "meta.k8s.io/v1", "Result should contain the meta.k8s.io/v1 API version")
95+
assert.Contains(t, textContent.Text, "PartialObjectMetadataList", "Result should be a PartialObjectMetadataList")
96+
assert.NotContains(t, textContent.Text, "rules", "Result should not contain the rules field")
9497
}
9598

9699
func TestHandleListResourcesNamespacedSuccess(t *testing.T) {
@@ -164,10 +167,13 @@ func TestHandleListResourcesNamespacedSuccess(t *testing.T) {
164167
// Verify the result is successful
165168
assert.False(t, result.IsError, "Result should not be an error")
166169

167-
// Verify the result contains the resource name
170+
// Verify the result contains the resource name in a PartialObjectMetadataList
168171
textContent, ok := mcp.AsTextContent(result.Content[0])
169172
assert.True(t, ok, "Content should be TextContent")
170173
assert.Contains(t, textContent.Text, "test-service", "Result should contain the service name")
174+
assert.Contains(t, textContent.Text, "meta.k8s.io/v1", "Result should contain the meta.k8s.io/v1 API version")
175+
assert.Contains(t, textContent.Text, "PartialObjectMetadataList", "Result should be a PartialObjectMetadataList")
176+
assert.NotContains(t, textContent.Text, "spec", "Result should not contain the spec field")
171177
}
172178

173179
func TestHandleListResourcesMissingParameters(t *testing.T) {
@@ -466,4 +472,120 @@ func TestHandleListAllResourcesError(t *testing.T) {
466472

467473
// Verify the result is nil
468474
assert.Nil(t, resources, "Resources should be nil")
475+
}
476+
477+
func TestHandleListResourcesWithLastAppliedConfig(t *testing.T) {
478+
// Create a mock k8s client
479+
mockClient := &k8s.Client{}
480+
481+
// Create a fake dynamic client
482+
scheme := runtime.NewScheme()
483+
484+
// Register list kinds for the resources we'll be testing
485+
listKinds := map[schema.GroupVersionResource]string{
486+
{Group: "apps", Version: "v1", Resource: "deployments"}: "DeploymentList",
487+
}
488+
489+
fakeDynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds)
490+
491+
// Add a fake list response with the last-applied-configuration annotation
492+
fakeDynamicClient.PrependReactor("list", "deployments", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
493+
// Create a large last-applied-configuration annotation
494+
lastAppliedConfig := `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"test-deployment","namespace":"default"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"test"}},"template":{"metadata":{"labels":{"app":"test"}},"spec":{"containers":[{"name":"test-container","image":"nginx:latest","ports":[{"containerPort":80}]}]}}}}`
495+
496+
list := &unstructured.UnstructuredList{
497+
Items: []unstructured.Unstructured{
498+
{
499+
Object: map[string]interface{}{
500+
"apiVersion": "apps/v1",
501+
"kind": "Deployment",
502+
"metadata": map[string]interface{}{
503+
"name": "test-deployment",
504+
"namespace": "default",
505+
"annotations": map[string]interface{}{
506+
"kubectl.kubernetes.io/last-applied-configuration": lastAppliedConfig,
507+
"deployment.kubernetes.io/revision": "1",
508+
},
509+
},
510+
"spec": map[string]interface{}{
511+
"replicas": int64(3),
512+
"selector": map[string]interface{}{
513+
"matchLabels": map[string]interface{}{
514+
"app": "test",
515+
},
516+
},
517+
"template": map[string]interface{}{
518+
"metadata": map[string]interface{}{
519+
"labels": map[string]interface{}{
520+
"app": "test",
521+
},
522+
},
523+
"spec": map[string]interface{}{
524+
"containers": []interface{}{
525+
map[string]interface{}{
526+
"name": "test-container",
527+
"image": "nginx:latest",
528+
"ports": []interface{}{
529+
map[string]interface{}{
530+
"containerPort": int64(80),
531+
},
532+
},
533+
},
534+
},
535+
},
536+
},
537+
},
538+
},
539+
},
540+
},
541+
}
542+
return true, list, nil
543+
})
544+
545+
// Set the dynamic client
546+
mockClient.SetDynamicClient(fakeDynamicClient)
547+
548+
// Create an implementation
549+
impl := NewImplementation(mockClient)
550+
551+
// Create a test request
552+
request := mcp.CallToolRequest{}
553+
request.Params.Name = "list_resources"
554+
request.Params.Arguments = map[string]interface{}{
555+
"resource_type": "namespaced",
556+
"group": "apps",
557+
"version": "v1",
558+
"resource": "deployments",
559+
"namespace": "default",
560+
}
561+
562+
// Test HandleListResources
563+
ctx := context.Background()
564+
result, err := impl.HandleListResources(ctx, request)
565+
566+
// Verify there was no error
567+
assert.NoError(t, err, "HandleListResources should not return an error")
568+
569+
// Verify the result is not nil
570+
assert.NotNil(t, result, "Result should not be nil")
571+
572+
// Verify the result is successful
573+
assert.False(t, result.IsError, "Result should not be an error")
574+
575+
// Get the text content
576+
textContent, ok := mcp.AsTextContent(result.Content[0])
577+
assert.True(t, ok, "Content should be TextContent")
578+
579+
// Verify the result contains the deployment name
580+
assert.Contains(t, textContent.Text, "test-deployment", "Result should contain the deployment name")
581+
582+
// Verify the result contains the other annotation
583+
assert.Contains(t, textContent.Text, "deployment.kubernetes.io/revision", "Result should contain other annotations")
584+
585+
// Verify the result does not contain the last-applied-configuration annotation
586+
assert.NotContains(t, textContent.Text, "kubectl.kubernetes.io/last-applied-configuration",
587+
"Result should not contain the kubectl.kubernetes.io/last-applied-configuration annotation")
588+
589+
// Verify the result does not contain the spec field
590+
assert.NotContains(t, textContent.Text, "spec", "Result should not contain the spec field")
469591
}

0 commit comments

Comments
 (0)