Skip to main content
Glama

kubernetes-mcp

by kkb0318
list.go16.9 kB
package tools import ( "context" "encoding/json" "errors" "fmt" "strings" "github.com/kkb0318/kubernetes-mcp/src/validation" "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" ) // ListResourcesInput represents the input parameters for listing Kubernetes resources. type ListResourcesInput struct { Context string `json:"context,omitempty"` Kind string `json:"kind"` GroupFilter string `json:"groupFilter,omitempty"` Namespace string `json:"namespace,omitempty"` LabelSelector string `json:"labelSelector,omitempty"` FieldSelector string `json:"fieldSelector,omitempty"` Limit int64 `json:"limit,omitempty"` TimeoutSeconds int64 `json:"timeoutSeconds,omitempty"` ShowDetails bool `json:"showDetails,omitempty"` } // ResourceWithStatus represents a resource with its status information extracted. type ResourceWithStatus struct { Name string `json:"name"` Namespace string `json:"namespace,omitempty"` Kind string `json:"kind"` Status any `json:"status,omitempty"` } // ListTool provides functionality to list Kubernetes resources by kind. type ListTool struct { multiClient MultiClusterClientInterface } // NewListTool creates a new ListTool instance with the provided MultiClusterClient. func NewListTool(multiClient MultiClusterClientInterface) ListTool { return ListTool{multiClient: multiClient} } // Tool returns the MCP tool definition for listing Kubernetes resources. func (l ListTool) Tool() mcp.Tool { return mcp.NewTool("list_resources", mcp.WithDescription("List Kubernetes resources with their status information by default, with advanced filtering options"), mcp.WithString("context", mcp.Description("Kubernetes context name from kubeconfig to use for this request (leave empty for current context)"), ), mcp.WithString("kind", mcp.Description("Kind of the Kubernetes resource, e.g., Pod, Deployment, Service, ConfigMap, or any CRD. Use 'all' with groupFilter to discover all resource types for a project."), ), mcp.WithString("groupFilter", mcp.Description("Filter by API group substring to discover all resources from a project (e.g., 'flux' for FluxCD, 'argo' for ArgoCD, 'istio' for Istio). When used with kind='all', returns all matching resource types."), ), mcp.WithString("namespace", mcp.Description("Kubernetes namespace to list resources from (leave empty for all namespaces, use 'default' for default namespace)"), ), mcp.WithString("labelSelector", mcp.Description("Filter resources by label selector (e.g., 'app=nginx', 'tier=frontend,environment!=prod')"), ), mcp.WithString("fieldSelector", mcp.Description("Filter resources by field selector (e.g., 'metadata.name=my-pod', 'spec.nodeName=node1')"), ), mcp.WithNumber("limit", mcp.Description("Maximum number of resources to return (useful for large clusters, default: no limit)"), ), mcp.WithNumber("timeoutSeconds", mcp.Description("Timeout for the list operation in seconds (default: 30)"), ), mcp.WithBoolean("showDetails", mcp.Description("Return complete resource objects instead of just name and status (default: false)"), ), ) } // Handler processes requests to list Kubernetes resources by kind and namespace. func (l ListTool) Handler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { input, err := parseAndValidateListParams(req.Params.Arguments) if err != nil { return nil, err } // Get the appropriate client for the context client, err := l.multiClient.GetClient(input.Context) if err != nil { return nil, fmt.Errorf("failed to get client for context '%s': %w", input.Context, err) } // Handle groupFilter functionality for discovering resources if input.GroupFilter != "" { if input.Kind == "all" || input.Kind == "" { // Discovery mode: return all resource types for the group return l.handleGroupDiscovery(client, input.GroupFilter) } else { // Filter mode: find specific kind within the group return l.handleGroupFilteredList(ctx, client, input) } } // Original functionality for specific kind gvrMatch, err := l.discoverResourceByKind(client, input.Kind) if err != nil { return nil, err } if input.ShowDetails { // Return full resource details (complete objects) resources, err := l.listResourceDetails(ctx, client, gvrMatch, input) if err != nil { return nil, err } out, err := json.Marshal(resources) if err != nil { return nil, fmt.Errorf("failed to marshal resource details: %w", err) } return mcp.NewToolResultText(string(out)), nil } else { // Default: Return resources with status information resourcesWithStatus, err := l.listResourcesWithStatus(ctx, client, gvrMatch, input) if err != nil { return nil, err } out, err := json.Marshal(resourcesWithStatus) if err != nil { return nil, fmt.Errorf("failed to marshal resources with status: %w", err) } return mcp.NewToolResultText(string(out)), nil } } // handleGroupDiscovery returns all available resource types for a given group filter func (l ListTool) handleGroupDiscovery(client Client, groupFilter string) (*mcp.CallToolResult, error) { discoClient, err := client.DiscoClient() if err != nil { return nil, fmt.Errorf("failed to create discovery client: %w", err) } apiResourceLists, err := discoClient.ServerPreferredResources() if err != nil { return nil, fmt.Errorf("failed to discover resources: %w", err) } matches, err := findGVRsByGroupSubstring(apiResourceLists, groupFilter) if err != nil { return nil, fmt.Errorf("failed to find resources by group substring: %w", err) } if len(matches) == 0 { return mcp.NewToolResultText(fmt.Sprintf(`{"message": "No resources found for group filter '%s'", "availableResources": []}`, groupFilter)), nil } // Format the discovered resource types discoveredTypes := make([]map[string]any, 0) for _, match := range matches { discoveredTypes = append(discoveredTypes, map[string]any{ "kind": match.apiRes.Kind, "group": match.groupVersion, "resource": match.apiRes.Name, "namespaced": match.namespaced, "shortNames": match.apiRes.ShortNames, }) } result := map[string]any{ "groupFilter": groupFilter, "discoveredTypes": discoveredTypes, "totalFound": len(matches), "message": fmt.Sprintf("Found %d resource types matching group filter '%s'", len(matches), groupFilter), } out, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal discovery result: %w", err) } return mcp.NewToolResultText(string(out)), nil } // handleGroupFilteredList lists resources of a specific kind within a filtered group func (l ListTool) handleGroupFilteredList(ctx context.Context, client Client, input *ListResourcesInput) (*mcp.CallToolResult, error) { discoClient, err := client.DiscoClient() if err != nil { return nil, fmt.Errorf("failed to create discovery client: %w", err) } apiResourceLists, err := discoClient.ServerPreferredResources() if err != nil { return nil, fmt.Errorf("failed to discover resources: %w", err) } // First find all resources in the group matches, err := findGVRsByGroupSubstring(apiResourceLists, input.GroupFilter) if err != nil { return nil, fmt.Errorf("failed to find resources by group substring: %w", err) } // Find the specific kind within the group var gvrMatch *gvrMatch kindLower := strings.ToLower(input.Kind) for _, match := range matches { if strings.ToLower(match.apiRes.Kind) == kindLower || strings.ToLower(match.apiRes.Name) == kindLower { gvrMatch = match break } // Check short names too for _, shortName := range match.apiRes.ShortNames { if strings.ToLower(shortName) == kindLower { gvrMatch = match break } } if gvrMatch != nil { break } } if gvrMatch == nil { return nil, fmt.Errorf("kind '%s' not found in group filter '%s'", input.Kind, input.GroupFilter) } // Now list the resources using the found GVR if input.ShowDetails { resources, err := l.listResourceDetails(ctx, client, gvrMatch, input) if err != nil { return nil, err } out, err := json.Marshal(resources) if err != nil { return nil, fmt.Errorf("failed to marshal resource details: %w", err) } return mcp.NewToolResultText(string(out)), nil } else { resourcesWithStatus, err := l.listResourcesWithStatus(ctx, client, gvrMatch, input) if err != nil { return nil, err } out, err := json.Marshal(resourcesWithStatus) if err != nil { return nil, fmt.Errorf("failed to marshal resources with status: %w", err) } return mcp.NewToolResultText(string(out)), nil } } // discoverResourceByKind discovers and returns the GroupVersionResource match for a given kind. func (l ListTool) discoverResourceByKind(client Client, kind string) (*gvrMatch, error) { discoClient, err := client.DiscoClient() if err != nil { return nil, fmt.Errorf("failed to create discovery client: %w", err) } apiResourceLists, err := discoClient.ServerPreferredResources() if err != nil { return nil, fmt.Errorf("failed to discover resources: %w", err) } return findGVRByKind(apiResourceLists, kind) } // listResourceDetails retrieves full details of all resources matching the given GVR and input parameters. func (l ListTool) listResourceDetails(ctx context.Context, client Client, gvrMatch *gvrMatch, input *ListResourcesInput) (any, error) { ri, err := client.ResourceInterface(*gvrMatch.ToGroupVersionResource(), gvrMatch.namespaced, input.Namespace) if err != nil { return nil, fmt.Errorf("failed to create resource interface: %w", err) } listOptions := l.buildListOptions(input) unstructList, err := ri.List(ctx, listOptions) if err != nil { return nil, fmt.Errorf("failed to list resources: %w", err) } return unstructList, nil } // buildListOptions creates metav1.ListOptions from the input parameters. func (l ListTool) buildListOptions(input *ListResourcesInput) metav1.ListOptions { listOptions := metav1.ListOptions{ LabelSelector: input.LabelSelector, FieldSelector: input.FieldSelector, } if input.Limit > 0 { listOptions.Limit = input.Limit } listOptions.TimeoutSeconds = &input.TimeoutSeconds return listOptions } // listResourcesWithStatus retrieves resources and extracts their status information. func (l ListTool) listResourcesWithStatus(ctx context.Context, client Client, gvrMatch *gvrMatch, input *ListResourcesInput) ([]ResourceWithStatus, error) { ri, err := client.ResourceInterface(*gvrMatch.ToGroupVersionResource(), gvrMatch.namespaced, input.Namespace) if err != nil { return nil, fmt.Errorf("failed to create resource interface: %w", err) } listOptions := l.buildListOptions(input) unstructList, err := ri.List(ctx, listOptions) if err != nil { return nil, fmt.Errorf("failed to list resources: %w", err) } var resourcesWithStatus []ResourceWithStatus for _, item := range unstructList.Items { resourceWithStatus := l.extractResourceStatus(&item) resourcesWithStatus = append(resourcesWithStatus, resourceWithStatus) } return resourcesWithStatus, nil } // extractResourceStatus extracts the status section from a resource. func (l ListTool) extractResourceStatus(obj *unstructured.Unstructured) ResourceWithStatus { resource := ResourceWithStatus{ Name: obj.GetName(), Namespace: obj.GetNamespace(), Kind: obj.GetKind(), } // Extract the entire status section if status, found, err := unstructured.NestedMap(obj.Object, "status"); found && err == nil { resource.Status = status } return resource } // parseAndValidateListParams validates and extracts parameters from request arguments. func parseAndValidateListParams(args map[string]any) (*ListResourcesInput, error) { input := &ListResourcesInput{} // Optional: context if context, ok := args["context"].(string); ok { input.Context = context } // Optional: groupFilter if groupFilter, ok := args["groupFilter"].(string); ok { input.GroupFilter = groupFilter } // Kind: Required unless groupFilter is used for discovery if kindVal, ok := args["kind"].(string); ok && kindVal != "" { input.Kind = kindVal if err := validation.ValidateKind(input.Kind); err != nil { return nil, fmt.Errorf("invalid kind: %w", err) } } else if input.GroupFilter == "" { return nil, errors.New("kind must be provided when groupFilter is not specified") } // Optional: namespace if ns, ok := args["namespace"].(string); ok { input.Namespace = ns if err := validation.ValidateNamespace(input.Namespace); err != nil { return nil, fmt.Errorf("invalid namespace: %w", err) } } if input.Namespace == "" { input.Namespace = metav1.NamespaceAll } // Optional: labelSelector if labelSelector, ok := args["labelSelector"].(string); ok { input.LabelSelector = labelSelector if err := validation.ValidateLabelSelector(input.LabelSelector); err != nil { return nil, fmt.Errorf("invalid labelSelector: %w", err) } } // Optional: fieldSelector if fieldSelector, ok := args["fieldSelector"].(string); ok { input.FieldSelector = fieldSelector } // Optional: limit if limit, ok := args["limit"].(float64); ok && limit > 0 { input.Limit = int64(limit) } // Optional: timeoutSeconds if timeoutSeconds, ok := args["timeoutSeconds"].(float64); ok && timeoutSeconds > 0 { input.TimeoutSeconds = int64(timeoutSeconds) } else { // Default timeout of 30 seconds input.TimeoutSeconds = 30 } // Optional: showDetails if showDetails, ok := args["showDetails"].(bool); ok { input.ShowDetails = showDetails } return input, nil } // gvrMatchList is a collection of GroupVersionResource matches. type gvrMatchList []*gvrMatch // ToGroupVersionResources converts the match list to a slice of GroupVersionResource pointers. func (f *gvrMatchList) ToGroupVersionResources() []*schema.GroupVersionResource { var gvrList []*schema.GroupVersionResource for _, found := range *f { gvr := found.ToGroupVersionResource() if gvr == nil { continue } gvrList = append(gvrList, gvr) } return gvrList } // gvrMatch represents a matched Kubernetes API resource with its group/version and namespacing info. type gvrMatch struct { apiRes *metav1.APIResource groupVersion string namespaced bool } // newGvrMatch creates a new gvrMatch instance. func newGvrMatch(apiRes *metav1.APIResource, groupVersion string, namespaced bool) *gvrMatch { return &gvrMatch{ apiRes, groupVersion, namespaced, } } // ToGroupVersionResource converts the match to a GroupVersionResource. Returns nil if invalid. func (f *gvrMatch) ToGroupVersionResource() *schema.GroupVersionResource { if f.groupVersion == "" { return nil } if f.apiRes == nil { return nil } parts := strings.Split(f.groupVersion, "/") var group, version string if len(parts) == 1 { group = "" version = parts[0] } else { group = parts[0] version = parts[1] } gvr := &schema.GroupVersionResource{ Group: group, Version: version, Resource: f.apiRes.Name, } return gvr } // findGVRsByGroupSubstring finds all resources whose group contains the specified substring (case-insensitive). func findGVRsByGroupSubstring(apiResourceLists []*metav1.APIResourceList, groupSubstring string) (gvrMatchList, error) { target := strings.ToLower(groupSubstring) var matches gvrMatchList for _, apiResList := range apiResourceLists { if apiResList == nil { continue } gv := apiResList.GroupVersion if !strings.Contains(gv, target) { continue } for _, r := range apiResList.APIResources { matches = append(matches, newGvrMatch(&r, gv, r.Namespaced)) } } return matches, nil } // findGVRByKind finds a resource by matching against plural name, Kind, or short names (case-insensitive). func findGVRByKind(apiResourceLists []*metav1.APIResourceList, kind string) (*gvrMatch, error) { target := strings.ToLower(kind) var found *gvrMatch for _, apiResList := range apiResourceLists { if apiResList == nil { continue } gv := apiResList.GroupVersion for _, r := range apiResList.APIResources { nameLower := strings.ToLower(r.Name) kindLower := strings.ToLower(r.Kind) if nameLower == target || kindLower == target { found = newGvrMatch(&r, gv, r.Namespaced) break } for _, sn := range r.ShortNames { if strings.ToLower(sn) == target { found = newGvrMatch(&r, gv, r.Namespaced) break } } if found != nil { break } } if found != nil { break } } if found == nil { return nil, fmt.Errorf("cannot find resource '%s'", kind) } if found.ToGroupVersionResource() == nil { return nil, fmt.Errorf("cannot find resource '%s'", kind) } return found, nil }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kkb0318/kubernetes-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server