Skip to main content
Glama

kubernetes-mcp

by kkb0318
list_events.go10.5 kB
package tools import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/kkb0318/kubernetes-mcp/src/validation" "github.com/mark3labs/mcp-go/mcp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" ) // ListEventsInput represents the input parameters for listing Kubernetes events. type ListEventsInput struct { Context string `json:"context,omitempty"` Namespace string `json:"namespace,omitempty"` Object string `json:"object,omitempty"` EventType string `json:"eventType,omitempty"` Reason string `json:"reason,omitempty"` Since string `json:"since,omitempty"` SinceTime string `json:"sinceTime,omitempty"` Limit int64 `json:"limit,omitempty"` TimeoutSeconds int64 `json:"timeoutSeconds,omitempty"` } // EventInfo represents formatted event information for better readability. type EventInfo struct { FirstTimestamp metav1.Time `json:"firstTimestamp"` LastTimestamp metav1.Time `json:"lastTimestamp"` Count int32 `json:"count"` Type string `json:"type"` Reason string `json:"reason"` Object string `json:"object"` Message string `json:"message"` Source string `json:"source,omitempty"` Namespace string `json:"namespace,omitempty"` } // ListEventsTool provides functionality to list Kubernetes events with advanced filtering. type ListEventsTool struct { multiClient MultiClusterClientInterface } // NewListEventsTool creates a new ListEventsTool instance with the provided MultiClusterClient. func NewListEventsTool(multiClient MultiClusterClientInterface) *ListEventsTool { return &ListEventsTool{multiClient: multiClient} } // Tool returns the MCP tool definition for listing Kubernetes events. func (l *ListEventsTool) Tool() mcp.Tool { return mcp.NewTool("list_events", mcp.WithDescription("List Kubernetes events with advanced filtering options for debugging and monitoring"), mcp.WithString("context", mcp.Description("Kubernetes context name from kubeconfig to use for this request (leave empty for current context)"), ), mcp.WithString("namespace", mcp.Description("Kubernetes namespace to list events from (leave empty for all namespaces, use 'default' for default namespace)"), ), mcp.WithString("object", mcp.Description("Filter events by the name of the Kubernetes object (e.g., pod name, deployment name)"), ), mcp.WithString("eventType", mcp.Description("Filter by event type: 'Normal' or 'Warning' (case-insensitive)"), ), mcp.WithString("reason", mcp.Description("Filter by event reason (e.g., 'Pulled', 'Failed', 'FailedScheduling', 'Killing')"), ), mcp.WithString("since", mcp.Description("Return events newer than a relative duration like '5s', '2m', '1h', '24h' (optional)"), ), mcp.WithString("sinceTime", mcp.Description("Return events after a specific time (RFC3339 format, e.g., 2025-06-20T10:00:00Z) (optional)"), ), mcp.WithNumber("limit", mcp.Description("Maximum number of events to return (default: 100, use 0 for no limit)"), ), mcp.WithNumber("timeoutSeconds", mcp.Description("Timeout for the list operation in seconds (default: 30)"), ), ) } // Handler processes requests to list Kubernetes events with filtering options. func (l *ListEventsTool) Handler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { input, err := l.parseAndValidateEventsParams(req.Params.Arguments) if err != nil { return nil, fmt.Errorf("failed to parse and validate events params: %w", 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) } clientset, err := client.Clientset() if err != nil { return nil, fmt.Errorf("failed to get clientset: %w", err) } listOptions := l.buildListOptions(input) var eventList *corev1.EventList if input.Namespace == "" { // List events from all namespaces eventList, err = clientset.CoreV1().Events("").List(ctx, listOptions) } else { // List events from specific namespace eventList, err = clientset.CoreV1().Events(input.Namespace).List(ctx, listOptions) } if err != nil { return nil, fmt.Errorf("failed to list events: %w", err) } // Filter events based on input parameters filteredEvents := l.filterEvents(eventList.Items, input) // Convert to EventInfo format for better readability eventInfos := l.convertToEventInfos(filteredEvents) result := map[string]any{ "events": eventInfos, "total": len(eventInfos), "namespace": input.Namespace, "filters": map[string]any{ "object": input.Object, "eventType": input.EventType, "reason": input.Reason, "since": input.Since, "sinceTime": input.SinceTime, }, } out, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal events: %w", err) } return mcp.NewToolResultText(string(out)), nil } // buildListOptions creates metav1.ListOptions from the input parameters. func (l *ListEventsTool) buildListOptions(input *ListEventsInput) metav1.ListOptions { listOptions := metav1.ListOptions{} // Build field selector for object if specified if input.Object != "" { listOptions.FieldSelector = fields.OneTermEqualSelector("involvedObject.name", input.Object).String() } // Set limit listOptions.Limit = input.Limit // Set timeout listOptions.TimeoutSeconds = &input.TimeoutSeconds return listOptions } // filterEvents applies additional filtering based on input parameters. func (l *ListEventsTool) filterEvents(events []corev1.Event, input *ListEventsInput) []corev1.Event { var filteredEvents []corev1.Event for _, event := range events { // Filter by event type if specified if input.EventType != "" { if !strings.EqualFold(event.Type, input.EventType) { continue } } // Filter by reason if specified if input.Reason != "" { if !strings.Contains(strings.ToLower(event.Reason), strings.ToLower(input.Reason)) { continue } } // Filter by time if specified if !l.isEventWithinTimeRange(&event, input) { continue } filteredEvents = append(filteredEvents, event) } return filteredEvents } // isEventWithinTimeRange checks if the event falls within the specified time range. func (l *ListEventsTool) isEventWithinTimeRange(event *corev1.Event, input *ListEventsInput) bool { var cutoffTime time.Time var err error // Parse since duration if input.Since != "" { duration, parseErr := time.ParseDuration(input.Since) if parseErr == nil { cutoffTime = time.Now().Add(-duration) } } // Parse sinceTime (overrides since duration if both specified) if input.SinceTime != "" { cutoffTime, err = time.Parse(time.RFC3339, input.SinceTime) if err != nil { // If parsing fails, ignore the time filter return true } } // If no time filter specified, include all events if cutoffTime.IsZero() { return true } // Check if event's last timestamp is after the cutoff time return event.LastTimestamp.Time.After(cutoffTime) } // convertToEventInfos converts raw events to formatted EventInfo structs. func (l *ListEventsTool) convertToEventInfos(events []corev1.Event) []EventInfo { var eventInfos []EventInfo for _, event := range events { eventInfo := EventInfo{ FirstTimestamp: event.FirstTimestamp, LastTimestamp: event.LastTimestamp, Count: event.Count, Type: event.Type, Reason: event.Reason, Message: event.Message, Namespace: event.Namespace, } // Format involved object information if event.InvolvedObject.Kind != "" && event.InvolvedObject.Name != "" { eventInfo.Object = fmt.Sprintf("%s/%s", event.InvolvedObject.Kind, event.InvolvedObject.Name) if event.InvolvedObject.Namespace != "" && event.InvolvedObject.Namespace != event.Namespace { eventInfo.Object = fmt.Sprintf("%s/%s/%s", event.InvolvedObject.Namespace, event.InvolvedObject.Kind, event.InvolvedObject.Name) } } // Format source information if event.Source.Component != "" { eventInfo.Source = event.Source.Component if event.Source.Host != "" { eventInfo.Source = fmt.Sprintf("%s (%s)", event.Source.Component, event.Source.Host) } } eventInfos = append(eventInfos, eventInfo) } return eventInfos } // parseAndValidateEventsParams validates and extracts parameters from request arguments. func (l *ListEventsTool) parseAndValidateEventsParams(args map[string]any) (*ListEventsInput, error) { input := &ListEventsInput{} // Optional: context if context, ok := args["context"].(string); ok && context != "" { input.Context = context } if ns, ok := args["namespace"].(string); ok && ns != "" { input.Namespace = ns if err := validation.ValidateNamespace(input.Namespace); err != nil { return nil, fmt.Errorf("invalid namespace: %w", err) } } if obj, ok := args["object"].(string); ok && obj != "" { input.Object = obj if err := validation.ValidateResourceName(input.Object); err != nil { return nil, fmt.Errorf("invalid object: %w", err) } } if eventType, ok := args["eventType"].(string); ok && eventType != "" { input.EventType = eventType if !strings.EqualFold(eventType, "Normal") && !strings.EqualFold(eventType, "Warning") { return nil, fmt.Errorf("invalid eventType: must be 'Normal' or 'Warning' (case-insensitive)") } } if reason, ok := args["reason"].(string); ok && reason != "" { input.Reason = reason } if since, ok := args["since"].(string); ok && since != "" { input.Since = since if _, err := time.ParseDuration(since); err != nil { return nil, fmt.Errorf("invalid since duration format: %w", err) } } if sinceTime, ok := args["sinceTime"].(string); ok && sinceTime != "" { input.SinceTime = sinceTime if _, err := time.Parse(time.RFC3339, sinceTime); err != nil { return nil, fmt.Errorf("invalid sinceTime format (expected RFC3339): %w", err) } } if limit, ok := args["limit"].(float64); ok && limit >= 0 { input.Limit = int64(limit) } else { input.Limit = 100 } if timeoutSeconds, ok := args["timeoutSeconds"].(float64); ok && timeoutSeconds > 0 { input.TimeoutSeconds = int64(timeoutSeconds) } else { input.TimeoutSeconds = 30 } return input, 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