This is page 2 of 5. Use http://codebase.md/blankcut/kubernetes-mcp-server?page={x} to view the full context. # Directory Structure ``` ├── .gitignore ├── docs │ ├── .astro │ │ ├── collections │ │ │ └── docs.schema.json │ │ ├── content-assets.mjs │ │ ├── content-modules.mjs │ │ ├── content.d.ts │ │ ├── data-store.json │ │ ├── settings.json │ │ └── types.d.ts │ ├── .gitignore │ ├── astro.config.mjs │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── images │ │ └── logo.svg │ ├── README.md │ ├── src │ │ ├── components │ │ │ ├── CodeBlock.astro │ │ │ ├── DocSidebar.astro │ │ │ ├── Footer.astro │ │ │ ├── Header.astro │ │ │ ├── HeadSEO.astro │ │ │ ├── Search.astro │ │ │ ├── Sidebar.astro │ │ │ └── TableOfContents.astro │ │ ├── content │ │ │ ├── config.ts │ │ │ └── docs │ │ │ ├── api-overview.md │ │ │ ├── configuration.md │ │ │ ├── installation.md │ │ │ ├── introduction.md │ │ │ ├── model-context-protocol.md │ │ │ ├── quick-start.md │ │ │ └── troubleshooting-resources.md │ │ ├── env.d.ts │ │ ├── layouts │ │ │ ├── BaseLayout.astro │ │ │ └── DocLayout.astro │ │ ├── pages │ │ │ ├── [...slug].astro │ │ │ ├── 404.astro │ │ │ ├── docs │ │ │ │ └── index.astro │ │ │ ├── docs-test.astro │ │ │ ├── examples │ │ │ │ └── index.astro │ │ │ └── index.astro │ │ └── styles │ │ └── global.css │ ├── tailwind.config.cjs │ └── tsconfig.json ├── go.mod ├── kubernetes-claude-mcp │ ├── .gitignore │ ├── cmd │ │ └── server │ │ └── main.go │ ├── docker-compose.yml │ ├── Dockerfile │ ├── go.mod │ ├── go.sum │ ├── internal │ │ ├── api │ │ │ ├── namespace_routes.go │ │ │ ├── routes.go │ │ │ └── server.go │ │ ├── argocd │ │ │ ├── applications.go │ │ │ ├── client.go │ │ │ └── history.go │ │ ├── auth │ │ │ ├── credentials.go │ │ │ ├── secrets.go │ │ │ └── vault.go │ │ ├── claude │ │ │ ├── client.go │ │ │ └── protocol.go │ │ ├── correlator │ │ │ ├── gitops.go │ │ │ ├── helm_correlator.go │ │ │ └── troubleshoot.go │ │ ├── gitlab │ │ │ ├── client.go │ │ │ ├── mergerequests.go │ │ │ ├── pipelines.go │ │ │ └── repositories.go │ │ ├── helm │ │ │ └── parser.go │ │ ├── k8s │ │ │ ├── client.go │ │ │ ├── enhanced_client.go │ │ │ ├── events.go │ │ │ ├── resource_mapper.go │ │ │ └── resources.go │ │ ├── mcp │ │ │ ├── context.go │ │ │ ├── namespace_analyzer.go │ │ │ ├── prompt.go │ │ │ └── protocol.go │ │ └── models │ │ ├── argocd.go │ │ ├── context.go │ │ ├── gitlab.go │ │ └── kubernetes.go │ └── pkg │ ├── config │ │ └── config.go │ ├── logging │ │ └── logging.go │ └── utils │ ├── serialization.go │ └── truncation.go ├── LICENSE └── README.md ``` # Files -------------------------------------------------------------------------------- /docs/src/components/Header.astro: -------------------------------------------------------------------------------- ``` --- import Search from './Search.astro'; --- <header class="sticky top-0 z-40 w-full backdrop-blur bg-white/90 dark:bg-slate-900/90 border-b border-slate-200 dark:border-slate-700"> <div class="container mx-auto px-4 py-3 flex justify-between items-center"> <a href="/" class="flex items-center space-x-2"> <img src="/images/logo.svg" alt="Kubernetes Claude MCP" class="h-8 w-8" /> <span class="font-bold text-xl">Kubernetes Claude MCP</span> </a> <div class="hidden md:flex items-center space-x-6"> <Search /> <nav class="flex space-x-6"> <a href="/docs/introduction" class="text-slate-700 hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400">Documentation</a> <a href="/examples" class="text-slate-700 hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400">Examples</a> </nav> <a href="https://github.com/blankcut/kubernetes-mcp-server" target="_blank" rel="noopener noreferrer" class="text-slate-700 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white"> <span class="sr-only">GitHub</span> <svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path> </svg> </a> </div> <button id="mobile-menu-toggle" class="md:hidden p-2"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path> </svg> </button> </div> <div id="mobile-menu" class="md:hidden hidden"> <div class="px-4 py-3 space-y-4"> <Search /> <nav class="flex flex-col space-y-3"> <a href="/docs/introduction" class="text-slate-700 hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400">Documentation</a> <a href="/examples" class="text-slate-700 hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400">Examples</a> <a href="https://github.com/blankcut/kubernetes-mcp-server" target="_blank" rel="noopener noreferrer" class="text-slate-700 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white flex items-center"> <svg class="h-5 w-5 mr-2" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path> </svg> GitHub </a> </nav> </div> </div> </header> <script> const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); const mobileMenu = document.getElementById('mobile-menu'); if (mobileMenuToggle && mobileMenu) { mobileMenuToggle.addEventListener('click', () => { mobileMenu.classList.toggle('hidden'); }); } </script> ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/claude/client.go: -------------------------------------------------------------------------------- ```go package claude import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" ) // Client handles communication with the Claude API type Client struct { apiKey string baseURL string modelID string maxTokens int temperature float64 httpClient *http.Client logger *logging.Logger } // Message represents a message in the Claude conversation type Message struct { Role string `json:"role"` Content string `json:"content"` } // CompletionRequest represents a request to the Claude API type CompletionRequest struct { Model string `json:"model"` System string `json:"system,omitempty"` Messages []Message `json:"messages"` MaxTokens int `json:"max_tokens,omitempty"` Temperature float64 `json:"temperature,omitempty"` } // ContentItem represents an item in the content array of a response type ContentItem struct { Type string `json:"type"` Text string `json:"text"` } // CompletionResponse represents a response from the Claude API type CompletionResponse struct { ID string `json:"id"` Type string `json:"type"` Model string `json:"model"` Content []ContentItem `json:"content"` Usage Usage `json:"usage"` } // Usage represents token usage information type Usage struct { InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` } // NewClient creates a new Claude API client func NewClient(cfg ClaudeConfig, logger *logging.Logger) *Client { if logger == nil { logger = logging.NewLogger().Named("claude") } return &Client{ apiKey: cfg.APIKey, baseURL: cfg.BaseURL, modelID: cfg.ModelID, maxTokens: cfg.MaxTokens, temperature: cfg.Temperature, httpClient: &http.Client{ Timeout: 120 * time.Second, }, logger: logger, } } // ClaudeConfig holds configuration for the Claude API client type ClaudeConfig struct { APIKey string `yaml:"apiKey"` BaseURL string `yaml:"baseURL"` ModelID string `yaml:"modelID"` MaxTokens int `yaml:"maxTokens"` Temperature float64 `yaml:"temperature"` } // Complete sends a completion request to the Claude API func (c *Client) Complete(ctx context.Context, messages []Message) (string, error) { c.logger.Debug("Sending completion request", "model", c.modelID, "messageCount", len(messages)) // Extract system message if present var systemPrompt string var userMessages []Message for _, msg := range messages { if msg.Role == "system" { systemPrompt = msg.Content } else { userMessages = append(userMessages, msg) } } reqBody := CompletionRequest{ Model: c.modelID, System: systemPrompt, Messages: userMessages, MaxTokens: c.maxTokens, Temperature: c.temperature, } reqJSON, err := json.Marshal(reqBody) if err != nil { return "", fmt.Errorf("failed to marshal request: %w", err) } req, err := http.NewRequestWithContext( ctx, http.MethodPost, c.baseURL+"/v1/messages", bytes.NewBuffer(reqJSON), ) if err != nil { return "", fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("x-api-key", c.apiKey) req.Header.Set("anthropic-version", "2023-06-01") resp, err := c.httpClient.Do(req) if err != nil { return "", fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("failed to read response body: %w", err) } if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, body) } var completionResponse CompletionResponse if err := json.Unmarshal(body, &completionResponse); err != nil { return "", fmt.Errorf("failed to unmarshal response: %w", err) } // Extract text from content array var responseText string for _, content := range completionResponse.Content { if content.Type == "text" { responseText += content.Text } } c.logger.Debug("Received completion response", "model", completionResponse.Model, "inputTokens", completionResponse.Usage.InputTokens, "outputTokens", completionResponse.Usage.OutputTokens) return responseText, nil } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/gitlab/pipelines.go: -------------------------------------------------------------------------------- ```go package gitlab import ( "io" "context" "encoding/json" "fmt" "net/http" "net/url" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" ) // ListPipelines returns a list of pipelines for a project func (c *Client) ListPipelines(ctx context.Context, projectID string) ([]models.GitLabPipeline, error) { c.logger.Debug("Listing pipelines", "projectID", projectID) endpoint := fmt.Sprintf("projects/%s/pipelines", url.PathEscape(projectID)) // Add query parameters for pagination u, err := url.Parse(endpoint) if err != nil { return nil, fmt.Errorf("invalid endpoint: %w", err) } q := u.Query() q.Set("per_page", "20") q.Set("order_by", "id") q.Set("sort", "desc") u.RawQuery = q.Encode() resp, err := c.doRequest(ctx, http.MethodGet, u.String(), nil) if err != nil { return nil, err } defer resp.Body.Close() var pipelines []models.GitLabPipeline if err := json.NewDecoder(resp.Body).Decode(&pipelines); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } c.logger.Debug("Listed pipelines", "projectID", projectID, "count", len(pipelines)) return pipelines, nil } // GetPipeline returns details about a specific pipeline func (c *Client) GetPipeline(ctx context.Context, projectID string, pipelineID int) (*models.GitLabPipeline, error) { c.logger.Debug("Getting pipeline", "projectID", projectID, "pipelineID", pipelineID) endpoint := fmt.Sprintf("projects/%s/pipelines/%d", url.PathEscape(projectID), pipelineID) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var pipeline models.GitLabPipeline if err := json.NewDecoder(resp.Body).Decode(&pipeline); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &pipeline, nil } // GetPipelineJobs returns jobs for a specific pipeline func (c *Client) GetPipelineJobs(ctx context.Context, projectID string, pipelineID int) ([]models.GitLabJob, error) { c.logger.Debug("Getting pipeline jobs", "projectID", projectID, "pipelineID", pipelineID) endpoint := fmt.Sprintf("projects/%s/pipelines/%d/jobs", url.PathEscape(projectID), pipelineID) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var jobs []models.GitLabJob if err := json.NewDecoder(resp.Body).Decode(&jobs); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } c.logger.Debug("Got pipeline jobs", "projectID", projectID, "pipelineID", pipelineID, "count", len(jobs)) return jobs, nil } // FindRecentDeployments finds recent deployments to a specific environment func (c *Client) FindRecentDeployments(ctx context.Context, projectID, environment string) ([]models.GitLabDeployment, error) { c.logger.Debug("Finding recent deployments", "projectID", projectID, "environment", environment) // Create endpoint with query parameters endpoint := fmt.Sprintf("projects/%s/deployments", url.PathEscape(projectID)) u, err := url.Parse(endpoint) if err != nil { return nil, fmt.Errorf("invalid endpoint: %w", err) } q := u.Query() q.Set("environment", environment) q.Set("order_by", "created_at") q.Set("sort", "desc") q.Set("per_page", "10") u.RawQuery = q.Encode() resp, err := c.doRequest(ctx, http.MethodGet, u.String(), nil) if err != nil { return nil, err } defer resp.Body.Close() var deployments []models.GitLabDeployment if err := json.NewDecoder(resp.Body).Decode(&deployments); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } c.logger.Debug("Found deployments", "projectID", projectID, "environment", environment, "count", len(deployments)) return deployments, nil } // GetJobLogs retrieves logs for a specific job func (c *Client) GetJobLogs(ctx context.Context, projectID string, jobID int) (string, error) { c.logger.Debug("Getting job logs", "projectID", projectID, "jobID", jobID) endpoint := fmt.Sprintf("projects/%s/jobs/%d/trace", url.PathEscape(projectID), jobID) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return "", err } defer resp.Body.Close() logs, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("failed to read logs: %w", err) } return string(logs), nil } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/models/context.go: -------------------------------------------------------------------------------- ```go package models // ResourceContext combines information about a Kubernetes resource with GitOps context type ResourceContext struct { // Basic resource information Kind string `json:"kind"` Name string `json:"name"` Namespace string `json:"namespace"` APIVersion string `json:"apiVersion"` Metadata map[string]interface{} `json:"metadata,omitempty"` ResourceData string `json:"resourceData,omitempty"` // Related ArgoCD information ArgoApplication *ArgoApplication `json:"argoApplication,omitempty"` ArgoSyncStatus string `json:"argoSyncStatus,omitempty"` ArgoHealthStatus string `json:"argoHealthStatus,omitempty"` ArgoSyncHistory []ArgoApplicationHistory `json:"argoSyncHistory,omitempty"` // Related GitLab information GitLabProject *GitLabProject `json:"gitlabProject,omitempty"` LastPipeline *GitLabPipeline `json:"lastPipeline,omitempty"` LastDeployment *GitLabDeployment `json:"lastDeployment,omitempty"` RecentCommits []GitLabCommit `json:"recentCommits,omitempty"` // Additional context Events []K8sEvent `json:"events,omitempty"` RelatedResources []string `json:"relatedResources,omitempty"` Errors []string `json:"errors,omitempty"` } // Issue represents a discovered issue or potential problem type Issue struct { Title string `json:"title"` Category string `json:"category"` Severity string `json:"severity"` Source string `json:"source"` Description string `json:"description"` } // TroubleshootResult contains troubleshooting findings and recommendations type TroubleshootResult struct { ResourceContext ResourceContext `json:"resourceContext"` Issues []Issue `json:"issues"` Recommendations []string `json:"recommendations"` } // MCPRequest represents a request to the MCP server type MCPRequest struct { Action string `json:"action"` Resource string `json:"resource,omitempty"` Namespace string `json:"namespace,omitempty"` Name string `json:"name,omitempty"` Query string `json:"query,omitempty"` CommitSHA string `json:"commitSha,omitempty"` ProjectID string `json:"projectId,omitempty"` MergeRequestIID int `json:"mergeRequestIid,omitempty"` ResourceSpecs map[string]interface{} `json:"resourceSpecs,omitempty"` Context string `json:"context,omitempty"` } // ResourceRelationship represents a relationship between two resources type ResourceRelationship struct { SourceKind string `json:"sourceKind"` SourceName string `json:"sourceName"` SourceNamespace string `json:"sourceNamespace"` TargetKind string `json:"targetKind"` TargetName string `json:"targetName"` TargetNamespace string `json:"targetNamespace"` RelationType string `json:"relationType"` } // NamespaceAnalysisResult contains the analysis of a namespace's resources type NamespaceAnalysisResult struct { Namespace string `json:"namespace"` ResourceCounts map[string]int `json:"resourceCounts"` HealthStatus map[string]map[string]int `json:"healthStatus"` ResourceRelationships []ResourceRelationship `json:"resourceRelationships"` Issues []Issue `json:"issues"` Recommendations []string `json:"recommendations"` Analysis string `json:"analysis"` } // MCPResponse represents a response from the MCP server type MCPResponse struct { Success bool `json:"success"` Message string `json:"message,omitempty"` Analysis string `json:"analysis,omitempty"` Context ResourceContext `json:"context,omitempty"` Actions []string `json:"actions,omitempty"` ErrorDetails string `json:"errorDetails,omitempty"` TroubleshootResult *TroubleshootResult `json:"troubleshootResult,omitempty"` NamespaceAnalysis *NamespaceAnalysisResult `json:"namespaceAnalysis,omitempty"` } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/auth/secrets.go: -------------------------------------------------------------------------------- ```go package auth import ( "context" "fmt" "os" "path/filepath" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" ) // SecretsManager handles access to secrets stored in various backends type SecretsManager struct { logger *logging.Logger // Directory where secrets files are stored secretsDir string // Flag to indicate if secrets manager is available available bool } // NewSecretsManager creates a new secrets manager func NewSecretsManager(logger *logging.Logger) *SecretsManager { if logger == nil { logger = logging.NewLogger().Named("secrets") } // Default secrets directory is ./secrets secretsDir := os.Getenv("SECRETS_DIR") if secretsDir == "" { secretsDir = "./secrets" } // Check if secrets directory exists _, err := os.Stat(secretsDir) available := err == nil if !available { logger.Warn("Secrets directory not available", "directory", secretsDir) } return &SecretsManager{ logger: logger, secretsDir: secretsDir, available: available, } } // IsAvailable returns true if the secrets manager is available func (sm *SecretsManager) IsAvailable() bool { return sm.available } // GetCredentials retrieves credentials for a service from the secrets manager func (sm *SecretsManager) GetCredentials(ctx context.Context, service string) (*Credentials, error) { if !sm.available { return nil, fmt.Errorf("secrets manager not available") } // Build paths to potential secret files tokenPath := filepath.Join(sm.secretsDir, service, "token") apiKeyPath := filepath.Join(sm.secretsDir, service, "apikey") usernamePath := filepath.Join(sm.secretsDir, service, "username") passwordPath := filepath.Join(sm.secretsDir, service, "password") // Initialize credentials creds := &Credentials{} // Try to read token tokenBytes, err := os.ReadFile(tokenPath) if err == nil { creds.Token = string(tokenBytes) sm.logger.Debug("Loaded token from file", "service", service) } // Try to read API key apiKeyBytes, err := os.ReadFile(apiKeyPath) if err == nil { creds.APIKey = string(apiKeyBytes) sm.logger.Debug("Loaded API key from file", "service", service) } // Try to read username usernameBytes, err := os.ReadFile(usernamePath) if err == nil { creds.Username = string(usernameBytes) sm.logger.Debug("Loaded username from file", "service", service) } // Try to read password passwordBytes, err := os.ReadFile(passwordPath) if err == nil { creds.Password = string(passwordBytes) sm.logger.Debug("Loaded password from file", "service", service) } // Check if we loaded any credentials if creds.Token == "" && creds.APIKey == "" && creds.Username == "" && creds.Password == "" { return nil, fmt.Errorf("no credentials found for service: %s", service) } return creds, nil } // SaveCredentials saves credentials for a service to the secrets manager func (sm *SecretsManager) SaveCredentials(ctx context.Context, service string, creds *Credentials) error { if !sm.available { return fmt.Errorf("secrets manager not available") } // Create service directory if it doesn't exist serviceDir := filepath.Join(sm.secretsDir, service) if err := os.MkdirAll(serviceDir, 0700); err != nil { return fmt.Errorf("failed to create service directory: %w", err) } // Save token if provided if creds.Token != "" { tokenPath := filepath.Join(serviceDir, "token") if err := os.WriteFile(tokenPath, []byte(creds.Token), 0600); err != nil { return fmt.Errorf("failed to save token: %w", err) } sm.logger.Debug("Saved token to file", "service", service) } // Save API key if provided if creds.APIKey != "" { apiKeyPath := filepath.Join(serviceDir, "apikey") if err := os.WriteFile(apiKeyPath, []byte(creds.APIKey), 0600); err != nil { return fmt.Errorf("failed to save API key: %w", err) } sm.logger.Debug("Saved API key to file", "service", service) } // Save username if provided if creds.Username != "" { usernamePath := filepath.Join(serviceDir, "username") if err := os.WriteFile(usernamePath, []byte(creds.Username), 0600); err != nil { return fmt.Errorf("failed to save username: %w", err) } sm.logger.Debug("Saved username to file", "service", service) } // Save password if provided if creds.Password != "" { passwordPath := filepath.Join(serviceDir, "password") if err := os.WriteFile(passwordPath, []byte(creds.Password), 0600); err != nil { return fmt.Errorf("failed to save password: %w", err) } sm.logger.Debug("Saved password to file", "service", service) } return nil } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/argocd/applications.go: -------------------------------------------------------------------------------- ```go package argocd import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" ) // ListApplications returns a list of all ArgoCD applications func (c *Client) ListApplications(ctx context.Context) ([]models.ArgoApplication, error) { c.logger.Debug("Listing ArgoCD applications") // Try the v1 API path endpoint := "/api/v1/applications" resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var result struct { Items []models.ArgoApplication `json:"items"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } c.logger.Debug("Listed ArgoCD applications", "count", len(result.Items)) return result.Items, nil } // GetApplication returns details about a specific ArgoCD application func (c *Client) GetApplication(ctx context.Context, name string) (*models.ArgoApplication, error) { c.logger.Debug("Getting ArgoCD application", "name", name) endpoint := fmt.Sprintf("/api/v1/applications/%s", url.PathEscape(name)) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var app models.ArgoApplication if err := json.NewDecoder(resp.Body).Decode(&app); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &app, nil } // GetResourceTree returns the resource hierarchy for an application func (c *Client) GetResourceTree(ctx context.Context, name string) (*models.ArgoResourceTree, error) { c.logger.Debug("Getting resource tree for application", "name", name) endpoint := fmt.Sprintf("/api/v1/applications/%s/resource-tree", url.PathEscape(name)) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var tree models.ArgoResourceTree if err := json.NewDecoder(resp.Body).Decode(&tree); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } c.logger.Debug("Retrieved resource tree", "name", name, "nodeCount", len(tree.Nodes)) return &tree, nil } // FindApplicationsByResource finds all ArgoCD applications that manage a specific Kubernetes resource func (c *Client) FindApplicationsByResource(ctx context.Context, kind, name, namespace string) ([]models.ArgoApplication, error) { c.logger.Debug("Finding applications by resource", "kind", kind, "name", name, "namespace", namespace) // First try to use the resource API endpoint if available endpoint := fmt.Sprintf("/api/v1/applications/resource/%s/%s/%s/%s/%s", url.PathEscape(""), url.PathEscape(kind), url.PathEscape(namespace), url.PathEscape(name), url.PathEscape(""), ) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err == nil { defer resp.Body.Close() var appRefs []struct { Name string `json:"name"` } if err := json.NewDecoder(resp.Body).Decode(&appRefs); err != nil { c.logger.Warn("Failed to decode application references", "error", err) } else if len(appRefs) > 0 { // Get full application details for each reference var apps []models.ArgoApplication for _, ref := range appRefs { app, err := c.GetApplication(ctx, ref.Name) if err != nil { c.logger.Warn("Failed to get application details", "name", ref.Name, "error", err) continue } apps = append(apps, *app) } c.logger.Debug("Found applications by resource API", "resourceKind", kind, "resourceName", name, "count", len(apps)) return apps, nil } } // Fallback: Get all applications and check their resource trees c.logger.Debug("Resource API failed, falling back to application scanning") apps, err := c.ListApplications(ctx) if err != nil { return nil, fmt.Errorf("failed to list applications: %w", err) } var matchingApps []models.ArgoApplication // For each application, check if it manages the specified resource for _, app := range apps { tree, err := c.GetResourceTree(ctx, app.Name) if err != nil { c.logger.Warn("Failed to get resource tree", "application", app.Name, "error", err) continue // Skip this app if we can't get its resource tree } for _, node := range tree.Nodes { // Match against the specified resource if strings.EqualFold(node.Kind, kind) && node.Name == name && (namespace == "" || node.Namespace == namespace) { matchingApps = append(matchingApps, app) break // Found a match in this app, move to the next app } } } c.logger.Debug("Found applications managing resource by scanning", "resourceKind", kind, "resourceName", name, "count", len(matchingApps)) return matchingApps, nil } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/cmd/server/main.go: -------------------------------------------------------------------------------- ```go package main import ( "context" "flag" "os" "os/signal" "syscall" "time" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/api" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/argocd" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/auth" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/claude" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/correlator" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/gitlab" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/k8s" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/mcp" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/config" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" ) func main() { // Parse command line flags configPath := flag.String("config", "config.yaml", "path to config file") logLevel := flag.String("log-level", "info", "logging level (debug, info, warn, error)") flag.Parse() // Initialize logger os.Setenv("LOG_LEVEL", *logLevel) logger := logging.NewLogger() logger.Info("Starting Kubernetes Claude MCP server") // Load configuration logger.Info("Loading configuration", "path", *configPath) cfg, err := config.Load(*configPath) if err != nil { logger.Fatal("Failed to load configuration", "error", err) } // Validate configuration if err := cfg.Validate(); err != nil { logger.Fatal("Invalid configuration", "error", err) } // Set up context with cancellation ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Initialize credential provider logger.Info("Initializing credential provider") credProvider := auth.NewCredentialProvider(cfg) if err := credProvider.LoadCredentials(ctx); err != nil { logger.Fatal("Failed to load credentials", "error", err) } // Initialize Kubernetes client logger.Info("Initializing Kubernetes client") k8sClient, err := k8s.NewClient(cfg.Kubernetes, logger.Named("k8s")) if err != nil { logger.Fatal("Failed to create Kubernetes client", "error", err) } // Check Kubernetes connectivity if err := k8sClient.CheckConnectivity(ctx); err != nil { logger.Warn("Kubernetes connectivity check failed", "error", err) } else { logger.Info("Kubernetes connectivity confirmed") } // Initialize ArgoCD client logger.Info("Initializing ArgoCD client") argoClient := argocd.NewClient(&cfg.ArgoCD, credProvider, logger.Named("argocd")) // Check ArgoCD connectivity (don't fail if unavailable) if err := argoClient.CheckConnectivity(ctx); err != nil { logger.Warn("ArgoCD connectivity check failed", "error", err) } else { logger.Info("ArgoCD connectivity confirmed") } // Initialize GitLab client logger.Info("Initializing GitLab client") gitlabClient := gitlab.NewClient(&cfg.GitLab, credProvider, logger.Named("gitlab")) // Check GitLab connectivity (don't fail if unavailable) if err := gitlabClient.CheckConnectivity(ctx); err != nil { logger.Warn("GitLab connectivity check failed", "error", err) } else { logger.Info("GitLab connectivity confirmed") } // Initialize Claude client logger.Info("Initializing Claude client") claudeConfig := claude.ClaudeConfig{ APIKey: cfg.Claude.APIKey, BaseURL: cfg.Claude.BaseURL, ModelID: cfg.Claude.ModelID, MaxTokens: cfg.Claude.MaxTokens, Temperature: cfg.Claude.Temperature, } claudeClient := claude.NewClient(claudeConfig, logger.Named("claude")) // Initialize GitOps correlator logger.Info("Initializing GitOps correlator") gitOpsCorrelator := correlator.NewGitOpsCorrelator( k8sClient, argoClient, gitlabClient, logger.Named("correlator"), ) // Initialize troubleshoot correlator troubleshootCorrelator := correlator.NewTroubleshootCorrelator( gitOpsCorrelator, k8sClient, logger.Named("troubleshoot"), ) // Initialize MCP protocol handler logger.Info("Initializing MCP protocol handler") mcpHandler := mcp.NewProtocolHandler( claudeClient, gitOpsCorrelator, k8sClient, logger.Named("mcp"), ) // Initialize API server logger.Info("Initializing API server") server := api.NewServer( cfg.Server, k8sClient, argoClient, gitlabClient, mcpHandler, troubleshootCorrelator, logger.Named("api"), ) // Handle graceful shutdown go func() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) sig := <-sigCh logger.Info("Received shutdown signal", "signal", sig) // Create a timeout context for shutdown shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) defer shutdownCancel() logger.Info("Shutting down server...") cancel() // Cancel the main context // Wait for server to shut down or timeout <-shutdownCtx.Done() }() // Start server logger.Info("Starting MCP server", "address", cfg.Server.Address) if err := server.Start(ctx); err != nil { logger.Fatal("Server error", "error", err) } logger.Info("Server shutdown complete") } ``` -------------------------------------------------------------------------------- /docs/.astro/content.d.ts: -------------------------------------------------------------------------------- ```typescript declare module 'astro:content' { interface Render { '.mdx': Promise<{ Content: import('astro').MarkdownInstance<{}>['Content']; headings: import('astro').MarkdownHeading[]; remarkPluginFrontmatter: Record<string, any>; components: import('astro').MDXInstance<{}>['components']; }>; } } declare module 'astro:content' { export interface RenderResult { Content: import('astro/runtime/server/index.js').AstroComponentFactory; headings: import('astro').MarkdownHeading[]; remarkPluginFrontmatter: Record<string, any>; } interface Render { '.md': Promise<RenderResult>; } export interface RenderedContent { html: string; metadata?: { imagePaths: Array<string>; [key: string]: unknown; }; } } declare module 'astro:content' { type Flatten<T> = T extends { [K: string]: infer U } ? U : never; export type CollectionKey = keyof AnyEntryMap; export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>; export type ContentCollectionKey = keyof ContentEntryMap; export type DataCollectionKey = keyof DataEntryMap; type AllValuesOf<T> = T extends any ? T[keyof T] : never; type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf< ContentEntryMap[C] >['slug']; export type ReferenceDataEntry< C extends CollectionKey, E extends keyof DataEntryMap[C] = string, > = { collection: C; id: E; }; export type ReferenceContentEntry< C extends keyof ContentEntryMap, E extends ValidContentEntrySlug<C> | (string & {}) = string, > = { collection: C; slug: E; }; /** @deprecated Use `getEntry` instead. */ export function getEntryBySlug< C extends keyof ContentEntryMap, E extends ValidContentEntrySlug<C> | (string & {}), >( collection: C, // Note that this has to accept a regular string too, for SSR entrySlug: E, ): E extends ValidContentEntrySlug<C> ? Promise<CollectionEntry<C>> : Promise<CollectionEntry<C> | undefined>; /** @deprecated Use `getEntry` instead. */ export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>( collection: C, entryId: E, ): Promise<CollectionEntry<C>>; export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>( collection: C, filter?: (entry: CollectionEntry<C>) => entry is E, ): Promise<E[]>; export function getCollection<C extends keyof AnyEntryMap>( collection: C, filter?: (entry: CollectionEntry<C>) => unknown, ): Promise<CollectionEntry<C>[]>; export function getEntry< C extends keyof ContentEntryMap, E extends ValidContentEntrySlug<C> | (string & {}), >( entry: ReferenceContentEntry<C, E>, ): E extends ValidContentEntrySlug<C> ? Promise<CollectionEntry<C>> : Promise<CollectionEntry<C> | undefined>; export function getEntry< C extends keyof DataEntryMap, E extends keyof DataEntryMap[C] | (string & {}), >( entry: ReferenceDataEntry<C, E>, ): E extends keyof DataEntryMap[C] ? Promise<DataEntryMap[C][E]> : Promise<CollectionEntry<C> | undefined>; export function getEntry< C extends keyof ContentEntryMap, E extends ValidContentEntrySlug<C> | (string & {}), >( collection: C, slug: E, ): E extends ValidContentEntrySlug<C> ? Promise<CollectionEntry<C>> : Promise<CollectionEntry<C> | undefined>; export function getEntry< C extends keyof DataEntryMap, E extends keyof DataEntryMap[C] | (string & {}), >( collection: C, id: E, ): E extends keyof DataEntryMap[C] ? string extends keyof DataEntryMap[C] ? Promise<DataEntryMap[C][E]> | undefined : Promise<DataEntryMap[C][E]> : Promise<CollectionEntry<C> | undefined>; /** Resolve an array of entry references from the same collection */ export function getEntries<C extends keyof ContentEntryMap>( entries: ReferenceContentEntry<C, ValidContentEntrySlug<C>>[], ): Promise<CollectionEntry<C>[]>; export function getEntries<C extends keyof DataEntryMap>( entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[], ): Promise<CollectionEntry<C>[]>; export function render<C extends keyof AnyEntryMap>( entry: AnyEntryMap[C][string], ): Promise<RenderResult>; export function reference<C extends keyof AnyEntryMap>( collection: C, ): import('astro/zod').ZodEffects< import('astro/zod').ZodString, C extends keyof ContentEntryMap ? ReferenceContentEntry<C, ValidContentEntrySlug<C>> : ReferenceDataEntry<C, keyof DataEntryMap[C]> >; // Allow generic `string` to avoid excessive type errors in the config // if `dev` is not running to update as you edit. // Invalid collection names will be caught at build time. export function reference<C extends string>( collection: C, ): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>; type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T; type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer< ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']> >; type ContentEntryMap = { }; type DataEntryMap = { "docs": Record<string, { id: string; render(): Render[".md"]; slug: string; body: string; collection: "docs"; data: InferEntrySchema<"docs">; rendered?: RenderedContent; filePath?: string; }>; }; type AnyEntryMap = ContentEntryMap & DataEntryMap; export type ContentConfig = typeof import("../src/content/config.js"); } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/k8s/client.go: -------------------------------------------------------------------------------- ```go package k8s import ( "context" "fmt" "path/filepath" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/util/homedir" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/config" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" ) // Client wraps the Kubernetes clientset and provides additional functionality type Client struct { clientset *kubernetes.Clientset dynamicClient dynamic.Interface discoveryClient *discovery.DiscoveryClient restConfig *rest.Config defaultNS string logger *logging.Logger ResourceMapper *ResourceMapper } // NewClient creates a new Kubernetes client based on the provided configuration func NewClient(cfg config.KubernetesConfig, logger *logging.Logger) (*Client, error) { if logger == nil { logger = logging.NewLogger().Named("k8s") } var restConfig *rest.Config var err error logger.Debug("Initializing Kubernetes client", "inCluster", cfg.InCluster, "kubeconfig", cfg.KubeConfig, "defaultNamespace", cfg.DefaultNamespace) if cfg.InCluster { // Use in-cluster config when deployed inside Kubernetes restConfig, err = rest.InClusterConfig() if err != nil { return nil, fmt.Errorf("failed to create in-cluster config: %w", err) } logger.Debug("Using in-cluster configuration") } else { // Use kubeconfig file kubeconfigPath := cfg.KubeConfig if kubeconfigPath == "" { // Try to use default location if not specified if home := homedir.HomeDir(); home != "" { kubeconfigPath = filepath.Join(home, ".kube", "config") logger.Debug("Using default kubeconfig path", "path", kubeconfigPath) } else { return nil, fmt.Errorf("kubeconfig not specified and home directory not found") } } // Build config from kubeconfig file configLoadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath} configOverrides := &clientcmd.ConfigOverrides{} if cfg.DefaultContext != "" { configOverrides.CurrentContext = cfg.DefaultContext logger.Debug("Using specified context", "context", cfg.DefaultContext) } kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( configLoadingRules, configOverrides, ) restConfig, err = kubeConfig.ClientConfig() if err != nil { return nil, fmt.Errorf("failed to build kubeconfig: %w", err) } } // Increase QPS and Burst for better performance in busy environments restConfig.QPS = 100 restConfig.Burst = 100 // Create clientset clientset, err := kubernetes.NewForConfig(restConfig) if err != nil { return nil, fmt.Errorf("failed to create Kubernetes clientset: %w", err) } // Create dynamic client dynamicClient, err := dynamic.NewForConfig(restConfig) if err != nil { return nil, fmt.Errorf("failed to create dynamic client: %w", err) } // Create discovery client discoveryClient, err := discovery.NewDiscoveryClientForConfig(restConfig) if err != nil { return nil, fmt.Errorf("failed to create discovery client: %w", err) } defaultNamespace := cfg.DefaultNamespace if defaultNamespace == "" { defaultNamespace = "default" } logger.Info("Kubernetes client initialized", "defaultNamespace", defaultNamespace) // Create the client instance client := &Client{ clientset: clientset, dynamicClient: dynamicClient, discoveryClient: discoveryClient, restConfig: restConfig, defaultNS: defaultNamespace, logger: logger, } // Initialize the ResourceMapper (ensure NewResourceMapper is defined in your package) client.ResourceMapper = NewResourceMapper(client) return client, nil } // CheckConnectivity verifies connectivity to the Kubernetes API func (c *Client) CheckConnectivity(ctx context.Context) error { c.logger.Debug("Checking Kubernetes connectivity") // Try to get server version as a basic connectivity test _, err := c.clientset.Discovery().ServerVersion() if err != nil { c.logger.Warn("Kubernetes connectivity check failed", "error", err) return fmt.Errorf("failed to connect to Kubernetes API: %w", err) } c.logger.Debug("Kubernetes connectivity check successful") return nil } // GetNamespaces returns a list of all namespaces in the cluster func (c *Client) GetNamespaces(ctx context.Context) ([]string, error) { c.logger.Debug("Getting namespaces") namespaceList, err := c.clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) if err != nil { return nil, fmt.Errorf("failed to list namespaces: %w", err) } var namespaces []string for _, ns := range namespaceList.Items { namespaces = append(namespaces, ns.Name) } c.logger.Debug("Got namespaces", "count", len(namespaces)) return namespaces, nil } // GetDefaultNamespace returns the default namespace for operations func (c *Client) GetDefaultNamespace() string { return c.defaultNS } // GetRestConfig returns the Kubernetes REST configuration func (c *Client) GetRestConfig() *rest.Config { return c.restConfig } // GetClientset returns the Kubernetes clientset func (c *Client) GetClientset() *kubernetes.Clientset { return c.clientset } // GetDynamicClient returns the dynamic client func (c *Client) GetDynamicClient() dynamic.Interface { return c.dynamicClient } // GetDiscoveryClient returns the discovery client func (c *Client) GetDiscoveryClient() *discovery.DiscoveryClient { return c.discoveryClient } // GetNamespaceTopology returns the topology for a specific namespace func (c *Client) GetNamespaceTopology(ctx context.Context, namespace string) (*NamespaceTopology, error) { return c.ResourceMapper.GetNamespaceTopology(ctx, namespace) } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/api/server.go: -------------------------------------------------------------------------------- ```go package api import ( "context" "net/http" "strings" "time" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/argocd" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/correlator" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/gitlab" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/k8s" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/mcp" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/config" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" "github.com/gorilla/mux" ) // Server represents the API server type Server struct { router *mux.Router server *http.Server k8sClient *k8s.Client argoClient *argocd.Client gitlabClient *gitlab.Client mcpHandler *mcp.ProtocolHandler troubleshootCorrelator *correlator.TroubleshootCorrelator resourceMapper *k8s.ResourceMapper config config.ServerConfig logger *logging.Logger } // NewServer creates a new API server func NewServer( cfg config.ServerConfig, k8sClient *k8s.Client, argoClient *argocd.Client, gitlabClient *gitlab.Client, mcpHandler *mcp.ProtocolHandler, troubleshootCorrelator *correlator.TroubleshootCorrelator, logger *logging.Logger, ) *Server { if logger == nil { logger = logging.NewLogger().Named("api") } server := &Server{ router: mux.NewRouter(), k8sClient: k8sClient, argoClient: argoClient, gitlabClient: gitlabClient, mcpHandler: mcpHandler, troubleshootCorrelator: troubleshootCorrelator, config: cfg, logger: logger, } // Initialize resource mapper server.resourceMapper = server.k8sClient.ResourceMapper // Set up routes server.setupRoutes() server.setupNamespaceRoutes() return server } // Start starts the HTTP server func (s *Server) Start(ctx context.Context) error { s.server = &http.Server{ Addr: s.config.Address, Handler: s.loggingMiddleware(s.router), ReadTimeout: time.Duration(s.config.ReadTimeout) * time.Second, WriteTimeout: time.Duration(s.config.WriteTimeout) * time.Second, } // Channel for server errors errCh := make(chan error, 1) // Start server in a goroutine go func() { s.logger.Info("Starting HTTP server", "address", s.config.Address) if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { errCh <- err } }() // Wait for context cancellation or server error select { case <-ctx.Done(): s.logger.Info("Context cancelled, shutting down server") return s.Shutdown(context.Background()) case err := <-errCh: return err } } // Shutdown gracefully shuts down the server func (s *Server) Shutdown(ctx context.Context) error { s.logger.Info("Shutting down HTTP server") return s.server.Shutdown(ctx) } // Middleware functions // loggingMiddleware logs information about each request func (s *Server) loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() // Create a response writer that captures status code rw := &responseWriter{w, http.StatusOK} // Call the next handler next.ServeHTTP(rw, r) // Log the request s.logger.Info("HTTP request", "method", r.Method, "path", r.URL.Path, "status", rw.statusCode, "duration", time.Since(start), "remote_addr", r.RemoteAddr, "user_agent", r.UserAgent(), ) }) } // authMiddleware checks for valid authentication func (s *Server) authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Get API key from header apiKey := r.Header.Get("X-API-Key") // Check for bearer token if API key is not provided if apiKey == "" { authHeader := r.Header.Get("Authorization") if authHeader == "" { s.respondWithError(w, http.StatusUnauthorized, "Authentication required", nil) return } // Extract token parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { s.respondWithError(w, http.StatusUnauthorized, "Invalid authorization format", nil) return } apiKey = parts[1] } // Validate the API key against the configured key if apiKey != s.config.Auth.APIKey { s.respondWithError(w, http.StatusUnauthorized, "Invalid API key", nil) return } // Call the next handler next.ServeHTTP(w, r) }) } // Custom response writer to capture status code type responseWriter struct { http.ResponseWriter statusCode int } // WriteHeader captures the status code func (rw *responseWriter) WriteHeader(code int) { rw.statusCode = code rw.ResponseWriter.WriteHeader(code) } // Initialize the resourceMapper in NewServer func (s *Server) initResourceMapper() { if s.k8sClient != nil { s.resourceMapper = k8s.NewResourceMapper(s.k8sClient) s.logger.Info("Resource mapper initialized") } else { s.logger.Warn("Cannot initialize resource mapper - K8s client is nil") } } func (s *Server) corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Set CORS headers w.Header().Set("Access-Control-Allow-Origin", "*") // Allow all origins in development w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") // If this is a preflight request, respond with 200 OK if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } // Call the next handler next.ServeHTTP(w, r) }) } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/gitlab/repositories.go: -------------------------------------------------------------------------------- ```go package gitlab import ( "io" "context" "encoding/json" "fmt" "net/http" "net/url" "time" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" ) // ListProjects returns a list of GitLab projects func (c *Client) ListProjects(ctx context.Context) ([]models.GitLabProject, error) { c.logger.Debug("Listing projects") // Create endpoint with query parameters endpoint := "projects" u, err := url.Parse(endpoint) if err != nil { return nil, fmt.Errorf("invalid endpoint: %w", err) } q := u.Query() q.Set("membership", "true") q.Set("order_by", "updated_at") q.Set("sort", "desc") q.Set("per_page", "100") u.RawQuery = q.Encode() resp, err := c.doRequest(ctx, http.MethodGet, u.String(), nil) if err != nil { return nil, err } defer resp.Body.Close() var projects []models.GitLabProject if err := json.NewDecoder(resp.Body).Decode(&projects); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } c.logger.Debug("Listed projects", "count", len(projects)) return projects, nil } // GetProject returns details about a specific GitLab project func (c *Client) GetProject(ctx context.Context, projectID string) (*models.GitLabProject, error) { c.logger.Debug("Getting project", "projectID", projectID) endpoint := fmt.Sprintf("projects/%s", url.PathEscape(projectID)) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var project models.GitLabProject if err := json.NewDecoder(resp.Body).Decode(&project); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &project, nil } // GetProjectByPath returns a project by its path (namespace/project-name) func (c *Client) GetProjectByPath(ctx context.Context, path string) (*models.GitLabProject, error) { c.logger.Debug("Getting project by path", "path", path) // GitLab API requires path to be URL encoded encodedPath := url.QueryEscape(path) endpoint := fmt.Sprintf("projects/%s", encodedPath) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var project models.GitLabProject if err := json.NewDecoder(resp.Body).Decode(&project); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &project, nil } // GetCommit returns details about a specific commit func (c *Client) GetCommit(ctx context.Context, projectID, sha string) (*models.GitLabCommit, error) { c.logger.Debug("Getting commit", "projectID", projectID, "sha", sha) endpoint := fmt.Sprintf("projects/%s/repository/commits/%s", url.PathEscape(projectID), url.PathEscape(sha)) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var commit models.GitLabCommit if err := json.NewDecoder(resp.Body).Decode(&commit); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &commit, nil } // GetCommitDiff returns the changes in a specific commit func (c *Client) GetCommitDiff(ctx context.Context, projectID, sha string) ([]models.GitLabDiff, error) { c.logger.Debug("Getting commit diff", "projectID", projectID, "sha", sha) endpoint := fmt.Sprintf("projects/%s/repository/commits/%s/diff", url.PathEscape(projectID), url.PathEscape(sha)) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var diffs []models.GitLabDiff if err := json.NewDecoder(resp.Body).Decode(&diffs); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } c.logger.Debug("Got commit diff", "projectID", projectID, "sha", sha, "count", len(diffs)) return diffs, nil } // GetFileContent returns the content of a file at a specific commit func (c *Client) GetFileContent(ctx context.Context, projectID, filePath, ref string) (string, error) { c.logger.Debug("Getting file content", "projectID", projectID, "filePath", filePath, "ref", ref) encodedFilePath := url.PathEscape(filePath) endpoint := fmt.Sprintf("projects/%s/repository/files/%s/raw", url.PathEscape(projectID), encodedFilePath) // Add ref parameter if provided if ref != "" { u, err := url.Parse(endpoint) if err != nil { return "", fmt.Errorf("invalid endpoint: %w", err) } q := u.Query() q.Set("ref", ref) u.RawQuery = q.Encode() endpoint = u.String() } resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return "", err } defer resp.Body.Close() content, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("failed to read file content: %w", err) } return string(content), nil } // FindRecentChanges finds recent changes (commits) for a project func (c *Client) FindRecentChanges(ctx context.Context, projectID string, since time.Time) ([]models.GitLabCommit, error) { c.logger.Debug("Finding recent changes", "projectID", projectID, "since", since.Format(time.RFC3339)) // Format time as ISO 8601 sinceStr := since.Format(time.RFC3339) // Create endpoint with query parameters endpoint := fmt.Sprintf("projects/%s/repository/commits", url.PathEscape(projectID)) u, err := url.Parse(endpoint) if err != nil { return nil, fmt.Errorf("invalid endpoint: %w", err) } q := u.Query() q.Set("since", sinceStr) q.Set("per_page", "20") u.RawQuery = q.Encode() resp, err := c.doRequest(ctx, http.MethodGet, u.String(), nil) if err != nil { return nil, err } defer resp.Body.Close() var commits []models.GitLabCommit if err := json.NewDecoder(resp.Body).Decode(&commits); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } c.logger.Debug("Found recent changes", "projectID", projectID, "count", len(commits)) return commits, nil } ``` -------------------------------------------------------------------------------- /docs/src/components/Footer.astro: -------------------------------------------------------------------------------- ``` --- const currentYear = new Date().getFullYear(); --- <footer class="bg-slate-100 dark:bg-slate-900 border-t border-slate-200 dark:border-slate-800 py-12"> <div class="container mx-auto px-4"> <div class="grid grid-cols-1 md:grid-cols-4 gap-8"> <div> <h3 class="font-semibold text-lg mb-4">Kubernetes Claude MCP</h3> <p class="text-slate-600 dark:text-slate-400"> An advanced Model Context Protocol server for Kubernetes, integrating Claude AI with GitOps workflows. </p> </div> <div> <h3 class="font-semibold text-lg mb-4">Documentation</h3> <ul class="space-y-2"> <li><a href="/docs/introduction" class="text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">Introduction</a></li> <li><a href="/docs/quick-start" class="text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">Quick Start</a></li> <li><a href="/docs/installation" class="text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">Installation</a></li> <li><a href="/docs/api-overview" class="text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">API Reference</a></li> </ul> </div> <div> <h3 class="font-semibold text-lg mb-4">Community</h3> <ul class="space-y-2"> <li><a href="https://github.com/blankcut/kubernetes-mcp-server" target="_blank" rel="noopener noreferrer" class="text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">GitHub</a></li> <li><a href="https://github.com/blankcut/kubernetes-mcp-server/issues" target="_blank" rel="noopener noreferrer" class="text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">Issues</a></li> <li><a href="https://github.com/blankcut/kubernetes-mcp-server/discussions" target="_blank" rel="noopener noreferrer" class="text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">Discussions</a></li> <li><a href="https://github.com/blankcut/kubernetes-mcp-server/releases" target="_blank" rel="noopener noreferrer" class="text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">Releases</a></li> </ul> </div> <div> <h3 class="font-semibold text-lg mb-4">Legal</h3> <ul class="space-y-2"> <li><a href="/license" class="text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">License</a></li> <li><a href="/privacy" class="text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">Privacy Policy</a></li> <li><a href="/terms" class="text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">Terms of Service</a></li> </ul> </div> </div> <div class="mt-12 pt-8 border-t border-slate-200 dark:border-slate-800 flex flex-col sm:flex-row justify-between items-center"> <p class="text-slate-600 dark:text-slate-400 text-sm mb-4 sm:mb-0"> © {currentYear} Blank Cut Inc. All rights reserved. </p> <div class="flex space-x-4"> <a href="https://github.com/blankcut" target="_blank" rel="noopener noreferrer" class="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"> <span class="sr-only">GitHub</span> <svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path> </svg> </a> <a href="https://twitter.com/blankcut" target="_blank" rel="noopener noreferrer" class="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"> <span class="sr-only">Twitter</span> <svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"></path> </svg> </a> <a href="https://www.linkedin.com/company/blankcut" target="_blank" rel="noopener noreferrer" class="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"> <span class="sr-only">LinkedIn</span> <svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path> </svg> </a> </div> </div> </div> </footer> ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/models/gitlab.go: -------------------------------------------------------------------------------- ```go package models // GitLabProject represents a GitLab project type GitLabProject struct { ID int `json:"id"` Name string `json:"name"` Path string `json:"path"` PathWithNamespace string `json:"path_with_namespace"` WebURL string `json:"web_url"` DefaultBranch string `json:"default_branch"` Visibility string `json:"visibility"` } // GitLabPipeline represents a GitLab CI/CD pipeline type GitLabPipeline struct { ID int `json:"id"` Status string `json:"status"` Ref string `json:"ref"` SHA string `json:"sha"` WebURL string `json:"web_url"` CreatedAt interface{} `json:"created_at"` UpdatedAt interface{} `json:"updated_at"` } // GitLabJob represents a job in a GitLab CI/CD pipeline type GitLabJob struct { ID int `json:"id"` Status string `json:"status"` Stage string `json:"stage"` Name string `json:"name"` Ref string `json:"ref"` CreatedAt int64 `json:"created_at"` StartedAt int64 `json:"started_at"` FinishedAt int64 `json:"finished_at"` Pipeline struct { ID int `json:"id"` } `json:"pipeline"` } // GitLabCommit represents a Git commit in GitLab type GitLabCommit struct { ID string `json:"id"` ShortID string `json:"short_id"` Title string `json:"title"` Message string `json:"message"` AuthorName string `json:"author_name"` AuthorEmail string `json:"author_email"` CommitterName string `json:"committer_name"` CommitterEmail string `json:"committer_email"` CreatedAt interface{} `json:"created_at"` ParentIDs []string `json:"parent_ids"` WebURL string `json:"web_url"` } // GitLabDiff represents a file diff in a commit type GitLabDiff struct { OldPath string `json:"old_path"` NewPath string `json:"new_path"` Diff string `json:"diff"` NewFile bool `json:"new_file"` RenamedFile bool `json:"renamed_file"` DeletedFile bool `json:"deleted_file"` } // GitLabDeployment represents a deployment in GitLab type GitLabDeployment struct { ID int `json:"id"` Status string `json:"status"` CreatedAt interface{} `json:"created_at"` UpdatedAt interface{} `json:"updated_at"` Environment struct { ID int `json:"id"` Name string `json:"name"` Slug string `json:"slug"` State string `json:"state"` } `json:"environment"` Deployable struct { ID int `json:"id"` Status string `json:"status"` Stage string `json:"stage"` Name string `json:"name"` Ref string `json:"ref"` Tag bool `json:"tag"` Pipeline struct { ID int `json:"id"` Status string `json:"status"` } `json:"pipeline"` } `json:"deployable"` Commit GitLabCommit `json:"commit"` } // GitLabRelease represents a release in GitLab type GitLabRelease struct { TagName string `json:"tag_name"` Description string `json:"description"` CreatedAt int64 `json:"created_at"` Assets struct { Links []struct { Name string `json:"name"` URL string `json:"url"` } `json:"links"` } `json:"assets"` } // GitLabMergeRequest represents a merge request in GitLab type GitLabMergeRequest struct { ID int `json:"id"` IID int `json:"iid"` ProjectID int `json:"project_id"` Title string `json:"title"` Description string `json:"description"` State string `json:"state"` MergedBy *struct { ID int `json:"id"` Username string `json:"username"` Name string `json:"name"` } `json:"merged_by,omitempty"` MergedAt interface{} `json:"merged_at"` CreatedAt interface{} `json:"created_at"` UpdatedAt interface{} `json:"updated_at"` TargetBranch string `json:"target_branch"` SourceBranch string `json:"source_branch"` Author struct { ID int `json:"id"` Username string `json:"username"` Name string `json:"name"` } `json:"author"` Assignees []struct { ID int `json:"id"` Username string `json:"username"` Name string `json:"name"` } `json:"assignees"` SourceProjectID int `json:"source_project_id"` TargetProjectID int `json:"target_project_id"` WebURL string `json:"web_url"` MergeStatus string `json:"merge_status"` Changes []struct { OldPath string `json:"old_path"` NewPath string `json:"new_path"` Diff string `json:"diff"` NewFile bool `json:"new_file"` RenamedFile bool `json:"renamed_file"` DeletedFile bool `json:"deleted_file"` } `json:"changes,omitempty"` DiffRefs struct { BaseSHA string `json:"base_sha"` HeadSHA string `json:"head_sha"` StartSHA string `json:"start_sha"` } `json:"diff_refs"` UserNotesCount int `json:"user_notes_count"` HasConflicts bool `json:"has_conflicts"` Pipelines []GitLabPipeline `json:"pipelines,omitempty"` MergeRequestContext struct { CommitMessages []string `json:"commit_messages,omitempty"` AffectedFiles []string `json:"affected_files,omitempty"` HelmChartAffected bool `json:"helm_chart_affected,omitempty"` KubernetesManifest bool `json:"kubernetes_manifests_affected,omitempty"` } `json:"merge_request_context,omitempty"` } // GitLabMergeRequestComment represents a comment on a GitLab merge request type GitLabMergeRequestComment struct { ID int `json:"id"` Body string `json:"body"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` System bool `json:"system"` NoteableID int `json:"noteable_id"` NoteableType string `json:"noteable_type"` Author struct { ID int `json:"id"` Username string `json:"username"` Name string `json:"name"` } `json:"author"` } // GitLabMergeRequestApproval represents approval information for a merge request type GitLabMergeRequestApproval struct { ID int `json:"id"` ProjectID int `json:"project_id"` ApprovalRequired bool `json:"approval_required"` ApprovedBy []struct { User struct { ID int `json:"id"` Username string `json:"username"` Name string `json:"name"` } `json:"user"` } `json:"approved_by"` ApprovalsRequired int `json:"approvals_required"` ApprovalsLeft int `json:"approvals_left"` } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/helm/parser.go: -------------------------------------------------------------------------------- ```go // internal/helm/parser.go package helm import ( "bytes" "context" "fmt" "os" "os/exec" "path/filepath" "strings" "gopkg.in/yaml.v2" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" ) // Parser handles Helm chart parsing and analysis type Parser struct { workDir string logger *logging.Logger } // NewParser creates a new Helm chart parser func NewParser(logger *logging.Logger) *Parser { if logger == nil { logger = logging.NewLogger().Named("helm") } // Create a temporary working directory workDir, err := os.MkdirTemp("", "helm-parser-*") if err != nil { logger.Error("Failed to create working directory", "error", err) return nil } return &Parser{ workDir: workDir, logger: logger, } } // ParseChart renders a Helm chart and returns the resulting Kubernetes manifests func (p *Parser) ParseChart(ctx context.Context, chartPath string, valuesFiles []string, values map[string]interface{}) ([]string, error) { p.logger.Debug("Parsing Helm chart", "chartPath", chartPath, "valuesFiles", valuesFiles) // Check if helm command is available if _, err := exec.LookPath("helm"); err != nil { return nil, fmt.Errorf("helm command not found in PATH: %w", err) } // Prepare helm template command args := []string{"template", "release", chartPath} // Add values files for _, valuesFile := range valuesFiles { args = append(args, "-f", valuesFile) } // Add --set arguments for values for k, v := range values { args = append(args, "--set", fmt.Sprintf("%s=%v", k, v)) } // Execute helm template command cmd := exec.CommandContext(ctx, "helm", args...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr p.logger.Debug("Executing helm template command", "args", args) err := cmd.Run() if err != nil { return nil, fmt.Errorf("failed to execute helm template: %s, error: %w", stderr.String(), err) } // Parse the rendered templates manifests := p.splitYAMLDocuments(stdout.String()) p.logger.Debug("Parsed Helm chart", "manifestCount", len(manifests)) return manifests, nil } // WriteChartFiles writes chart files to the working directory for processing func (p *Parser) WriteChartFiles(files map[string]string) (string, error) { chartDir := filepath.Join(p.workDir, "chart") // Create chart directory if not exists if err := os.MkdirAll(chartDir, 0755); err != nil { return "", fmt.Errorf("failed to create chart directory: %w", err) } // Write files for path, content := range files { fullPath := filepath.Join(chartDir, path) dirPath := filepath.Dir(fullPath) // Create directories if err := os.MkdirAll(dirPath, 0755); err != nil { return "", fmt.Errorf("failed to create directory %s: %w", dirPath, err) } // Write file if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil { return "", fmt.Errorf("failed to write file %s: %w", fullPath, err) } } return chartDir, nil } // WriteValuesFile writes a values file to the working directory func (p *Parser) WriteValuesFile(content string) (string, error) { valuesFile := filepath.Join(p.workDir, "values.yaml") if err := os.WriteFile(valuesFile, []byte(content), 0644); err != nil { return "", fmt.Errorf("failed to write values file: %w", err) } return valuesFile, nil } // ParseYAML parses a YAML file to extract Kubernetes resources func (p *Parser) ParseYAML(content string) ([]map[string]interface{}, error) { // Split YAML documents documents := p.splitYAMLDocuments(content) var resources []map[string]interface{} for _, doc := range documents { // Parse each document as YAML var resource map[string]interface{} // *** Add this line (or similar depending on your library) *** err := yaml.Unmarshal([]byte(doc), &resource) // Use your chosen library's unmarshal function if err != nil { // Handle the error appropriately, maybe log it and continue p.logger.Warn("Failed to unmarshal YAML document", "error", err) continue } // Add to resources if it's a valid Kubernetes resource (and not empty after parsing) if resource != nil { resources = append(resources, resource) } } return resources, nil } // splitYAMLDocuments splits multi-document YAML into individual documents func (p *Parser) splitYAMLDocuments(content string) []string { // Simple implementation - in a real system, use a proper YAML parser var documents []string // Split on document separator parts := strings.Split(content, "---") for _, part := range parts { // Trim whitespace trimmed := strings.TrimSpace(part) if trimmed != "" { documents = append(documents, trimmed) } } return documents } // Cleanup removes temporary files func (p *Parser) Cleanup() { if p.workDir != "" { p.logger.Debug("Cleaning up working directory", "path", p.workDir) os.RemoveAll(p.workDir) } } // DiffChartVersions compares two versions of a chart and returns resources that would be affected func (p *Parser) DiffChartVersions(ctx context.Context, chartPath1, chartPath2 string, valuesFiles []string) ([]string, error) { // Render both chart versions manifests1, err := p.ParseChart(ctx, chartPath1, valuesFiles, nil) if err != nil { return nil, fmt.Errorf("failed to parse first chart version: %w", err) } manifests2, err := p.ParseChart(ctx, chartPath2, valuesFiles, nil) if err != nil { return nil, fmt.Errorf("failed to parse second chart version: %w", err) } // Compare manifests to find differences diff := p.compareManifests(manifests1, manifests2) return diff, nil } // compareManifests compares two sets of manifests and returns the names of resources that differ func (p *Parser) compareManifests(manifests1, manifests2 []string) []string { // This is a simplified implementation // In a real system, you would parse the YAML and compare by resource identifiers var changedResources []string // For now, we just assume all manifests might be affected // In a real implementation, you'd compare name/kind/namespace for _, manifest := range manifests2 { // Extract resource name and kind if strings.Contains(manifest, "kind:") && strings.Contains(manifest, "name:") { // Very simplistic parsing - would need proper YAML parsing in real code lines := strings.Split(manifest, "\n") var kind, name string for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "kind:") { kind = strings.TrimSpace(strings.TrimPrefix(line, "kind:")) } else if strings.HasPrefix(line, "name:") { name = strings.TrimSpace(strings.TrimPrefix(line, "name:")) } if kind != "" && name != "" { changedResources = append(changedResources, fmt.Sprintf("%s/%s", kind, name)) break } } } } return changedResources } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/k8s/events.go: -------------------------------------------------------------------------------- ```go package k8s import ( "context" "fmt" "sort" "strings" "time" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" ) // GetResourceEvents returns events related to a specific resource func (c *Client) GetResourceEvents(ctx context.Context, namespace, kind, name string) ([]models.K8sEvent, error) { c.logger.Debug("Getting events for resource", "namespace", namespace, "kind", kind, "name", name) // Build field selector var fieldSelector fields.Selector if namespace != "" { // For namespaced resources fieldSelector = fields.AndSelectors( fields.OneTermEqualSelector("involvedObject.name", name), fields.OneTermEqualSelector("involvedObject.kind", kind), fields.OneTermEqualSelector("involvedObject.namespace", namespace), ) } else { // For cluster-scoped resources (no namespace) fieldSelector = fields.AndSelectors( fields.OneTermEqualSelector("involvedObject.name", name), fields.OneTermEqualSelector("involvedObject.kind", kind), ) } // Get events eventList, err := c.clientset.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{ FieldSelector: fieldSelector.String(), }) if err != nil { return nil, fmt.Errorf("failed to list events: %w", err) } // Convert to our model var events []models.K8sEvent for _, event := range eventList.Items { e := models.K8sEvent{ Reason: event.Reason, Message: event.Message, Type: event.Type, Count: int(event.Count), FirstTime: event.FirstTimestamp.Time, LastTime: event.LastTimestamp.Time, Object: struct { Kind string `json:"kind"` Name string `json:"name"` Namespace string `json:"namespace"` }{ Kind: event.InvolvedObject.Kind, Name: event.InvolvedObject.Name, Namespace: event.InvolvedObject.Namespace, }, } events = append(events, e) } // Sort events by last time, most recent first sort.Slice(events, func(i, j int) bool { return events[i].LastTime.After(events[j].LastTime) }) c.logger.Debug("Got events for resource", "namespace", namespace, "kind", kind, "name", name, "count", len(events)) return events, nil } // GetNamespaceEvents returns all events in a namespace func (c *Client) GetNamespaceEvents(ctx context.Context, namespace string) ([]models.K8sEvent, error) { c.logger.Debug("Getting events for namespace", "namespace", namespace) // Get events eventList, err := c.clientset.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{}) if err != nil { return nil, fmt.Errorf("failed to list events: %w", err) } // Convert to our model var events []models.K8sEvent for _, event := range eventList.Items { e := models.K8sEvent{ Reason: event.Reason, Message: event.Message, Type: event.Type, Count: int(event.Count), FirstTime: event.FirstTimestamp.Time, LastTime: event.LastTimestamp.Time, Object: struct { Kind string `json:"kind"` Name string `json:"name"` Namespace string `json:"namespace"` }{ Kind: event.InvolvedObject.Kind, Name: event.InvolvedObject.Name, Namespace: event.InvolvedObject.Namespace, }, } events = append(events, e) } // Sort events by last time, most recent first sort.Slice(events, func(i, j int) bool { return events[i].LastTime.After(events[j].LastTime) }) c.logger.Debug("Got events for namespace", "namespace", namespace, "count", len(events)) return events, nil } // GetRecentWarningEvents returns recent warning events across all namespaces func (c *Client) GetRecentWarningEvents(ctx context.Context, timeWindow time.Duration) ([]models.K8sEvent, error) { c.logger.Debug("Getting recent warning events", "timeWindow", timeWindow) // Calculate the cutoff time cutoffTime := time.Now().Add(-timeWindow) // Get events from all namespaces eventList, err := c.clientset.CoreV1().Events("").List(ctx, metav1.ListOptions{ FieldSelector: fields.OneTermEqualSelector("type", "Warning").String(), }) if err != nil { return nil, fmt.Errorf("failed to list warning events: %w", err) } // Filter and convert to our model var events []models.K8sEvent for _, event := range eventList.Items { // Skip events older than the cutoff time if event.LastTimestamp.Time.Before(cutoffTime) { continue } e := models.K8sEvent{ Reason: event.Reason, Message: event.Message, Type: event.Type, Count: int(event.Count), FirstTime: event.FirstTimestamp.Time, LastTime: event.LastTimestamp.Time, Object: struct { Kind string `json:"kind"` Name string `json:"name"` Namespace string `json:"namespace"` }{ Kind: event.InvolvedObject.Kind, Name: event.InvolvedObject.Name, Namespace: event.InvolvedObject.Namespace, }, } events = append(events, e) } // Sort events by last time, most recent first sort.Slice(events, func(i, j int) bool { return events[i].LastTime.After(events[j].LastTime) }) c.logger.Debug("Got recent warning events", "count", len(events), "timeWindow", timeWindow) return events, nil } // GetClusterHealthEvents returns events that might indicate cluster health issues func (c *Client) GetClusterHealthEvents(ctx context.Context) ([]models.K8sEvent, error) { c.logger.Debug("Getting cluster health events") // Define keywords that might indicate cluster health issues healthIssueKeywords := []string{ "Failed", "Error", "CrashLoopBackOff", "OOMKilled", "Evicted", "NodeNotReady", "Unhealthy", "OutOfDisk", "MemoryPressure", "DiskPressure", "NetworkUnavailable", "Unschedulable", } // Build field selector for warning events fieldSelector := fields.OneTermEqualSelector("type", "Warning") // Get events from all namespaces eventList, err := c.clientset.CoreV1().Events("").List(ctx, metav1.ListOptions{ FieldSelector: fieldSelector.String(), }) if err != nil { return nil, fmt.Errorf("failed to list warning events: %w", err) } // Filter and convert to our model var events []models.K8sEvent for _, event := range eventList.Items { // Check if the event matches any health issue keywords matchesKeyword := false for _, keyword := range healthIssueKeywords { if strings.Contains(event.Reason, keyword) || strings.Contains(event.Message, keyword) { matchesKeyword = true break } } if !matchesKeyword { continue } e := models.K8sEvent{ Reason: event.Reason, Message: event.Message, Type: event.Type, Count: int(event.Count), FirstTime: event.FirstTimestamp.Time, LastTime: event.LastTimestamp.Time, Object: struct { Kind string `json:"kind"` Name string `json:"name"` Namespace string `json:"namespace"` }{ Kind: event.InvolvedObject.Kind, Name: event.InvolvedObject.Name, Namespace: event.InvolvedObject.Namespace, }, } events = append(events, e) } // Sort events by last time, most recent first sort.Slice(events, func(i, j int) bool { return events[i].LastTime.After(events[j].LastTime) }) c.logger.Debug("Got cluster health events", "count", len(events)) return events, nil } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/k8s/enhanced_client.go: -------------------------------------------------------------------------------- ```go package k8s import ( "context" "fmt" "strings" "sync" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // NamespaceResourcesCollection contains all resources in a namespace type NamespaceResourcesCollection struct { Namespace string `json:"namespace"` Resources map[string][]unstructured.Unstructured `json:"resources"` Stats map[string]int `json:"stats"` } // ResourceDetails contains detailed information about a resource type ResourceDetails struct { Resource *unstructured.Unstructured `json:"resource"` Events []interface{} `json:"events"` Relationships []ResourceRelationship `json:"relationships"` Metrics map[string]interface{} `json:"metrics"` } // GetAllNamespaceResources retrieves all resources in a namespace func (c *Client) GetAllNamespaceResources(ctx context.Context, namespace string) (*NamespaceResourcesCollection, error) { c.logger.Info("Getting all resources in namespace", "namespace", namespace) collection := &NamespaceResourcesCollection{ Namespace: namespace, Resources: make(map[string][]unstructured.Unstructured), Stats: make(map[string]int), } // Discover all available resource types resources, err := c.discoveryClient.ServerPreferredResources() if err != nil { return nil, fmt.Errorf("failed to get server resources: %w", err) } // Use a wait group to parallelize resource collection var wg sync.WaitGroup var mu sync.Mutex // Mutex for safely updating the collection // Collect resources for each API group concurrently for _, resourceList := range resources { wg.Add(1) go func(resourceList *metav1.APIResourceList) { defer wg.Done() gv, err := schema.ParseGroupVersion(resourceList.GroupVersion) if err != nil { c.logger.Warn("Failed to parse group version", "groupVersion", resourceList.GroupVersion) return } for _, r := range resourceList.APIResources { // Skip resources that can't be listed or aren't namespaced if !strings.Contains(r.Verbs.String(), "list") || !r.Namespaced { continue } // Skip subresources (contains slash) if strings.Contains(r.Name, "/") { continue } // Build GVR for this resource type gvr := schema.GroupVersionResource{ Group: gv.Group, Version: gv.Version, Resource: r.Name, } // List resources of this type list, err := c.dynamicClient.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{}) if err != nil { c.logger.Warn("Failed to list resources", "namespace", namespace, "resource", r.Name, "error", err) continue } // Skip if no resources found if len(list.Items) == 0 { continue } // Add to collection with thread safety mu.Lock() collection.Resources[r.Kind] = list.Items collection.Stats[r.Kind] = len(list.Items) mu.Unlock() } }(resourceList) } // Wait for all resource collections to complete wg.Wait() c.logger.Info("Collected all namespace resources", "namespace", namespace, "resourceTypes", len(collection.Resources), "totalResources", c.countTotalResources(collection.Stats)) return collection, nil } // countTotalResources counts the total number of resources across all types func (c *Client) countTotalResources(stats map[string]int) int { total := 0 for _, count := range stats { total += count } return total } // GetResourceDetails gets detailed information about a specific resource func (c *Client) GetResourceDetails(ctx context.Context, kind, namespace, name string) (*ResourceDetails, error) { c.logger.Info("Getting resource details", "kind", kind, "namespace", namespace, "name", name) // Get the resource resource, err := c.GetResource(ctx, kind, namespace, name) if err != nil { return nil, fmt.Errorf("failed to get resource: %w", err) } // Initialize resource details details := &ResourceDetails{ Resource: resource, Metrics: make(map[string]interface{}), } // Get resource events events, err := c.GetResourceEvents(ctx, namespace, kind, name) if err != nil { c.logger.Warn("Failed to get resource events", "error", err) } else { // Convert events to interface for JSON serialization eventsInterface := make([]interface{}, len(events)) for i, event := range events { eventMap := map[string]interface{}{ "reason": event.Reason, "message": event.Message, "type": event.Type, "count": event.Count, "firstTime": event.FirstTime, "lastTime": event.LastTime, "object": map[string]string{ "kind": event.Object.Kind, "name": event.Object.Name, "namespace": event.Object.Namespace, }, } eventsInterface[i] = eventMap } details.Events = eventsInterface } // Add resource-specific metrics c.addResourceMetrics(ctx, resource, details) return details, nil } // addResourceMetrics adds resource-specific metrics based on resource type func (c *Client) addResourceMetrics(ctx context.Context, resource *unstructured.Unstructured, details *ResourceDetails) { kind := resource.GetKind() switch kind { case "Pod": // Add container statuses containers, found, _ := unstructured.NestedSlice(resource.Object, "spec", "containers") if found { details.Metrics["containerCount"] = len(containers) } // Add status phase phase, found, _ := unstructured.NestedString(resource.Object, "status", "phase") if found { details.Metrics["phase"] = phase } // Add restart counts containerStatuses, found, _ := unstructured.NestedSlice(resource.Object, "status", "containerStatuses") if found { totalRestarts := 0 for _, cs := range containerStatuses { containerStatus, ok := cs.(map[string]interface{}) if !ok { continue } restarts, found, _ := unstructured.NestedInt64(containerStatus, "restartCount") if found { totalRestarts += int(restarts) } } details.Metrics["totalRestarts"] = totalRestarts } case "Deployment", "StatefulSet", "DaemonSet", "ReplicaSet": // Add replica counts replicas, found, _ := unstructured.NestedInt64(resource.Object, "spec", "replicas") if found { details.Metrics["desiredReplicas"] = replicas } availableReplicas, found, _ := unstructured.NestedInt64(resource.Object, "status", "availableReplicas") if found { details.Metrics["availableReplicas"] = availableReplicas } readyReplicas, found, _ := unstructured.NestedInt64(resource.Object, "status", "readyReplicas") if found { details.Metrics["readyReplicas"] = readyReplicas } if kind == "Deployment" { // Add deployment strategy strategy, found, _ := unstructured.NestedString(resource.Object, "spec", "strategy", "type") if found { details.Metrics["strategy"] = strategy } } case "Service": // Add service type serviceType, found, _ := unstructured.NestedString(resource.Object, "spec", "type") if found { details.Metrics["type"] = serviceType } // Add port count ports, found, _ := unstructured.NestedSlice(resource.Object, "spec", "ports") if found { details.Metrics["portCount"] = len(ports) } case "PersistentVolumeClaim": // Add storage capacity capacity, found, _ := unstructured.NestedString(resource.Object, "spec", "resources", "requests", "storage") if found { details.Metrics["requestedStorage"] = capacity } // Add access modes accessModes, found, _ := unstructured.NestedStringSlice(resource.Object, "spec", "accessModes") if found { details.Metrics["accessModes"] = accessModes } // Add phase phase, found, _ := unstructured.NestedString(resource.Object, "status", "phase") if found { details.Metrics["phase"] = phase } } } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/correlator/helm_correlator.go: -------------------------------------------------------------------------------- ```go // internal/correlator/helm_correlator.go package correlator import ( "context" "fmt" "path/filepath" "strings" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/gitlab" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/helm" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" ) // HelmCorrelator correlates Helm charts with Kubernetes resources type HelmCorrelator struct { gitlabClient *gitlab.Client helmParser *helm.Parser logger *logging.Logger } // NewHelmCorrelator creates a new Helm correlator func NewHelmCorrelator(gitlabClient *gitlab.Client, logger *logging.Logger) *HelmCorrelator { if logger == nil { logger = logging.NewLogger().Named("helm-correlator") } return &HelmCorrelator{ gitlabClient: gitlabClient, helmParser: helm.NewParser(logger.Named("helm")), logger: logger, } } // AnalyzeCommitHelmChanges analyzes Helm changes in a commit func (c *HelmCorrelator) AnalyzeCommitHelmChanges(ctx context.Context, projectID string, commitSHA string) ([]string, error) { c.logger.Debug("Analyzing Helm changes in commit", "projectID", projectID, "commitSHA", commitSHA) // Get commit diff diffs, err := c.gitlabClient.GetCommitDiff(ctx, projectID, commitSHA) if err != nil { return nil, fmt.Errorf("failed to get commit diff: %w", err) } // Identify Helm chart changes helmCharts := c.identifyHelmCharts(diffs) if len(helmCharts) == 0 { c.logger.Debug("No Helm chart changes found in commit") return nil, nil } // Analyze each chart var affectedResources []string for chartPath, files := range helmCharts { resources, err := c.analyzeHelmChart(ctx, projectID, commitSHA, chartPath, files) if err != nil { c.logger.Warn("Failed to analyze Helm chart", "chartPath", chartPath, "error", err) continue } affectedResources = append(affectedResources, resources...) } return affectedResources, nil } // AnalyzeMergeRequestHelmChanges analyzes Helm changes in a merge request func (c *HelmCorrelator) AnalyzeMergeRequestHelmChanges(ctx context.Context, projectID string, mergeRequestIID int) ([]string, error) { c.logger.Debug("Analyzing Helm changes in merge request", "projectID", projectID, "mergeRequestIID", mergeRequestIID) // Get merge request changes mrChanges, err := c.gitlabClient.GetMergeRequestChanges(ctx, projectID, mergeRequestIID) if err != nil { return nil, fmt.Errorf("failed to get merge request changes: %w", err) } // Identify Helm chart changes var gitlabDiffs []models.GitLabDiff for _, change := range mrChanges.Changes { diff := models.GitLabDiff{ OldPath: change.OldPath, NewPath: change.NewPath, Diff: change.Diff, NewFile: change.NewFile, RenamedFile: change.RenamedFile, DeletedFile: change.DeletedFile, } gitlabDiffs = append(gitlabDiffs, diff) } helmCharts := c.identifyHelmCharts(gitlabDiffs) if len(helmCharts) == 0 { c.logger.Debug("No Helm chart changes found in merge request") return nil, nil } // Get commits in the merge request commits, err := c.gitlabClient.GetMergeRequestCommits(ctx, projectID, mergeRequestIID) if err != nil { return nil, fmt.Errorf("failed to get merge request commits: %w", err) } // Use the latest commit SHA for analysis var latestCommitSHA string if len(commits) > 0 { latestCommitSHA = commits[0].ID } else { latestCommitSHA = mrChanges.DiffRefs.HeadSHA } // Analyze each chart var affectedResources []string for chartPath, files := range helmCharts { resources, err := c.analyzeHelmChart(ctx, projectID, latestCommitSHA, chartPath, files) if err != nil { c.logger.Warn("Failed to analyze Helm chart", "chartPath", chartPath, "error", err) continue } affectedResources = append(affectedResources, resources...) } return affectedResources, nil } // identifyHelmCharts identifies Helm charts in changed files func (c *HelmCorrelator) identifyHelmCharts(diffs []models.GitLabDiff) map[string][]string { helmCharts := make(map[string][]string) for _, diff := range diffs { path := diff.NewPath // Skip deleted files if diff.DeletedFile { continue } // Check if it's a Helm-related file if strings.Contains(path, "Chart.yaml") || strings.Contains(path, "values.yaml") || (strings.Contains(path, "templates/") && strings.HasSuffix(path, ".yaml")) { // Extract chart path (parent directory of Chart.yaml or parent's parent for templates) chartPath := filepath.Dir(path) if strings.Contains(path, "templates/") { chartPath = filepath.Dir(filepath.Dir(path)) } // Add to chart files if _, exists := helmCharts[chartPath]; !exists { helmCharts[chartPath] = []string{} } helmCharts[chartPath] = append(helmCharts[chartPath], path) } } return helmCharts } // analyzeHelmChart analyzes changes in a Helm chart func (c *HelmCorrelator) analyzeHelmChart(ctx context.Context, projectID, commitSHA, chartPath string, changedFiles []string) ([]string, error) { c.logger.Debug("Analyzing Helm chart", "chartPath", chartPath, "changedFiles", changedFiles) // Determine chart structure chartFiles := make(map[string]string) // Get Chart.yaml chartYaml, err := c.gitlabClient.GetFileContent(ctx, projectID, fmt.Sprintf("%s/Chart.yaml", chartPath), commitSHA) if err != nil { c.logger.Warn("Failed to get Chart.yaml", "error", err) // Try to continue without Chart.yaml } else { chartFiles["Chart.yaml"] = chartYaml } // Get values.yaml valuesYaml, err := c.gitlabClient.GetFileContent(ctx, projectID, fmt.Sprintf("%s/values.yaml", chartPath), commitSHA) if err != nil { c.logger.Warn("Failed to get values.yaml", "error", err) // Try to continue without values.yaml } else { chartFiles["values.yaml"] = valuesYaml } // Get template files for _, file := range changedFiles { if strings.Contains(file, "templates/") { content, err := c.gitlabClient.GetFileContent(ctx, projectID, file, commitSHA) if err != nil { c.logger.Warn("Failed to get template file", "file", file, "error", err) continue } // Store template file relative to chart path relPath := strings.TrimPrefix(file, chartPath+"/") chartFiles[relPath] = content } } // Write chart files to disk for processing chartDir, err := c.helmParser.WriteChartFiles(chartFiles) if err != nil { return nil, fmt.Errorf("failed to write chart files: %w", err) } // Parse chart to get manifests manifests, err := c.helmParser.ParseChart(ctx, chartDir, nil, nil) if err != nil { return nil, fmt.Errorf("failed to parse chart: %w", err) } // Extract resources from manifests var resources []string for _, manifest := range manifests { // Extract resource information kind, name, namespace := c.extractResourceInfo(manifest) if kind != "" && name != "" { resource := fmt.Sprintf("%s/%s", kind, name) if namespace != "" { resource = fmt.Sprintf("%s/%s/%s", namespace, kind, name) } resources = append(resources, resource) } } c.logger.Debug("Analyzed Helm chart", "chartPath", chartPath, "resourceCount", len(resources)) return resources, nil } // extractResourceInfo extracts kind, name, and namespace from a YAML manifest func (c *HelmCorrelator) extractResourceInfo(manifest string) (kind, name, namespace string) { // Simple parsing - in a real implementation, use proper YAML parsing lines := strings.Split(manifest, "\n") for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "kind:") { kind = strings.TrimSpace(strings.TrimPrefix(line, "kind:")) } else if strings.HasPrefix(line, "name:") { name = strings.TrimSpace(strings.TrimPrefix(line, "name:")) } else if strings.HasPrefix(line, "namespace:") { namespace = strings.TrimSpace(strings.TrimPrefix(line, "namespace:")) } } return kind, name, namespace } // Cleanup cleans up temporary resources func (c *HelmCorrelator) Cleanup() { if c.helmParser != nil { c.helmParser.Cleanup() } } ``` -------------------------------------------------------------------------------- /docs/src/content/docs/model-context-protocol.md: -------------------------------------------------------------------------------- ```markdown --- title: Model Context Protocol description: Learn about the Model Context Protocol (MCP) and how it powers AI-driven analysis of Kubernetes and GitOps workflows. date: 2025-03-01 order: 7 tags: ['concepts', 'architecture'] --- # Model Context Protocol The Model Context Protocol (MCP) is the core technology behind the Kubernetes Claude MCP server. It enables the collection, correlation, and presentation of rich contextual information to Claude AI, allowing for deep analysis and troubleshooting of complex Kubernetes environments. ## What is MCP? MCP is a framework for providing structured context to large language models (LLMs) like Claude. It solves a fundamental challenge when using AI to analyze complex systems: how to collect and structure all the relevant information about a system so that an AI can understand the complete picture. In the context of Kubernetes and GitOps: - **Complete Context**: MCP gathers comprehensive information about resources, their relationships, history, and current state. - **Cross-System Correlation**: It correlates information from Kubernetes, ArgoCD, GitLab, and other systems. - **Intelligent Filtering**: It filters and prioritizes information to focus on what's most relevant. - **Structured Formatting**: It presents information in a way that maximizes Claude's understanding. ## Core Components of MCP The MCP framework consists of several key components: ### 1. Context Collection The first step of MCP is gathering comprehensive information about the system being analyzed. For Kubernetes environments, this includes: - **Resource Definitions**: The complete YAML/JSON specifications of resources - **Resource Status**: Current runtime status information - **Events**: Related Kubernetes events - **Logs**: Container logs for relevant pods - **Relationships**: Parent-child relationships between resources - **History**: Deployment history, changes, and previous states - **GitOps Context**: ArgoCD sync status, GitLab commits, pipelines ### 2. Context Correlation Once data is collected, MCP correlates information across different systems: - **Resource to Git**: Which Git repository, branch, and files define a resource - **Resource to CI/CD**: Which pipelines deployed a resource - **Resource to Owners**: Which teams or individuals own a resource - **Dependencies**: How resources depend on each other - **Change Impact**: How changes in one system affect others ### 3. Context Formatting MCP formats the correlated information in a standardized structure: - **Hierarchical Organization**: Information is organized in a logical hierarchy - **Relevance Sorting**: Most important information is presented first - **Cross-References**: Clear references between related pieces of information - **Compact Representation**: Information is presented efficiently to maximize context window usage ### 4. Context Presentation Finally, MCP presents the formatted context to Claude for analysis: - **System Prompt**: Instructs Claude on how to interpret the context - **User Query**: Focuses Claude's analysis on specific questions or issues - **Analysis Parameters**: Controls the depth, breadth, and style of analysis ## MCP in Action Here's a simplified view of how MCP works when troubleshooting a Kubernetes deployment: 1. **User Query**: "Why is my deployment not scaling?" 2. **Context Collection**: MCP gathers information about the deployment, related pods, events, logs, node resources, and GitOps configurations. 3. **Context Correlation**: MCP connects the deployment to its ArgoCD application and recent GitLab commits. 4. **Context Formatting**: The information is structured in a hierarchical format that prioritizes scaling-related details. 5. **Claude Analysis**: Claude analyzes the context and identifies that the deployment can't scale because of resource constraints. 6. **Response**: The user receives a detailed explanation and recommendations. ## Protocol Architecture The MCP implementation consists of several key components: ### 1. Collectors Collectors are responsible for gathering information from different sources: - **Kubernetes Collector**: Gathers resource definitions, status, and events - **ArgoCD Collector**: Gathers application definitions, sync status, and history - **GitLab Collector**: Gathers repository information, commits, and pipelines - **Log Collector**: Gathers container logs and application logs ### 2. Correlators Correlators connect information across different systems: - **GitOps Correlator**: Connects Kubernetes resources to their Git definitions - **Deployment Correlator**: Connects resources to their deployment pipelines - **Issue Correlator**: Connects observed issues to their potential causes - **Resource Correlator**: Connects resources to their related resources ### 3. Context Manager The Context Manager is responsible for organizing and formatting the context: - **Context Selection**: Determines what information to include - **Context Prioritization**: Prioritizes the most relevant information - **Context Formatting**: Formats the information for maximum effectiveness - **Context Truncation**: Ensures the context fits within Claude's context window ### 4. Protocol Handler The Protocol Handler handles the interaction with Claude: - **Prompt Generation**: Creates effective system and user prompts - **Response Processing**: Processes and formats Claude's responses - **Follow-up Management**: Handles follow-up queries and clarifications ## Example MCP Context Here's a simplified example of how MCP formats context for Claude: ``` # Kubernetes Resource: Deployment/my-app Namespace: default API Version: apps/v1 ## Specification Replicas: 5 Strategy: RollingUpdate Selector: app=my-app Template: ...truncated for brevity... ## Status Available Replicas: 3 Ready Replicas: 3 Updated Replicas: 3 Conditions: - Type: Available, Status: True - Type: Progressing, Status: True ## Recent Events 1. [Warning] FailedCreate: pods "my-app-7b9d7f8d9-" failed to fit in any node 2. [Normal] ScalingReplicaSet: Scaled up replica set my-app-7b9d7f8d9 to 5 3. [Warning] FailedScheduling: 0/3 nodes are available: insufficient cpu ## ArgoCD Application Name: my-app Sync Status: Synced Health Status: Degraded Source: https://github.com/myorg/myrepo.git Path: applications/my-app Target Revision: main ## Recent GitLab Commits 1. [2025-03-01T10:15:30Z] 7a8b9c0d: Increase replicas from 3 to 5 (John Smith) 2. [2025-02-28T15:45:20Z] 1b2c3d4e: Update resource requests (Jane Doe) ## Node Resources Total CPU Capacity: 12 cores Used CPU: 10.5 cores Available CPU: 1.5 cores ``` This formatted context makes it easy for Claude to understand the complete picture and identify that the deployment can't scale to 5 replicas because of insufficient CPU resources. ## Benefits of MCP Using the Model Context Protocol provides several key benefits: 1. **Complete Understanding**: Claude gets a holistic view of your environment. 2. **Deeper Analysis**: With more context, Claude can provide more accurate and insightful analysis. 3. **Cross-System Correlation**: Issues that span multiple systems are easier to identify. 4. **Efficient Context Usage**: Structured information maximizes the use of Claude's context window. 5. **Consistent Analysis**: Standardized context leads to more consistent analysis over time. ## Extending MCP The Model Context Protocol is designed to be extensible. You can add support for additional systems and information sources: 1. **Custom Collectors**: Implement collectors for your specific systems. 2. **Custom Correlators**: Create correlators for your organization's workflows. 3. **Context Templates**: Define custom context templates for your use cases. 4. **Prompt Templates**: Customize prompts for your specific needs. For more information on extending MCP, see the [Custom Integrations](/docs/custom-integrations) guide. ## Next Steps Now that you understand the Model Context Protocol, you can: 1. [Explore GitOps Integration](/docs/gitops-integration) to learn how MCP connects with ArgoCD and GitLab. 2. [Try Troubleshooting Resources](/docs/troubleshooting-resources) to see MCP in action. 3. [Review the API Reference](/docs/api-overview) to learn how to interact with the MCP server. ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/k8s/resources.go: -------------------------------------------------------------------------------- ```go package k8s import ( "bytes" "io" "context" "fmt" "strings" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" ) // resourceMappings maps common resource types to their API versions and kinds var resourceMappings = map[string]schema.GroupVersionResource{ "pod": {Group: "", Version: "v1", Resource: "pods"}, "deployment": {Group: "apps", Version: "v1", Resource: "deployments"}, "service": {Group: "", Version: "v1", Resource: "services"}, "configmap": {Group: "", Version: "v1", Resource: "configmaps"}, "secret": {Group: "", Version: "v1", Resource: "secrets"}, "statefulset": {Group: "apps", Version: "v1", Resource: "statefulsets"}, "daemonset": {Group: "apps", Version: "v1", Resource: "daemonsets"}, "job": {Group: "batch", Version: "v1", Resource: "jobs"}, "cronjob": {Group: "batch", Version: "v1", Resource: "cronjobs"}, "ingress": {Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"}, "namespace": {Group: "", Version: "v1", Resource: "namespaces"}, "node": {Group: "", Version: "v1", Resource: "nodes"}, "pv": {Group: "", Version: "v1", Resource: "persistentvolumes"}, "pvc": {Group: "", Version: "v1", Resource: "persistentvolumeclaims"}, } // getGVR returns the GroupVersionResource for a given resource type func (c *Client) getGVR(resourceType string) (schema.GroupVersionResource, error) { // Check if it's in our pre-defined mappings resourceType = strings.ToLower(resourceType) if gvr, ok := resourceMappings[resourceType]; ok { return gvr, nil } // Try to get it from the API discovery c.logger.Debug("Resource not in predefined mappings, discovering from API", "resourceType", resourceType) resources, err := c.discoveryClient.ServerPreferredResources() if err != nil { return schema.GroupVersionResource{}, fmt.Errorf("failed to get server resources: %w", err) } for _, list := range resources { gv, err := schema.ParseGroupVersion(list.GroupVersion) if err != nil { continue } for _, r := range list.APIResources { if strings.EqualFold(r.Name, resourceType) || strings.EqualFold(r.SingularName, resourceType) { c.logger.Debug("Found resource via API discovery", "resourceType", resourceType, "group", gv.Group, "version", gv.Version, "resource", r.Name) return schema.GroupVersionResource{ Group: gv.Group, Version: gv.Version, Resource: r.Name, }, nil } } } return schema.GroupVersionResource{}, fmt.Errorf("unknown resource type: %s", resourceType) } // GetResource retrieves a specific resource by kind, namespace, and name func (c *Client) GetResource(ctx context.Context, kind, namespace, name string) (*unstructured.Unstructured, error) { c.logger.Debug("Getting resource", "kind", kind, "namespace", namespace, "name", name) gvr, err := c.getGVR(kind) if err != nil { return nil, err } var obj *unstructured.Unstructured if namespace != "" { obj, err = c.dynamicClient.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) } else { obj, err = c.dynamicClient.Resource(gvr).Get(ctx, name, metav1.GetOptions{}) } if err != nil { return nil, fmt.Errorf("failed to get %s %s/%s: %w", kind, namespace, name, err) } return obj, nil } // ListResources lists resources of a specific type, optionally filtered by namespace func (c *Client) ListResources(ctx context.Context, kind, namespace string) ([]unstructured.Unstructured, error) { c.logger.Debug("Listing resources", "kind", kind, "namespace", namespace) gvr, err := c.getGVR(kind) if err != nil { return nil, err } var list *unstructured.UnstructuredList if namespace != "" { list, err = c.dynamicClient.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{}) } else { list, err = c.dynamicClient.Resource(gvr).List(ctx, metav1.ListOptions{}) } if err != nil { return nil, fmt.Errorf("failed to list resources: %w", err) } c.logger.Debug("Listed resources", "kind", kind, "count", len(list.Items)) return list.Items, nil } // GetPodStatus returns detailed status information for a pod func (c *Client) GetPodStatus(ctx context.Context, namespace, name string) (*models.K8sPodStatus, error) { c.logger.Debug("Getting pod status", "namespace", namespace, "name", name) pod, err := c.clientset.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("failed to get pod: %w", err) } status := &models.K8sPodStatus{ Phase: string(pod.Status.Phase), } for _, condition := range pod.Status.Conditions { status.Conditions = append(status.Conditions, struct { Type string `json:"type"` Status string `json:"status"` }{ Type: string(condition.Type), Status: string(condition.Status), }) } // Copy container statuses for _, containerStatus := range pod.Status.ContainerStatuses { cs := struct { Name string `json:"name"` Ready bool `json:"ready"` RestartCount int `json:"restartCount"` State struct { Running *struct{} `json:"running,omitempty"` Waiting *struct{} `json:"waiting,omitempty"` Terminated *struct{} `json:"terminated,omitempty"` } `json:"state"` LastState struct { Running *struct{} `json:"running,omitempty"` Waiting *struct{} `json:"waiting,omitempty"` Terminated *struct{} `json:"terminated,omitempty"` } `json:"lastState"` }{ Name: containerStatus.Name, Ready: containerStatus.Ready, RestartCount: int(containerStatus.RestartCount), } // Set state if containerStatus.State.Running != nil { cs.State.Running = &struct{}{} } if containerStatus.State.Waiting != nil { cs.State.Waiting = &struct{}{} } if containerStatus.State.Terminated != nil { cs.State.Terminated = &struct{}{} } // Set last state if containerStatus.LastTerminationState.Running != nil { cs.LastState.Running = &struct{}{} } if containerStatus.LastTerminationState.Waiting != nil { cs.LastState.Waiting = &struct{}{} } if containerStatus.LastTerminationState.Terminated != nil { cs.LastState.Terminated = &struct{}{} } status.ContainerStatuses = append(status.ContainerStatuses, cs) } return status, nil } // GetPodLogs returns logs for a specific container in a pod func (c *Client) GetPodLogs(ctx context.Context, namespace, name, container string, tailLines int64) (string, error) { c.logger.Debug("Getting pod logs", "namespace", namespace, "name", name, "container", container, "tailLines", tailLines) podLogOptions := corev1.PodLogOptions{ Container: container, } if tailLines > 0 { podLogOptions.TailLines = &tailLines } req := c.clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOptions) podLogs, err := req.Stream(ctx) if err != nil { return "", fmt.Errorf("failed to get pod logs: %w", err) } defer podLogs.Close() buf := new(bytes.Buffer) _, err = io.Copy(buf, podLogs) if err != nil { return "", fmt.Errorf("failed to read pod logs: %w", err) } return buf.String(), nil } // FindOwnerReferences finds the owner references for a resource func (c *Client) FindOwnerReferences(ctx context.Context, obj *unstructured.Unstructured) ([]unstructured.Unstructured, error) { c.logger.Debug("Finding owner references", "kind", obj.GetKind(), "name", obj.GetName(), "namespace", obj.GetNamespace()) ownerRefs := obj.GetOwnerReferences() if len(ownerRefs) == 0 { return nil, nil } var owners []unstructured.Unstructured for _, ref := range ownerRefs { c.logger.Debug("Found owner reference", "kind", ref.Kind, "name", ref.Name, "namespace", obj.GetNamespace()) gvr, err := c.getGVR(ref.Kind) if err != nil { c.logger.Warn("Failed to get GroupVersionResource for owner", "kind", ref.Kind, "error", err) continue } namespace := obj.GetNamespace() owner, err := c.dynamicClient.Resource(gvr).Namespace(namespace).Get(ctx, ref.Name, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { c.logger.Warn("Owner not found", "kind", ref.Kind, "name", ref.Name, "namespace", namespace) continue } return nil, fmt.Errorf("failed to get owner reference: %w", err) } owners = append(owners, *owner) } return owners, nil } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/gitlab/mergerequests.go: -------------------------------------------------------------------------------- ```go // internal/gitlab/mergerequests.go package gitlab import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" ) // ListMergeRequests returns a list of merge requests for a project func (c *Client) ListMergeRequests(ctx context.Context, projectID string, state string) ([]models.GitLabMergeRequest, error) { c.logger.Debug("Listing merge requests", "projectID", projectID, "state", state) // Create endpoint with query parameters endpoint := fmt.Sprintf("projects/%s/merge_requests", url.PathEscape(projectID)) u, err := url.Parse(endpoint) if err != nil { return nil, fmt.Errorf("invalid endpoint: %w", err) } q := u.Query() if state != "" { q.Set("state", state) } q.Set("order_by", "updated_at") q.Set("sort", "desc") q.Set("per_page", "20") u.RawQuery = q.Encode() resp, err := c.doRequest(ctx, http.MethodGet, u.String(), nil) if err != nil { return nil, err } defer resp.Body.Close() var mergeRequests []models.GitLabMergeRequest if err := json.NewDecoder(resp.Body).Decode(&mergeRequests); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } c.logger.Debug("Listed merge requests", "projectID", projectID, "count", len(mergeRequests)) return mergeRequests, nil } // GetMergeRequest returns details about a specific merge request func (c *Client) GetMergeRequest(ctx context.Context, projectID string, mergeRequestIID int) (*models.GitLabMergeRequest, error) { c.logger.Debug("Getting merge request", "projectID", projectID, "mergeRequestIID", mergeRequestIID) endpoint := fmt.Sprintf("projects/%s/merge_requests/%d", url.PathEscape(projectID), mergeRequestIID) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var mergeRequest models.GitLabMergeRequest if err := json.NewDecoder(resp.Body).Decode(&mergeRequest); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &mergeRequest, nil } // GetMergeRequestChanges returns the changes in a specific merge request func (c *Client) GetMergeRequestChanges(ctx context.Context, projectID string, mergeRequestIID int) (*models.GitLabMergeRequest, error) { c.logger.Debug("Getting merge request changes", "projectID", projectID, "mergeRequestIID", mergeRequestIID) endpoint := fmt.Sprintf("projects/%s/merge_requests/%d/changes", url.PathEscape(projectID), mergeRequestIID) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var mergeRequest models.GitLabMergeRequest if err := json.NewDecoder(resp.Body).Decode(&mergeRequest); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &mergeRequest, nil } // GetMergeRequestApprovals returns approval information for a merge request func (c *Client) GetMergeRequestApprovals(ctx context.Context, projectID string, mergeRequestIID int) (*models.GitLabMergeRequestApproval, error) { c.logger.Debug("Getting merge request approvals", "projectID", projectID, "mergeRequestIID", mergeRequestIID) endpoint := fmt.Sprintf("projects/%s/merge_requests/%d/approvals", url.PathEscape(projectID), mergeRequestIID) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var approvals models.GitLabMergeRequestApproval if err := json.NewDecoder(resp.Body).Decode(&approvals); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &approvals, nil } // GetMergeRequestComments returns comments on a merge request func (c *Client) GetMergeRequestComments(ctx context.Context, projectID string, mergeRequestIID int) ([]models.GitLabMergeRequestComment, error) { c.logger.Debug("Getting merge request comments", "projectID", projectID, "mergeRequestIID", mergeRequestIID) endpoint := fmt.Sprintf("projects/%s/merge_requests/%d/notes", url.PathEscape(projectID), mergeRequestIID) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var comments []models.GitLabMergeRequestComment if err := json.NewDecoder(resp.Body).Decode(&comments); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } c.logger.Debug("Got merge request comments", "projectID", projectID, "mergeRequestIID", mergeRequestIID, "count", len(comments)) return comments, nil } // GetMergeRequestCommits returns the commits in a merge request func (c *Client) GetMergeRequestCommits(ctx context.Context, projectID string, mergeRequestIID int) ([]models.GitLabCommit, error) { c.logger.Debug("Getting merge request commits", "projectID", projectID, "mergeRequestIID", mergeRequestIID) endpoint := fmt.Sprintf("projects/%s/merge_requests/%d/commits", url.PathEscape(projectID), mergeRequestIID) resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() var commits []models.GitLabCommit if err := json.NewDecoder(resp.Body).Decode(&commits); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } c.logger.Debug("Got merge request commits", "projectID", projectID, "mergeRequestIID", mergeRequestIID, "count", len(commits)) return commits, nil } // AnalyzeMergeRequest analyzes a merge request for Kubernetes/Helm changes func (c *Client) AnalyzeMergeRequest(ctx context.Context, projectID string, mergeRequestIID int) (*models.GitLabMergeRequest, error) { c.logger.Debug("Analyzing merge request", "projectID", projectID, "mergeRequestIID", mergeRequestIID) // Get basic merge request data mr, err := c.GetMergeRequest(ctx, projectID, mergeRequestIID) if err != nil { return nil, fmt.Errorf("failed to get merge request: %w", err) } // Get changes mrChanges, err := c.GetMergeRequestChanges(ctx, projectID, mergeRequestIID) if err != nil { return nil, fmt.Errorf("failed to get merge request changes: %w", err) } // Copy changes to the original merge request mr.Changes = mrChanges.Changes // Initialize context analysis mr.MergeRequestContext.AffectedFiles = make([]string, 0) mr.MergeRequestContext.HelmChartAffected = false mr.MergeRequestContext.KubernetesManifest = false // Analyze changes for _, change := range mr.Changes { mr.MergeRequestContext.AffectedFiles = append(mr.MergeRequestContext.AffectedFiles, change.NewPath) // Check for Helm charts if strings.Contains(change.NewPath, "Chart.yaml") || strings.Contains(change.NewPath, "values.yaml") || (strings.Contains(change.NewPath, "templates/") && strings.HasSuffix(change.NewPath, ".yaml")) { mr.MergeRequestContext.HelmChartAffected = true } // Check for Kubernetes manifests if strings.HasSuffix(change.NewPath, ".yaml") || strings.HasSuffix(change.NewPath, ".yml") { // Look for Kubernetes kind in the file content if strings.Contains(change.Diff, "kind:") && (strings.Contains(change.Diff, "Deployment") || strings.Contains(change.Diff, "Service") || strings.Contains(change.Diff, "ConfigMap") || strings.Contains(change.Diff, "Secret") || strings.Contains(change.Diff, "Pod")) { mr.MergeRequestContext.KubernetesManifest = true } } } // Get commits commits, err := c.GetMergeRequestCommits(ctx, projectID, mergeRequestIID) if err != nil { c.logger.Warn("Failed to get merge request commits", "error", err) } else { // Extract commit messages mr.MergeRequestContext.CommitMessages = make([]string, 0) for _, commit := range commits { mr.MergeRequestContext.CommitMessages = append(mr.MergeRequestContext.CommitMessages, commit.Title) } } return mr, nil } // CreateMergeRequestComment creates a new comment on a merge request func (c *Client) CreateMergeRequestComment(ctx context.Context, projectID string, mergeRequestIID int, body string) (*models.GitLabMergeRequestComment, error) { c.logger.Debug("Creating merge request comment", "projectID", projectID, "mergeRequestIID", mergeRequestIID) endpoint := fmt.Sprintf("projects/%s/merge_requests/%d/notes", url.PathEscape(projectID), mergeRequestIID) // Create request payload reqBody := map[string]string{ "body": body, } jsonBody, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("failed to marshal request body: %w", err) } resp, err := c.doRequest(ctx, http.MethodPost, endpoint, strings.NewReader(string(jsonBody))) if err != nil { return nil, err } defer resp.Body.Close() var comment models.GitLabMergeRequestComment if err := json.NewDecoder(resp.Body).Decode(&comment); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &comment, nil } ``` -------------------------------------------------------------------------------- /docs/src/pages/examples/index.astro: -------------------------------------------------------------------------------- ``` --- import BaseLayout from '../../layouts/BaseLayout.astro'; import CodeBlock from '../../components/CodeBlock.astro'; const exampleCategories = [ { title: "Basic API Usage", description: "Examples of common API calls for resource analysis", examples: [ { title: "Analyzing a Pod", description: "Query the status and health of a pod", code: `curl -X POST \\ -H "Content-Type: application/json" \\ -H "X-API-Key: your_api_key" \\ -d '{ "resource": "pod", "name": "my-app-pod", "namespace": "default", "query": "Is this pod healthy? What do the resource usage metrics show?" }' \\ http://mcp-server.example.com/api/v1/mcp/resource`, language: "bash" }, { title: "Checking Service Connectivity", description: "Investigate connectivity issues between services", code: `curl -X POST \\ -H "Content-Type: application/json" \\ -H "X-API-Key: your_api_key" \\ -d '{ "resource": "service", "name": "backend-service", "namespace": "default", "query": "Why can't my frontend pods connect to this service?" }' \\ http://mcp-server.example.com/api/v1/mcp/resource`, language: "bash" }, { title: "Deployment Analysis", description: "Understand why a deployment isn't scaling properly", code: `curl -X POST \\ -H "Content-Type: application/json" \\ -H "X-API-Key: your_api_key" \\ -d '{ "resource": "deployment", "name": "web-frontend", "namespace": "default", "query": "Why is this deployment not scaling to the requested replicas?" }' \\ http://mcp-server.example.com/api/v1/mcp/resource`, language: "bash" } ] }, { title: "Troubleshooting", description: "Examples for diagnosing and fixing common issues", examples: [ { title: "CrashLoopBackOff Investigation", description: "Troubleshoot a pod in CrashLoopBackOff state", code: `curl -X POST \\ -H "Content-Type: application/json" \\ -H "X-API-Key: your_api_key" \\ -d '{ "resource": "pod", "name": "crashing-pod", "namespace": "production", "query": "This pod is in CrashLoopBackOff. What's causing it and how can I fix it?" }' \\ http://mcp-server.example.com/api/v1/mcp/troubleshoot`, language: "bash" }, { title: "Ingress Issues", description: "Debug problems with an Ingress resource", code: `curl -X POST \\ -H "Content-Type: application/json" \\ -H "X-API-Key: your_api_key" \\ -d '{ "resource": "ingress", "name": "app-ingress", "namespace": "default", "query": "External users are getting 404 errors when accessing the application. What's wrong with this ingress?" }' \\ http://mcp-server.example.com/api/v1/mcp/troubleshoot`, language: "bash" }, { title: "Storage Problems", description: "Troubleshoot issues with PersistentVolumeClaims", code: `curl -X POST \\ -H "Content-Type: application/json" \\ -H "X-API-Key: your_api_key" \\ -d '{ "resource": "persistentvolumeclaim", "name": "database-storage", "namespace": "database", "query": "Why is this PVC stuck in pending state?" }' \\ http://mcp-server.example.com/api/v1/mcp/troubleshoot`, language: "bash" } ] }, { title: "GitOps Workflows", description: "Examples for CI/CD integration and GitOps analysis", examples: [ { title: "ArgoCD Application Analysis", description: "Check sync status and health of an ArgoCD application", code: `curl -X POST \\ -H "Content-Type: application/json" \\ -H "X-API-Key: your_api_key" \\ -d '{ "resource": "application", "name": "production-app", "namespace": "argocd", "query": "Is this application synced and healthy? If not, what are the issues?" }' \\ http://mcp-server.example.com/api/v1/mcp/resource`, language: "bash" }, { title: "Commit Impact Analysis", description: "Analyze how a specific commit affected the cluster", code: `curl -X POST \\ -H "Content-Type: application/json" \\ -H "X-API-Key: your_api_key" \\ -d '{ "projectId": "mygroup/myproject", "commitSha": "a1b2c3d4e5f6", "query": "What changes were made in this commit and how have they affected the deployed resources?" }' \\ http://mcp-server.example.com/api/v1/mcp/commit`, language: "bash" }, { title: "ArgoCD Sync Failure", description: "Troubleshoot why an ArgoCD application isn't syncing", code: `curl -X POST \\ -H "Content-Type: application/json" \\ -H "X-API-Key: your_api_key" \\ -d '{ "resource": "application", "name": "failing-app", "namespace": "argocd", "query": "Why is this application failing to sync? What specific errors are occurring?" }' \\ http://mcp-server.example.com/api/v1/mcp/troubleshoot`, language: "bash" } ] }, { title: "Advanced Usage", description: "Examples for more complex scenarios and integrations", examples: [ { title: "Resource Relationship Analysis", description: "Understanding dependencies between resources", code: `curl -X POST \\ -H "Content-Type: application/json" \\ -H "X-API-Key: your_api_key" \\ -d '{ "resource": "deployment", "name": "application", "namespace": "production", "query": "Create a map of all resources related to this deployment, including services, configmaps, secrets, and ingresses." }' \\ http://mcp-server.example.com/api/v1/mcp/resource`, language: "bash" }, { title: "Multi-Resource Correlation", description: "Analyze interactions between multiple resources", code: `curl -X POST \\ -H "Content-Type: application/json" \\ -H "X-API-Key: your_api_key" \\ -d '{ "query": "Analyze the connection between the frontend deployment, backend service, and redis statefulset in the web namespace. Are there any connectivity or configuration issues?" }' \\ http://mcp-server.example.com/api/v1/mcp`, language: "bash" }, { title: "GitOps Security Audit", description: "Audit resources for security issues and best practices", code: `curl -X POST \\ -H "Content-Type: application/json" \\ -H "X-API-Key: your_api_key" \\ -d '{ "resource": "namespace", "name": "production", "query": "Perform a security audit of all resources in this namespace. Check for security best practices, RBAC issues, and potential vulnerabilities." }' \\ http://mcp-server.example.com/api/v1/mcp/resource`, language: "bash" } ] } ]; --- <BaseLayout title="Examples | Kubernetes Claude MCP"> <div class="container mx-auto px-4 py-12"> <div class="max-w-5xl mx-auto"> <h1 class="text-4xl font-bold mb-6 text-primary-600">Examples</h1> <p class="text-xl text-slate-600 mb-10"> Explore practical examples of using the Kubernetes Claude MCP server for various use cases. These examples demonstrate how to leverage the API for resource analysis, troubleshooting, and GitOps workflows. </p> <div class="space-y-16"> {exampleCategories.map(category => ( <section class="example-category"> <h2 class="text-2xl font-bold mb-3 text-primary-600">{category.title}</h2> <p class="text-lg text-slate-600 mb-6">{category.description}</p> <div class="space-y-8"> {category.examples.map(example => ( <div class="example-card border border-secondary-300 rounded-lg overflow-hidden bg-secondary-50"> <div class="p-5 border-b border-secondary-300 bg-secondary-100"> <h3 class="text-xl font-semibold text-primary-600">{example.title}</h3> <p class="text-slate-600 mt-1">{example.description}</p> </div> <div class="p-5"> <CodeBlock code={example.code} lang={example.language} showLineNumbers={true} /> </div> </div> ))} </div> </section> ))} </div> <div class="mt-12 text-center"> <h2 class="text-2xl font-bold mb-4 text-primary-600">Need More Help?</h2> <p class="text-lg text-slate-600 mb-6"> Check out the detailed usage guides in the documentation or visit our GitHub repository. </p> <div class="flex flex-col sm:flex-row gap-4 justify-center"> <a href="/docs/api-overview" class="btn bg-primary-500 hover:bg-primary-600 text-white font-medium py-2 px-6 rounded-md"> API Reference </a> </div> </div> </div> </div> </BaseLayout> ``` -------------------------------------------------------------------------------- /docs/src/content/docs/api-overview.md: -------------------------------------------------------------------------------- ```markdown --- title: API Overview description: Comprehensive documentation of the Kubernetes Claude MCP REST API endpoints, parameters, and response formats. date: 2025-03-01 order: 5 tags: ['api', 'reference'] --- # API Overview Kubernetes Claude MCP provides a comprehensive REST API for interacting with Kubernetes resources, ArgoCD applications, GitLab repositories, and Claude's AI capabilities. This document provides an overview of all available endpoints, their parameters, and response formats. ## API Structure The API is organized into the following sections: - **General**: Health check and general information - **Kubernetes API**: Direct access to Kubernetes resources - **ArgoCD API**: Access to ArgoCD applications and sync status - **MCP API**: AI-powered analysis and troubleshooting All API calls require authentication using an API key, which is passed in the `X-API-Key` header or as a bearer token in the `Authorization` header. ```bash # Using X-API-Key header curl -H "X-API-Key: your_api_key" https://mcp.example.com/api/v1/health # Using Authorization header curl -H "Authorization: Bearer your_api_key" https://mcp.example.com/api/v1/health ``` ## Health Check ### GET /api/v1/health Check the health status of the server and its connected services. **Response:** ```json { "status": "ok", "services": { "kubernetes": "available", "argocd": "available", "gitlab": "available", "claude": "assumed available" } } ``` The `status` field will be `ok` if all required services are available, or `degraded` if some services are unavailable. ## Kubernetes API ### GET /api/v1/namespaces List all namespaces in the Kubernetes cluster. **Response:** ```json { "namespaces": [ "default", "kube-system", "monitoring", "argocd" ] } ``` ### GET /api/v1/resources/{kind}?namespace={ns} List all resources of a specific kind, optionally filtered by namespace. **Parameters:** - `kind`: The Kubernetes resource kind (e.g., `pod`, `deployment`, `service`) - `namespace`: (Optional) The namespace to filter by **Response:** ```json { "resources": [ { "apiVersion": "v1", "kind": "Pod", "metadata": { "name": "example-pod", "namespace": "default", "...": "..." }, "spec": { "...": "..." }, "status": { "...": "..." } }, // More resources... ] } ``` ### GET /api/v1/resources/{kind}/{name}?namespace={ns} Get a specific resource by kind, name, and namespace. **Parameters:** - `kind`: The Kubernetes resource kind - `name`: The resource name - `namespace`: (Optional) The namespace of the resource **Response:** ```json { "apiVersion": "v1", "kind": "Pod", "metadata": { "name": "example-pod", "namespace": "default", "...": "..." }, "spec": { "...": "..." }, "status": { "...": "..." } } ``` ### GET /api/v1/events?namespace={ns}&resource={kind}&name={name} Get events related to a specific resource. **Parameters:** - `namespace`: The namespace of the resource - `resource`: The resource kind - `name`: The resource name **Response:** ```json { "events": [ { "reason": "Created", "message": "Created container nginx", "type": "Normal", "count": 1, "firstTime": "2025-03-01T12:00:00Z", "lastTime": "2025-03-01T12:00:00Z", "object": { "kind": "Pod", "name": "example-pod", "namespace": "default" } }, // More events... ] } ``` ## ArgoCD API ### GET /api/v1/argocd/applications List all ArgoCD applications. **Response:** ```json { "applications": [ { "metadata": { "name": "example-app", "namespace": "argocd" }, "spec": { "source": { "repoURL": "https://github.com/example/repo.git", "path": "manifests", "targetRevision": "HEAD" }, "destination": { "server": "https://kubernetes.default.svc", "namespace": "default" } }, "status": { "sync": { "status": "Synced" }, "health": { "status": "Healthy" } } }, // More applications... ] } ``` ### GET /api/v1/argocd/applications/{name} Get a specific ArgoCD application by name. **Parameters:** - `name`: The ArgoCD application name **Response:** ```json { "metadata": { "name": "example-app", "namespace": "argocd" }, "spec": { "source": { "repoURL": "https://github.com/example/repo.git", "path": "manifests", "targetRevision": "HEAD" }, "destination": { "server": "https://kubernetes.default.svc", "namespace": "default" } }, "status": { "sync": { "status": "Synced" }, "health": { "status": "Healthy" } } } ``` ## MCP API The MCP API provides access to Claude's AI capabilities for analyzing Kubernetes resources and GitOps workflows. ### POST /api/v1/mcp Generic MCP request for Claude analysis. **Request:** ```json { "action": "string", "resource": "string", "name": "string", "namespace": "string", "query": "string", "commitSha": "string", "projectId": "string", "resourceSpecs": {}, "context": "string" } ``` **Response:** ```json { "success": true, "message": "Successfully processed request", "analysis": "Detailed analysis from Claude...", "actions": ["suggested actions..."], "context": {} } ``` ### POST /api/v1/mcp/resource Analyze a specific Kubernetes resource. **Request:** ```json { "resource": "pod", "name": "example-pod", "namespace": "default", "query": "Is this pod healthy? If not, what are the issues?" } ``` **Response:** ```json { "success": true, "message": "Successfully processed queryResource request", "analysis": "Detailed analysis of the pod's health status...", "context": { "kind": "Pod", "name": "example-pod", "namespace": "default", "argoApplication": {}, "gitlabProject": {}, "events": [] } } ``` ### POST /api/v1/mcp/commit Analyze the impact of a specific GitLab commit. **Request:** ```json { "projectId": "group/project", "commitSha": "abcdef123456", "query": "What changes were made in this commit and how do they affect the deployed resources?" } ``` **Response:** ```json { "success": true, "message": "Successfully processed queryCommit request", "analysis": "Detailed analysis of the commit and its impact...", "context": { "commit": {}, "affectedResources": [] } } ``` ### POST /api/v1/mcp/troubleshoot Troubleshoot a specific Kubernetes resource. **Request:** ```json { "resource": "deployment", "name": "example-deployment", "namespace": "default", "query": "Why is this deployment not scaling properly?" } ``` **Response:** ```json { "success": true, "message": "Successfully processed troubleshoot request", "analysis": "Detailed troubleshooting analysis...", "troubleshootResult": { "issues": [ { "title": "Resource Constraint", "category": "ResourceIssue", "severity": "Warning", "source": "Kubernetes", "description": "Deployment cannot scale due to insufficient CPU resources" } ], "recommendations": [ "Increase CPU request to allow for additional replicas", "Check node resources to ensure sufficient capacity" ] } } ``` ## Error Handling All API endpoints return standard HTTP status codes: - `200 OK`: Request was successful - `400 Bad Request`: Invalid request format or parameters - `401 Unauthorized`: Missing or invalid API key - `404 Not Found`: Resource not found - `500 Internal Server Error`: Server error Error responses include a JSON body with details: ```json { "error": "Failed to get resource", "details": "pod 'example-pod' not found in namespace 'default'" } ``` ## Pagination For endpoints that return collections, pagination is supported using the following query parameters: - `limit`: Maximum number of items to return (default: 100) - `page`: Page number to return (default: 1) Example: ``` GET /api/v1/resources/pods?namespace=default&limit=10&page=2 ``` Response includes pagination metadata: ```json { "resources": [...], "pagination": { "total": 25, "pages": 3, "currentPage": 2, "limit": 10 } } ``` ## API Versioning The API version is included in the URL path (`/api/v1/`). Future API versions will be made available at different paths (e.g., `/api/v2/`) to ensure backward compatibility. ## Rate Limiting The API implements rate limiting to prevent abuse. Rate limits vary by endpoint: - General endpoints: 100 requests per minute - Kubernetes endpoints: 60 requests per minute - ArgoCD endpoints: 60 requests per minute - MCP endpoints: 20 requests per minute Rate limit information is included in response headers: - `X-RateLimit-Limit`: Total requests allowed per minute - `X-RateLimit-Remaining`: Remaining requests in the current window - `X-RateLimit-Reset`: Seconds until the rate limit resets ``` -------------------------------------------------------------------------------- /docs/src/pages/index.astro: -------------------------------------------------------------------------------- ``` --- import BaseLayout from '../layouts/BaseLayout.astro'; --- <BaseLayout title="Kubernetes Claude MCP - AI-powered GitOps with Claude" description="The official documentation for Kubernetes Claude MCP Server, integrating Claude AI with Kubernetes, ArgoCD, and GitLab"> <!-- Hero Section --> <section class="py-20 px-4 bg-gradient-to-b from-secondary-200 to-secondary-100"> <div class="container mx-auto max-w-5xl"> <div class="flex flex-col items-center text-center"> <img src="/images/logo.svg" alt="Kubernetes Claude MCP Logo" class="w-48 h-48 mb-6"> <h1 class="text-5xl font-bold tracking-tight mb-6 text-primary-600"> Kubernetes Claude MCP </h1> <p class="text-xl text-slate-700 mb-8 max-w-2xl"> Model Context Protocol server for Kubernetes, integrating Claude AI with ArgoCD and GitLab for advanced GitOps workflows. </p> <div class="flex flex-col sm:flex-row gap-4"> <a href="/docs/introduction" class="btn bg-primary-500 hover:bg-primary-600 text-white font-medium py-2 px-6 rounded-md"> Get Started </a> <a href="https://github.com/blankcut/kubernetes-mcp-server" class="btn bg-secondary-200 hover:bg-secondary-300 text-primary-700 font-medium py-2 px-6 rounded-md" target="_blank" rel="noopener"> GitHub </a> </div> </div> </div> </section> <!-- Features Section --> <section class="py-16 px-4 bg-secondary-100"> <div class="container mx-auto max-w-6xl"> <h2 class="text-3xl font-bold text-center mb-12 text-primary-600">Key Features</h2> <div class="grid md:grid-cols-3 gap-8"> <!-- Feature 1 --> <div class="feature-card p-6 rounded-lg border border-secondary-200 shadow-sm bg-secondary-100"> <div class="mb-4 text-primary-500"> <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /> </svg> </div> <h3 class="text-xl font-semibold mb-2 text-primary-600">Kubernetes Integration</h3> <p class="text-slate-600"> Seamlessly connects to your Kubernetes cluster, providing automated analysis and troubleshooting of resources. </p> </div> <!-- Feature 2 --> <div class="feature-card p-6 rounded-lg border border-secondary-200 shadow-sm bg-secondary-100"> <div class="mb-4 text-primary-500"> <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /> </svg> </div> <h3 class="text-xl font-semibold mb-2 text-primary-600">Claude AI Powered</h3> <p class="text-slate-600"> Leverages Claude's AI capabilities to analyze configurations, explain issues, and recommend solutions for your cluster. </p> </div> <!-- Feature 3 --> <div class="feature-card p-6 rounded-lg border border-secondary-200 shadow-sm bg-secondary-100"> <div class="mb-4 text-primary-500"> <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" /> </svg> </div> <h3 class="text-xl font-semibold mb-2 text-primary-600">GitOps Integration</h3> <p class="text-slate-600"> Integrates with ArgoCD and GitLab to provide complete GitOps context for your deployments and configuration changes. </p> </div> </div> <div class="grid md:grid-cols-2 gap-8 mt-8"> <!-- Feature 4 --> <div class="feature-card p-6 rounded-lg border border-secondary-200 shadow-sm bg-secondary-100"> <div class="mb-4 text-primary-500"> <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /> </svg> </div> <h3 class="text-xl font-semibold mb-2 text-primary-600">Troubleshooting & Analysis</h3> <p class="text-slate-600"> Quickly identify issues in your cluster with comprehensive troubleshooting and analysis capabilities that trace through your entire deployment pipeline. </p> </div> <!-- Feature 5 --> <div class="feature-card p-6 rounded-lg border border-secondary-200 shadow-sm bg-secondary-100"> <div class="mb-4 text-primary-500"> <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" /> </svg> </div> <h3 class="text-xl font-semibold mb-2 text-primary-600">RESTful API</h3> <p class="text-slate-600"> Comprehensive API for integrating with your existing tools and workflows, with detailed documentation and examples. </p> </div> </div> </div> </section> <!-- How It Works Section --> <section class="py-16 px-4"> <div class="container mx-auto max-w-6xl"> <h2 class="text-3xl font-bold text-center mb-12 text-primary-600">How It Works</h2> <div class="grid md:grid-cols-3 gap-8 text-center"> <!-- Step 1 --> <div class="step"> <div class="bg-secondary-200 rounded-full h-16 w-16 flex items-center justify-center mx-auto mb-4 border border-secondary-300"> <span class="text-primary-600 text-xl font-bold">1</span> </div> <h3 class="text-xl font-semibold mb-2 text-primary-600">Connect</h3> <p class="text-slate-600"> Connect the server to your Kubernetes cluster, ArgoCD instance, and GitLab repositories. </p> </div> <!-- Step 2 --> <div class="step"> <div class="bg-secondary-200 rounded-full h-16 w-16 flex items-center justify-center mx-auto mb-4 border border-secondary-300"> <span class="text-primary-600 text-xl font-bold">2</span> </div> <h3 class="text-xl font-semibold mb-2 text-primary-600">Query</h3> <p class="text-slate-600"> Query resources, deployments, or commits through the API to get AI-powered analysis and context. </p> </div> <!-- Step 3 --> <div class="step"> <div class="bg-secondary-200 rounded-full h-16 w-16 flex items-center justify-center mx-auto mb-4 border border-secondary-300"> <span class="text-primary-600 text-xl font-bold">3</span> </div> <h3 class="text-xl font-semibold mb-2 text-primary-600">Analyze</h3> <p class="text-slate-600"> Receive detailed analysis, troubleshooting recommendations, and insights from Claude AI. </p> </div> </div> </div> </section> <!-- CTA Section --> <section class="py-16 px-4 bg-secondary-100"> <div class="container mx-auto max-w-5xl"> <div class="bg-primary-500 text-white rounded-lg p-8 md:p-12 shadow-lg"> <div class="text-center"> <h2 class="text-3xl font-bold mb-4">Ready to transform your Kubernetes experience?</h2> <p class="text-lg opacity-90 mb-8 max-w-2xl mx-auto"> Get started with Kubernetes Claude MCP today and enhance your GitOps workflows with AI-powered analysis and troubleshooting. </p> <div class="flex flex-col sm:flex-row gap-4 justify-center"> <a href="/docs/quick-start" class="btn bg-white text-primary-700 hover:bg-secondary-100 font-medium py-2 px-6 rounded-md"> Quick Start Guide </a> <a href="/docs/installation" class="btn bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-6 rounded-md border border-primary-400"> Installation Guide </a> </div> </div> </div> </div> </section> </BaseLayout> ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/argocd/client.go: -------------------------------------------------------------------------------- ```go package argocd import ( "bytes" "context" "crypto/tls" "encoding/json" "fmt" "io" "net/http" "net/url" "path" "strings" "time" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/auth" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/config" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" ) // Client handles communication with the ArgoCD API type Client struct { baseURL string httpClient *http.Client credentialProvider *auth.CredentialProvider config *config.ArgoCDConfig logger *logging.Logger } // NewClient creates a new ArgoCD API client func NewClient(cfg *config.ArgoCDConfig, credProvider *auth.CredentialProvider, logger *logging.Logger) *Client { if logger == nil { logger = logging.NewLogger().Named("argocd") } // Create transport with optional insecure mode transport := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: cfg.Insecure, }, } return &Client{ baseURL: cfg.URL, httpClient: &http.Client{ Timeout: 30 * time.Second, Transport: transport, }, credentialProvider: credProvider, config: cfg, logger: logger, } } // CheckConnectivity tests the connection to the ArgoCD API func (c *Client) CheckConnectivity(ctx context.Context) error { c.logger.Debug("Checking ArgoCD connectivity") // Try to get ArgoCD version as a basic connectivity test endpoint := "/api/version" resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return fmt.Errorf("failed to connect to ArgoCD: %w", err) } defer resp.Body.Close() var version struct { Version string `json:"version"` } if err := json.NewDecoder(resp.Body).Decode(&version); err != nil { return fmt.Errorf("failed to decode ArgoCD version: %w", err) } c.logger.Debug("ArgoCD connectivity check successful", "version", version.Version) return nil } // doRequest performs an HTTP request to the ArgoCD API with authentication func (c *Client) doRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) { // Try the request with current credentials resp, err := c.attemptRequest(ctx, method, endpoint, body) // If we get a 401 unauthorized, try to refresh the token and retry once if err != nil && resp != nil && resp.StatusCode == http.StatusUnauthorized { c.logger.Debug("Received 401 from ArgoCD, attempting to refresh token") // Only try to refresh the token if we have username/password creds, err := c.credentialProvider.GetCredentials(auth.ServiceArgoCD) if err == nil && creds.Username != "" && creds.Password != "" { // Attempt to create a new session newToken, _, err := c.createSession(ctx, creds.Username, creds.Password) if err != nil { return nil, fmt.Errorf("failed to refresh ArgoCD token: %w", err) } // Update the credentials with the new token c.credentialProvider.UpdateArgoToken(ctx, newToken) // Retry the request with the new token return c.attemptRequest(ctx, method, endpoint, body) } } return resp, err } // attemptRequest makes a single request attempt func (c *Client) attemptRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) { // This contains the original doRequest logic u, err := url.Parse(c.baseURL) if err != nil { return nil, fmt.Errorf("invalid ArgoCD URL: %w", err) } u.Path = path.Join(u.Path, endpoint) req, err := http.NewRequestWithContext(ctx, method, u.String(), body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } if err := c.addAuth(req); err != nil { return nil, fmt.Errorf("failed to add authentication: %w", err) } req.Header.Set("Content-Type", "application/json") c.logger.Debug("Sending request to ArgoCD API", "method", method, "endpoint", endpoint) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } if resp.StatusCode >= 400 && resp.StatusCode != 401 { defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("ArgoCD API error (status %d): %s", resp.StatusCode, string(body)) } return resp, nil } // createSession creates a new ArgoCD session func (c *Client) createSession(ctx context.Context, username, password string) (string, time.Time, error) { // Create session request sessionReq := struct { Username string `json:"username"` Password string `json:"password"` }{ Username: username, Password: password, } // Convert to JSON sessionReqBody, err := json.Marshal(sessionReq) if err != nil { return "", time.Time{}, fmt.Errorf("failed to marshal session request: %w", err) } // Create a new HTTP client without authentication for this request u, err := url.Parse(c.baseURL) if err != nil { return "", time.Time{}, fmt.Errorf("invalid ArgoCD URL: %w", err) } u.Path = path.Join(u.Path, "/api/v1/session") req, err := http.NewRequestWithContext( ctx, http.MethodPost, u.String(), bytes.NewReader(sessionReqBody), ) if err != nil { return "", time.Time{}, fmt.Errorf("failed to create session request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return "", time.Time{}, fmt.Errorf("session request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return "", time.Time{}, fmt.Errorf("failed to create session (status %d): %s", resp.StatusCode, string(body)) } var sessionResp struct { Token string `json:"token"` } if err := json.NewDecoder(resp.Body).Decode(&sessionResp); err != nil { return "", time.Time{}, fmt.Errorf("failed to decode session response: %w", err) } // ArgoCD tokens will expire after 24 hours by default... expiry := time.Now().Add(24 * time.Hour) return sessionResp.Token, expiry, nil } // addAuth adds authentication to the request func (c *Client) addAuth(req *http.Request) error { creds, err := c.credentialProvider.GetCredentials(auth.ServiceArgoCD) if err != nil { return fmt.Errorf("failed to get ArgoCD credentials: %w", err) } if creds.Token != "" { // Set both header formats that ArgoCD might accept req.Header.Set("Authorization", "Bearer "+creds.Token) req.Header.Set("Cookie", "argocd.token="+creds.Token) return nil } if creds.Username != "" && creds.Password != "" { // We need to get a session token first token, _, err := c.createSession(req.Context(), creds.Username, creds.Password) if err != nil { return fmt.Errorf("failed to create ArgoCD session: %w", err) } // Update credentials with the new token c.credentialProvider.UpdateArgoToken(req.Context(), token) // Set both header formats req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Cookie", "argocd.token="+token) return nil } return fmt.Errorf("no valid ArgoCD credentials available") } // refreshToken gets a new token using username/password credentials func (c *Client) refreshToken(ctx context.Context) (string, time.Time, error) { creds, err := c.credentialProvider.GetCredentials(auth.ServiceArgoCD) if err != nil { return "", time.Time{}, fmt.Errorf("failed to get ArgoCD credentials: %w", err) } if creds.Username == "" || creds.Password == "" { return "", time.Time{}, fmt.Errorf("username/password required for token refresh") } // Create session request sessionReq := struct { Username string `json:"username"` Password string `json:"password"` }{ Username: creds.Username, Password: creds.Password, } // Convert to JSON sessionReqBody, err := json.Marshal(sessionReq) if err != nil { return "", time.Time{}, fmt.Errorf("failed to marshal session request: %w", err) } // Create a new HTTP client without authentication for this request req, err := http.NewRequestWithContext( ctx, http.MethodPost, fmt.Sprintf("%s/api/v1/session", c.baseURL), io.NopCloser(strings.NewReader(string(sessionReqBody))), ) if err != nil { return "", time.Time{}, fmt.Errorf("failed to create session request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return "", time.Time{}, fmt.Errorf("session request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return "", time.Time{}, fmt.Errorf("failed to create session (status %d): %s", resp.StatusCode, string(body)) } var sessionResp struct { Token string `json:"token"` } if err := json.NewDecoder(resp.Body).Decode(&sessionResp); err != nil { return "", time.Time{}, fmt.Errorf("failed to decode session response: %w", err) } // ArgoCD tokens typically expire after 24 hours expiry := time.Now().Add(24 * time.Hour) return sessionResp.Token, expiry, nil } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/mcp/protocol.go: -------------------------------------------------------------------------------- ```go package mcp import ( "context" "fmt" "strings" "time" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/claude" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/correlator" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/k8s" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/utils" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) // ProtocolHandler handles the Model Context Protocol for Kubernetes type ProtocolHandler struct { claudeClient *claude.Client claudeProtocol *claude.ProtocolHandler gitOpsCorrelator *correlator.GitOpsCorrelator k8sClient *k8s.Client contextManager *ContextManager promptGenerator *PromptGenerator logger *logging.Logger } // NewProtocolHandler creates a new MCP protocol handler func NewProtocolHandler( claudeClient *claude.Client, gitOpsCorrelator *correlator.GitOpsCorrelator, k8sClient *k8s.Client, logger *logging.Logger, ) *ProtocolHandler { if logger == nil { logger = logging.NewLogger().Named("mcp") } return &ProtocolHandler{ claudeClient: claudeClient, claudeProtocol: claude.NewProtocolHandler(claudeClient), gitOpsCorrelator: gitOpsCorrelator, k8sClient: k8sClient, contextManager: NewContextManager(100000, logger.Named("context")), promptGenerator: NewPromptGenerator(logger.Named("prompt")), logger: logger, } } // ProcessRequest processes an MCP request func (h *ProtocolHandler) ProcessRequest(ctx context.Context, request *models.MCPRequest) (*models.MCPResponse, error) { startTime := time.Now() h.logger.Info("Processing MCP request", "action", request.Action) var resourceContext string var err error // Handle different types of queries switch request.Action { case "queryResource": // If we have pre-populated context, use it if request.Context != "" { resourceContext = request.Context } else { // Trace deployment for a specific resource resourceInfo, err := h.gitOpsCorrelator.TraceResourceDeployment( ctx, request.Namespace, request.Resource, request.Name, ) if err != nil { return nil, fmt.Errorf("failed to trace resource deployment: %w", err) } // For non-namespace resources, enhance with the actual resource data if !strings.EqualFold(request.Resource, "namespace") { // Get the full resource details resource, err := h.k8sClient.GetResource(ctx, request.Resource, request.Namespace, request.Name) if err == nil && resource != nil { // Add the full resource details to the context resourceData, err := utils.ToJSON(resource.Object) if err == nil { resourceInfo.ResourceData = resourceData // Extract important deployment-specific information if available if strings.EqualFold(request.Resource, "deployment") { // Extract replicas info specReplicas, found, _ := unstructured.NestedInt64(resource.Object, "spec", "replicas") if found { if resourceInfo.Metadata == nil { resourceInfo.Metadata = make(map[string]interface{}) } resourceInfo.Metadata["desiredReplicas"] = specReplicas } // Extract status replica counts statusReplicas, found, _ := unstructured.NestedInt64(resource.Object, "status", "replicas") if found { if resourceInfo.Metadata == nil { resourceInfo.Metadata = make(map[string]interface{}) } resourceInfo.Metadata["currentReplicas"] = statusReplicas } // Extract readyReplicas readyReplicas, found, _ := unstructured.NestedInt64(resource.Object, "status", "readyReplicas") if found { if resourceInfo.Metadata == nil { resourceInfo.Metadata = make(map[string]interface{}) } resourceInfo.Metadata["readyReplicas"] = readyReplicas } // Extract availableReplicas availableReplicas, found, _ := unstructured.NestedInt64(resource.Object, "status", "availableReplicas") if found { if resourceInfo.Metadata == nil { resourceInfo.Metadata = make(map[string]interface{}) } resourceInfo.Metadata["availableReplicas"] = availableReplicas } // Extract container info containers, found, _ := unstructured.NestedSlice(resource.Object, "spec", "template", "spec", "containers") if found { var containerInfo []map[string]interface{} for _, c := range containers { container, ok := c.(map[string]interface{}) if !ok { continue } containerData := map[string]interface{}{ "name": container["name"], } if image, ok := container["image"].(string); ok { containerData["image"] = image } if resources, ok := container["resources"].(map[string]interface{}); ok { containerData["resources"] = resources } containerInfo = append(containerInfo, containerData) } if resourceInfo.Metadata == nil { resourceInfo.Metadata = make(map[string]interface{}) } resourceInfo.Metadata["containers"] = containerInfo } } } } } formattedContext, err := h.contextManager.FormatResourceContext(resourceInfo) if err != nil { return nil, fmt.Errorf("failed to format resource context: %w", err) } resourceContext = formattedContext } case "queryMergeRequest": // Analyze merge request resources, err := h.gitOpsCorrelator.AnalyzeMergeRequest( ctx, request.ProjectID, request.MergeRequestIID, ) if err != nil { return nil, fmt.Errorf("failed to analyze merge request: %w", err) } resourceContext, err = h.contextManager.CombineContexts(ctx, resources) if err != nil { return nil, fmt.Errorf("failed to combine resource contexts: %w", err) } default: return nil, fmt.Errorf("unsupported action: %s", request.Action) } // Generate prompts for Claude h.logger.Debug("Generating prompts for Claude") systemPrompt := h.promptGenerator.GenerateSystemPrompt() userPrompt := h.promptGenerator.GenerateUserPrompt(resourceContext, request.Query) // Get completion from Claude h.logger.Debug("Sending request to Claude", "systemPromptLength", len(systemPrompt), "userPromptLength", len(userPrompt)) analysis, err := h.claudeProtocol.GetCompletion(ctx, systemPrompt, userPrompt) if err != nil { return nil, fmt.Errorf("failed to get completion from Claude: %w", err) } // Build response response := &models.MCPResponse{ Success: true, Analysis: analysis, Message: fmt.Sprintf("Successfully processed %s request in %v", request.Action, time.Since(startTime)), } h.logger.Info("MCP request processed successfully", "action", request.Action, "duration", time.Since(startTime), "responseLength", len(analysis)) return response, nil } // ProcessTroubleshootRequest processes a troubleshooting request with detected issues func (h *ProtocolHandler) ProcessTroubleshootRequest(ctx context.Context, request *models.MCPRequest, troubleshootResult *models.TroubleshootResult) (*models.MCPResponse, error) { startTime := time.Now() h.logger.Debug("Processing troubleshoot request") // Extract issues and recommendations var issuesText string for i, issue := range troubleshootResult.Issues { issuesText += fmt.Sprintf("%d. %s (%s): %s\n", i+1, issue.Title, issue.Severity, issue.Description) } var recommendationsText string for i, rec := range troubleshootResult.Recommendations { recommendationsText += fmt.Sprintf("%d. %s\n", i+1, rec) } // Create a prompt for Claude with the troubleshooting results userPrompt := fmt.Sprintf( "I'm troubleshooting a Kubernetes %s named '%s' in namespace '%s'.\n\n"+ "The following issues were detected:\n%s\n"+ "General recommendations:\n%s\n\n"+ "Based on these detected issues, please provide specific kubectl commands "+ "that I can use to troubleshoot and fix the problems. %s", request.Resource, request.Name, request.Namespace, issuesText, recommendationsText, request.Query) // Generate system prompt systemPrompt := h.promptGenerator.GenerateSystemPrompt() // Get Claude's analysis h.logger.Debug("Sending troubleshoot request to Claude", "systemPromptLength", len(systemPrompt), "userPromptLength", len(userPrompt)) analysis, err := h.claudeProtocol.GetCompletion(ctx, systemPrompt, userPrompt) if err != nil { return nil, fmt.Errorf("failed to get completion for troubleshoot request: %w", err) } // Create response response := &models.MCPResponse{ Success: true, Analysis: analysis, Message: fmt.Sprintf("Successfully processed troubleshoot request in %v", time.Since(startTime)), } h.logger.Info("Troubleshoot request processed successfully", "duration", time.Since(startTime), "responseLength", len(analysis)) return response, nil } // WithCustomPrompt sets a custom base prompt template func (h *ProtocolHandler) WithCustomPrompt(template string) *ProtocolHandler { h.promptGenerator.WithBasePrompt(template) return h } // WithMaxContextSize sets the maximum context size func (h *ProtocolHandler) WithMaxContextSize(size int) *ProtocolHandler { h.contextManager = NewContextManager(size, h.logger.Named("context")) return h } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/mcp/namespace_analyzer.go: -------------------------------------------------------------------------------- ```go package mcp import ( "context" "fmt" "strings" "time" "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" k8s "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/k8s" ) // NamespaceAnalysisResult contains the analysis of a namespace's resources type NamespaceAnalysisResult struct { Namespace string `json:"namespace"` ResourceCounts map[string]int `json:"resourceCounts"` HealthStatus map[string]map[string]int `json:"healthStatus"` ResourceRelationships []k8s.ResourceRelationship `json:"resourceRelationships"` Issues []models.Issue `json:"issues"` Recommendations []string `json:"recommendations"` Analysis string `json:"analysis"` } // AnalyzeNamespace analyzes all resources in a namespace using Claude func (h *ProtocolHandler) AnalyzeNamespace(ctx context.Context, namespace string) (*models.NamespaceAnalysisResult, error) { startTime := time.Now() h.logger.Info("Analyzing namespace", "namespace", namespace) // Get namespace topology topology, err := h.k8sClient.GetNamespaceTopology(ctx, namespace) if err != nil { return nil, fmt.Errorf("failed to get namespace topology: %w", err) } // Initialize result result := &models.NamespaceAnalysisResult{ Namespace: namespace, ResourceCounts: make(map[string]int), HealthStatus: make(map[string]map[string]int), Issues: []models.Issue{}, Recommendations: []string{}, } // Extract resource counts for kind, resources := range topology.Resources { result.ResourceCounts[kind] = len(resources) } // Extract health status for kind, statusMap := range topology.Health { healthCounts := make(map[string]int) for _, status := range statusMap { healthCounts[status]++ } result.HealthStatus[kind] = healthCounts } // Add relationships - Convert from k8s.ResourceRelationship to models.ResourceRelationship for _, rel := range topology.Relationships { modelRel := models.ResourceRelationship{ SourceKind: rel.SourceKind, SourceName: rel.SourceName, SourceNamespace: rel.SourceNamespace, TargetKind: rel.TargetKind, TargetName: rel.TargetName, TargetNamespace: rel.TargetNamespace, RelationType: rel.RelationType, } result.ResourceRelationships = append(result.ResourceRelationships, modelRel) } // Get events for the namespace events, err := h.k8sClient.GetNamespaceEvents(ctx, namespace) if err != nil { h.logger.Warn("Failed to get namespace events", "error", err) } // Identify issues from events for _, event := range events { if event.Type == "Warning" { issue := models.Issue{ Source: "Kubernetes", Severity: "Warning", Description: fmt.Sprintf("%s: %s", event.Reason, event.Message), } // Categorize common issues switch { case strings.Contains(event.Reason, "Failed") && strings.Contains(event.Message, "ImagePull"): issue.Category = "ImagePullError" issue.Title = "Image Pull Failure" case strings.Contains(event.Reason, "Unhealthy"): issue.Category = "HealthCheckFailure" issue.Title = "Health Check Failure" case strings.Contains(event.Message, "memory"): issue.Category = "ResourceIssue" issue.Title = "Memory Resource Issue" case strings.Contains(event.Message, "cpu"): issue.Category = "ResourceIssue" issue.Title = "CPU Resource Issue" case strings.Contains(event.Reason, "BackOff"): issue.Category = "CrashLoopBackOff" issue.Title = "Container Crash Loop" default: issue.Category = "OtherWarning" issue.Title = "Kubernetes Warning" } result.Issues = append(result.Issues, issue) } } // Generate Claude analysis analysisPrompt := h.generateNamespaceAnalysisPrompt(namespace, topology, events) systemPrompt := h.promptGenerator.GenerateSystemPrompt() h.logger.Debug("Sending namespace analysis request to Claude", "namespace", namespace, "systemPromptLength", len(systemPrompt), "analysisPromptLength", len(analysisPrompt)) analysis, err := h.claudeProtocol.GetCompletion(ctx, systemPrompt, analysisPrompt) if err != nil { return nil, fmt.Errorf("failed to get completion for namespace analysis: %w", err) } // Extract recommendations from analysis lines := strings.Split(analysis, "\n") inRecommendations := false for _, line := range lines { if strings.Contains(strings.ToLower(line), "recommendation") || strings.Contains(strings.ToLower(line), "recommendations") || strings.Contains(strings.ToLower(line), "suggest") { inRecommendations = true continue } if inRecommendations && strings.TrimSpace(line) != "" && !strings.HasPrefix(line, "#") { // Remove leading dash or number if it exists cleanLine := strings.TrimSpace(line) if strings.HasPrefix(cleanLine, "- ") { cleanLine = cleanLine[2:] } else if len(cleanLine) > 2 && strings.HasPrefix(cleanLine, "* ") { cleanLine = cleanLine[2:] } else if len(cleanLine) > 3 && ((cleanLine[0] >= '1' && cleanLine[0] <= '9') && (cleanLine[1] == '.' || cleanLine[1] == ')') && (cleanLine[2] == ' ')) { cleanLine = cleanLine[3:] } if cleanLine != "" && len(result.Recommendations) < 10 { result.Recommendations = append(result.Recommendations, cleanLine) } } } result.Analysis = analysis h.logger.Info("Namespace analysis completed", "namespace", namespace, "duration", time.Since(startTime), "issueCount", len(result.Issues), "recommendationCount", len(result.Recommendations)) return result, nil } // generateNamespaceAnalysisPrompt creates a prompt for namespace analysis func (h *ProtocolHandler) generateNamespaceAnalysisPrompt(namespace string, topology *k8s.NamespaceTopology, events []models.K8sEvent) string { // Start with namespace overview prompt := fmt.Sprintf("# Namespace Analysis: %s\n\n", namespace) // Add resource summary prompt += "## Resource Summary\n\n" for kind, resources := range topology.Resources { prompt += fmt.Sprintf("- %s: %d resources\n", kind, len(resources)) } prompt += "\n" // Add health status summary prompt += "## Health Status\n\n" for kind, statusMap := range topology.Health { prompt += fmt.Sprintf("### %s Health\n", kind) // Count the statuses healthCounts := make(map[string]int) for _, status := range statusMap { healthCounts[status]++ } // List the counts for status, count := range healthCounts { prompt += fmt.Sprintf("- %s: %d resources\n", status, count) } // List unhealthy resources unhealthyResources := []string{} for name, status := range statusMap { if status == "unhealthy" { unhealthyResources = append(unhealthyResources, name) } } if len(unhealthyResources) > 0 { prompt += "\nUnhealthy resources:\n" for _, name := range unhealthyResources { prompt += fmt.Sprintf("- %s\n", name) } } prompt += "\n" } // Add relationship summary if len(topology.Relationships) > 0 { prompt += "## Resource Relationships\n\n" // Group by relationship type relationshipsByType := make(map[string][]string) for _, rel := range topology.Relationships { key := rel.RelationType relationshipsByType[key] = append( relationshipsByType[key], fmt.Sprintf("%s/%s -> %s/%s", rel.SourceKind, rel.SourceName, rel.TargetKind, rel.TargetName)) } // List relationships by type for relType, relations := range relationshipsByType { prompt += fmt.Sprintf("### %s Relationships\n", strings.Title(relType)) for _, rel := range relations { prompt += fmt.Sprintf("- %s\n", rel) } prompt += "\n" } } // Add recent events if len(events) > 0 { prompt += "## Recent Events\n\n" // Group events by type warningEvents := []models.K8sEvent{} normalEvents := []models.K8sEvent{} for _, event := range events { if event.Type == "Warning" { warningEvents = append(warningEvents, event) } else { normalEvents = append(normalEvents, event) } } // Add warning events first (limited to 10) if len(warningEvents) > 0 { prompt += "### Warning Events\n" count := 0 for _, event := range warningEvents { if count >= 10 { break } prompt += fmt.Sprintf("- [%s] %s: %s (%s)\n", event.LastTime.Format(time.RFC3339), event.Reason, event.Message, fmt.Sprintf("%s/%s", event.Object.Kind, event.Object.Name)) count++ } prompt += "\n" } // Add a few normal events (limited to 5) if len(normalEvents) > 0 { prompt += "### Normal Events\n" count := 0 for _, event := range normalEvents { if count >= 5 { break } prompt += fmt.Sprintf("- [%s] %s: %s (%s)\n", event.LastTime.Format(time.RFC3339), event.Reason, event.Message, fmt.Sprintf("%s/%s", event.Object.Kind, event.Object.Name)) count++ } prompt += "\n" } } // Add analysis request prompt += "## Analysis Request\n\n" prompt += "Based on the information above, please provide a comprehensive analysis of this Kubernetes namespace, including:\n\n" prompt += "1. Overall health assessment\n" prompt += "2. Identification of any issues or problems\n" prompt += "3. Analysis of resource relationships and dependencies\n" prompt += "4. Potential bottlenecks or misconfigurations\n" prompt += "5. Security concerns (if any can be identified)\n" prompt += "6. Specific recommendations for improvement\n\n" prompt += "Please format your analysis with clear sections and provide specific, actionable recommendations that would help improve the reliability, efficiency, and security of this namespace." return prompt } ```