This is page 2 of 5. Use http://codebase.md/blankcut/kubernetes-mcp-server?lines=true&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 -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/gitlab/client.go: -------------------------------------------------------------------------------- ```go 1 | package gitlab 2 | 3 | import ( 4 | "strings" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "path" 12 | "time" 13 | 14 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/auth" 15 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/config" 16 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" 17 | ) 18 | 19 | // Client handles communication with the GitLab API 20 | type Client struct { 21 | baseURL string 22 | httpClient *http.Client 23 | credentialProvider *auth.CredentialProvider 24 | config *config.GitLabConfig 25 | logger *logging.Logger 26 | } 27 | 28 | // NewClient creates a new GitLab API client 29 | func NewClient(cfg *config.GitLabConfig, credProvider *auth.CredentialProvider, logger *logging.Logger) *Client { 30 | if logger == nil { 31 | logger = logging.NewLogger().Named("gitlab") 32 | } 33 | 34 | return &Client{ 35 | baseURL: cfg.URL, 36 | httpClient: &http.Client{ 37 | Timeout: 30 * time.Second, 38 | }, 39 | credentialProvider: credProvider, 40 | config: cfg, 41 | logger: logger, 42 | } 43 | } 44 | 45 | // CheckConnectivity tests the connection to the GitLab API 46 | func (c *Client) CheckConnectivity(ctx context.Context) error { 47 | c.logger.Debug("Checking GitLab connectivity") 48 | 49 | // Try to get version information 50 | endpoint := "/api/v4/version" 51 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 52 | if err != nil { 53 | return fmt.Errorf("failed to connect to GitLab: %w", err) 54 | } 55 | defer resp.Body.Close() 56 | 57 | var version struct { 58 | Version string `json:"version"` 59 | } 60 | 61 | if err := json.NewDecoder(resp.Body).Decode(&version); err != nil { 62 | return fmt.Errorf("failed to decode GitLab version: %w", err) 63 | } 64 | 65 | c.logger.Debug("GitLab connectivity check successful", "version", version.Version) 66 | return nil 67 | } 68 | 69 | // doRequest performs an HTTP request to the GitLab API with authentication 70 | func (c *Client) doRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) { 71 | u, err := url.Parse(c.baseURL) 72 | if err != nil { 73 | return nil, fmt.Errorf("invalid GitLab URL: %w", err) 74 | } 75 | 76 | // Add API version if not already in the endpoint 77 | if !strings.HasPrefix(endpoint, "/api") { 78 | endpoint = path.Join("/api", c.config.APIVersion, endpoint) 79 | } 80 | 81 | u.Path = path.Join(u.Path, endpoint) 82 | 83 | req, err := http.NewRequestWithContext(ctx, method, u.String(), body) 84 | if err != nil { 85 | return nil, fmt.Errorf("failed to create request: %w", err) 86 | } 87 | 88 | // Add auth header 89 | if err := c.addAuth(req); err != nil { 90 | return nil, fmt.Errorf("failed to add authentication: %w", err) 91 | } 92 | 93 | req.Header.Set("Content-Type", "application/json") 94 | 95 | c.logger.Debug("Sending request to GitLab API", "method", method, "endpoint", endpoint) 96 | resp, err := c.httpClient.Do(req) 97 | if err != nil { 98 | return nil, fmt.Errorf("request failed: %w", err) 99 | } 100 | 101 | if resp.StatusCode >= 400 { 102 | defer resp.Body.Close() 103 | body, _ := io.ReadAll(resp.Body) 104 | return nil, fmt.Errorf("GitLab API error (status %d): %s", resp.StatusCode, string(body)) 105 | } 106 | 107 | return resp, nil 108 | } 109 | 110 | // addAuth adds authentication to the request 111 | func (c *Client) addAuth(req *http.Request) error { 112 | creds, err := c.credentialProvider.GetCredentials(auth.ServiceGitLab) 113 | if err != nil { 114 | return fmt.Errorf("failed to get GitLab credentials: %w", err) 115 | } 116 | 117 | if creds.Token != "" { 118 | req.Header.Set("PRIVATE-TOKEN", creds.Token) 119 | return nil 120 | } 121 | 122 | return fmt.Errorf("no valid GitLab credentials available") 123 | } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/pkg/config/config.go: -------------------------------------------------------------------------------- ```go 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | // Config holds all configuration for the MCP server 11 | type Config struct { 12 | Server ServerConfig `yaml:"server"` 13 | Kubernetes KubernetesConfig `yaml:"kubernetes"` 14 | ArgoCD ArgoCDConfig `yaml:"argocd"` 15 | GitLab GitLabConfig `yaml:"gitlab"` 16 | Claude ClaudeConfig `yaml:"claude"` 17 | } 18 | 19 | // ServerConfig holds the HTTP server configuration 20 | type ServerConfig struct { 21 | Address string `yaml:"address"` 22 | ReadTimeout int `yaml:"readTimeout"` 23 | WriteTimeout int `yaml:"writeTimeout"` 24 | Auth struct { 25 | APIKey string `yaml:"apiKey"` 26 | } `yaml:"auth"` 27 | } 28 | 29 | // KubernetesConfig holds configuration for Kubernetes client 30 | type KubernetesConfig struct { 31 | KubeConfig string `yaml:"kubeconfig"` 32 | InCluster bool `yaml:"inCluster"` 33 | DefaultContext string `yaml:"defaultContext"` 34 | DefaultNamespace string `yaml:"defaultNamespace"` 35 | } 36 | 37 | // ArgoCDConfig holds configuration for the ArgoCD client 38 | type ArgoCDConfig struct { 39 | URL string `yaml:"url"` 40 | AuthToken string `yaml:"authToken"` 41 | Username string `yaml:"username"` 42 | Password string `yaml:"password"` 43 | Insecure bool `yaml:"insecure"` 44 | } 45 | 46 | // GitLabConfig holds configuration for the GitLab client 47 | type GitLabConfig struct { 48 | URL string `yaml:"url"` 49 | AuthToken string `yaml:"authToken"` 50 | APIVersion string `yaml:"apiVersion"` 51 | } 52 | 53 | // ClaudeConfig holds configuration for the Claude API client 54 | type ClaudeConfig struct { 55 | APIKey string `yaml:"apiKey"` 56 | BaseURL string `yaml:"baseURL"` 57 | ModelID string `yaml:"modelID"` 58 | MaxTokens int `yaml:"maxTokens"` 59 | Temperature float64 `yaml:"temperature"` 60 | } 61 | 62 | // Load reads configuration from a file and environment variables 63 | func Load(path string) (*Config, error) { 64 | config := &Config{} 65 | 66 | // Read config file 67 | data, err := os.ReadFile(path) 68 | if err != nil { 69 | return nil, fmt.Errorf("error reading config file: %w", err) 70 | } 71 | 72 | // Parse YAML 73 | if err := yaml.Unmarshal(data, config); err != nil { 74 | return nil, fmt.Errorf("error parsing config file: %w", err) 75 | } 76 | 77 | // Override with environment variables if present 78 | if kubeconfig := os.Getenv("KUBECONFIG"); kubeconfig != "" { 79 | config.Kubernetes.KubeConfig = kubeconfig 80 | } 81 | 82 | // Claude API settings 83 | if apiKey := os.Getenv("CLAUDE_API_KEY"); apiKey != "" { 84 | config.Claude.APIKey = apiKey 85 | } 86 | 87 | // ArgoCD settings 88 | if argoURL := os.Getenv("ARGOCD_SERVER"); argoURL != "" { 89 | config.ArgoCD.URL = argoURL 90 | } 91 | if argoToken := os.Getenv("ARGOCD_AUTH_TOKEN"); argoToken != "" { 92 | config.ArgoCD.AuthToken = argoToken 93 | } 94 | if argoUser := os.Getenv("ARGOCD_USERNAME"); argoUser != "" { 95 | config.ArgoCD.Username = argoUser 96 | } 97 | if argoPass := os.Getenv("ARGOCD_PASSWORD"); argoPass != "" { 98 | config.ArgoCD.Password = argoPass 99 | } 100 | 101 | // GitLab settings 102 | if gitlabURL := os.Getenv("GITLAB_URL"); gitlabURL != "" { 103 | config.GitLab.URL = gitlabURL 104 | } 105 | if gitlabToken := os.Getenv("GITLAB_AUTH_TOKEN"); gitlabToken != "" { 106 | config.GitLab.AuthToken = gitlabToken 107 | } 108 | 109 | return config, nil 110 | } 111 | 112 | // Validate checks if the configuration is valid 113 | func (c *Config) Validate() error { 114 | // Check server configuration 115 | if c.Server.Address == "" { 116 | return fmt.Errorf("server address is required") 117 | } 118 | 119 | // Check Claude configuration 120 | if c.Claude.APIKey == "" { 121 | return fmt.Errorf("Claude API key is required") 122 | } 123 | 124 | if c.Claude.ModelID == "" { 125 | return fmt.Errorf("Claude model ID is required") 126 | } 127 | 128 | return nil 129 | } ``` -------------------------------------------------------------------------------- /docs/src/components/Sidebar.astro: -------------------------------------------------------------------------------- ``` 1 | --- 2 | const currentPath = Astro.url.pathname; 3 | 4 | const sidebarData = [ 5 | { 6 | section: 'Getting Started', 7 | items: [ 8 | { text: 'Introduction', link: '/docs/introduction' }, 9 | { text: 'Quick Start', link: '/docs/quick-start' }, 10 | { text: 'Installation', link: '/docs/installation' }, 11 | { text: 'Configuration', link: '/docs/configuration' }, 12 | ] 13 | }, 14 | { 15 | section: 'Core Concepts', 16 | items: [ 17 | { text: 'Model Context Protocol', link: '/docs/model-context-protocol' }, 18 | { text: 'GitOps Integration', link: '/docs/gitops-integration' }, 19 | { text: 'Claude Integration', link: '/docs/claude-integration' }, 20 | ] 21 | }, 22 | { 23 | section: 'API Reference', 24 | items: [ 25 | { text: 'Overview', link: '/docs/api-overview' }, 26 | { text: 'Authentication', link: '/docs/api-authentication' }, 27 | { text: 'Kubernetes API', link: '/docs/kubernetes-api' }, 28 | { text: 'ArgoCD API', link: '/docs/argocd-api' }, 29 | { text: 'GitLab API', link: '/docs/gitlab-api' }, 30 | { text: 'MCP API', link: '/docs/mcp-api' }, 31 | ] 32 | }, 33 | { 34 | section: 'Guides', 35 | items: [ 36 | { text: 'Troubleshooting Resources', link: '/docs/troubleshooting-resources' }, 37 | { text: 'Analyzing Deployments', link: '/docs/analyzing-deployments' }, 38 | { text: 'Commit Analysis', link: '/docs/commit-analysis' }, 39 | { text: 'Using with ArgoCD', link: '/docs/using-with-argocd' }, 40 | { text: 'Using with GitLab', link: '/docs/using-with-gitlab' }, 41 | ] 42 | }, 43 | { 44 | section: 'Examples', 45 | items: [ 46 | { text: 'Basic Usage', link: '/docs/examples/basic-usage' }, 47 | { text: 'Troubleshooting', link: '/docs/examples/troubleshooting' }, 48 | { text: 'CI/CD Integration', link: '/docs/examples/cicd-integration' }, 49 | { text: 'Custom Prompts', link: '/docs/examples/custom-prompts' }, 50 | ] 51 | }, 52 | { 53 | section: 'Advanced', 54 | items: [ 55 | { text: 'Server Architecture', link: '/docs/server-architecture' }, 56 | { text: 'Custom Integrations', link: '/docs/custom-integrations' }, 57 | { text: 'Security Best Practices', link: '/docs/security-best-practices' }, 58 | { text: 'Production Deployment', link: '/docs/production-deployment' }, 59 | ] 60 | }, 61 | ]; 62 | 63 | --- 64 | 65 | <aside class="sidebar"> 66 | <div class="mb-6"> 67 | <input 68 | type="text" 69 | placeholder="Search docs..." 70 | class="w-full px-3 py-2 border rounded-md border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" 71 | /> 72 | </div> 73 | 74 | <nav> 75 | {sidebarData.map(section => ( 76 | <div class="mb-6"> 77 | <h2 class="font-bold text-sm uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2"> 78 | {section.section} 79 | </h2> 80 | <ul class="space-y-2"> 81 | {section.items.map(item => { 82 | const isActive = currentPath === item.link || 83 | (currentPath.startsWith(item.link) && item.link !== '/docs/introduction'); 84 | 85 | return ( 86 | <li> 87 | <a href={item.link} 88 | class={`block py-1 px-2 rounded-md ${ 89 | isActive 90 | ? 'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400 font-medium' 91 | : 'text-slate-700 hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400' 92 | }`} 93 | > 94 | {item.text} 95 | </a> 96 | </li> 97 | ); 98 | })} 99 | </ul> 100 | </div> 101 | ))} 102 | </nav> 103 | </aside> ``` -------------------------------------------------------------------------------- /docs/src/layouts/BaseLayout.astro: -------------------------------------------------------------------------------- ``` 1 | --- 2 | import '../styles/global.css'; 3 | import DocSidebar from '../components/DocSidebar.astro'; 4 | 5 | export interface Props { 6 | title: string; 7 | description?: string; 8 | image?: string; 9 | canonical?: string; 10 | showSidebar?: boolean; 11 | } 12 | 13 | const { 14 | title, 15 | description, 16 | image, 17 | canonical, 18 | showSidebar = true 19 | } = Astro.props; 20 | 21 | const isDocPage = Astro.url.pathname.includes('/docs/') || 22 | Astro.url.pathname === '/docs'; 23 | --- 24 | 25 | <!DOCTYPE html> 26 | <html lang="en"> 27 | <head> 28 | <meta charset="UTF-8" /> 29 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 30 | <title>{title}</title> 31 | {description && <meta name="description" content={description} />} 32 | <link rel="icon" type="image/svg+xml" href="/images/logo.svg" /> 33 | <slot name="head" /> 34 | </head> 35 | <body class="bg-[#F8EDE3]"> 36 | <header class="sticky top-0 z-40 w-full bg-[#F8EDE3] border-b border-secondary-300"> 37 | <div class="container mx-auto px-4 py-3 flex justify-between items-center"> 38 | <a href="/" class="flex items-center space-x-2"> 39 | <img src="/images/logo.svg" alt="Kubernetes Claude MCP" class="h-10 w-10" /> 40 | <span class="font-bold text-xl text-primary-600">Kubernetes Claude MCP</span> 41 | </a> 42 | <nav class="hidden md:flex space-x-6"> 43 | <a href="/docs/introduction" class="text-slate-700 hover:text-primary-600">Documentation</a> 44 | <a href="/examples" class="text-slate-700 hover:text-primary-600">Examples</a> 45 | <a href="https://github.com/blankcut/kubernetes-mcp-server" target="_blank" rel="noopener noreferrer" class="text-slate-700 hover:text-primary-600"> 46 | <span class="sr-only">GitHub</span> 47 | <svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> 48 | <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> 49 | </svg> 50 | </a> 51 | </nav> 52 | <button id="mobile-menu-toggle" class="md:hidden p-2"> 53 | <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"> 54 | <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path> 55 | </svg> 56 | </button> 57 | </div> 58 | </header> 59 | 60 | {isDocPage ? ( 61 | <div class="flex min-h-screen"> 62 | <DocSidebar /> 63 | <main class="flex-1 bg-[#F8EDE3]"> 64 | <slot /> 65 | </main> 66 | </div> 67 | ) : ( 68 | <main class="bg-[#F8EDE3]"> 69 | <slot /> 70 | </main> 71 | )} 72 | 73 | <footer class="bg-secondary-200 border-t border-secondary-300 py-12"> 74 | <div class="container mx-auto px-4 text-center"> 75 | <p class="text-slate-600 text-sm"> 76 | © 2025 Blank Cut Inc. All rights reserved. 77 | </p> 78 | </div> 79 | </footer> 80 | 81 | <script> 82 | // Mobile menu toggle 83 | const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); 84 | mobileMenuToggle?.addEventListener('click', () => { 85 | const mobileMenu = document.getElementById('mobile-menu'); 86 | mobileMenu?.classList.toggle('hidden'); 87 | }); 88 | </script> 89 | </body> 90 | </html> 91 | ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/argocd/history.go: -------------------------------------------------------------------------------- ```go 1 | package argocd 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | "bytes" 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | 13 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" 14 | ) 15 | 16 | // GetApplicationHistory returns the deployment history for an application 17 | func (c *Client) GetApplicationHistory(ctx context.Context, name string) ([]models.ArgoApplicationHistory, error) { 18 | c.logger.Debug("Getting application history", "name", name) 19 | 20 | endpoint := fmt.Sprintf("/api/v1/applications/%s/history", url.PathEscape(name)) 21 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 22 | if err != nil { 23 | return nil, err 24 | } 25 | defer resp.Body.Close() 26 | 27 | var result struct { 28 | History []models.ArgoApplicationHistory `json:"history"` 29 | } 30 | 31 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 32 | return nil, fmt.Errorf("failed to decode response: %w", err) 33 | } 34 | 35 | c.logger.Debug("Retrieved application history", "name", name, "entryCount", len(result.History)) 36 | return result.History, nil 37 | } 38 | 39 | // GetApplicationLogs retrieves logs for a specific application component 40 | func (c *Client) GetApplicationLogs(ctx context.Context, name, podName, containerName string) ([]string, error) { 41 | c.logger.Debug("Getting application logs", 42 | "application", name, 43 | "pod", podName, 44 | "container", containerName) 45 | 46 | endpoint := fmt.Sprintf("/api/v1/applications/%s/pods/%s/logs?container=%s", 47 | url.PathEscape(name), 48 | url.PathEscape(podName), 49 | url.QueryEscape(containerName)) 50 | 51 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 52 | if err != nil { 53 | return nil, err 54 | } 55 | defer resp.Body.Close() 56 | 57 | body, err := io.ReadAll(resp.Body) 58 | if err != nil { 59 | return nil, fmt.Errorf("failed to read response body: %w", err) 60 | } 61 | 62 | // ArgoCD returns logs as a newline-separated string here... 63 | logEntries := strings.Split(string(body), "\n") 64 | var logs []string 65 | for _, entry := range logEntries { 66 | if entry != "" { 67 | logs = append(logs, entry) 68 | } 69 | } 70 | 71 | c.logger.Debug("Retrieved application logs", 72 | "application", name, 73 | "pod", podName, 74 | "container", containerName, 75 | "lineCount", len(logs)) 76 | return logs, nil 77 | } 78 | 79 | // GetApplicationRevisionMetadata gets metadata about a specific revision 80 | func (c *Client) GetApplicationRevisionMetadata(ctx context.Context, name, revision string) (*models.GitLabCommit, error) { 81 | c.logger.Debug("Getting revision metadata", "application", name, "revision", revision) 82 | 83 | endpoint := fmt.Sprintf("/api/v1/applications/%s/revisions/%s/metadata", 84 | url.PathEscape(name), 85 | url.PathEscape(revision)) 86 | 87 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 88 | if err != nil { 89 | return nil, err 90 | } 91 | defer resp.Body.Close() 92 | 93 | var result models.GitLabCommit 94 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 95 | return nil, fmt.Errorf("failed to decode response: %w", err) 96 | } 97 | 98 | return &result, nil 99 | } 100 | 101 | // SyncApplication triggers a sync operation for an application 102 | func (c *Client) SyncApplication(ctx context.Context, name string, revision string, prune bool, resources []string) error { 103 | c.logger.Debug("Syncing application", 104 | "name", name, 105 | "revision", revision, 106 | "prune", prune) 107 | 108 | // Prepare sync request body 109 | syncRequest := struct { 110 | Revision string `json:"revision,omitempty"` 111 | Prune bool `json:"prune"` 112 | Resources []string `json:"resources,omitempty"` 113 | DryRun bool `json:"dryRun"` 114 | }{ 115 | Revision: revision, 116 | Prune: prune, 117 | Resources: resources, 118 | DryRun: false, 119 | } 120 | 121 | jsonBody, err := json.Marshal(syncRequest) 122 | if err != nil { 123 | return fmt.Errorf("failed to marshal sync request: %w", err) 124 | } 125 | 126 | endpoint := fmt.Sprintf("/api/v1/applications/%s/sync", url.PathEscape(name)) 127 | resp, err := c.doRequest(ctx, http.MethodPost, endpoint, bytes.NewReader(jsonBody)) 128 | if err != nil { 129 | return err 130 | } 131 | defer resp.Body.Close() 132 | 133 | // Read response but we won't need to process it 134 | _, err = io.ReadAll(resp.Body) 135 | if err != nil { 136 | return fmt.Errorf("failed to read response body: %w", err) 137 | } 138 | 139 | c.logger.Info("Application sync initiated", "name", name) 140 | return nil 141 | } ``` -------------------------------------------------------------------------------- /docs/src/components/Header.astro: -------------------------------------------------------------------------------- ``` 1 | --- 2 | import Search from './Search.astro'; 3 | --- 4 | 5 | <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"> 6 | <div class="container mx-auto px-4 py-3 flex justify-between items-center"> 7 | <a href="/" class="flex items-center space-x-2"> 8 | <img src="/images/logo.svg" alt="Kubernetes Claude MCP" class="h-8 w-8" /> 9 | <span class="font-bold text-xl">Kubernetes Claude MCP</span> 10 | </a> 11 | <div class="hidden md:flex items-center space-x-6"> 12 | <Search /> 13 | <nav class="flex space-x-6"> 14 | <a href="/docs/introduction" class="text-slate-700 hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400">Documentation</a> 15 | <a href="/examples" class="text-slate-700 hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400">Examples</a> 16 | </nav> 17 | <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"> 18 | <span class="sr-only">GitHub</span> 19 | <svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> 20 | <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> 21 | </svg> 22 | </a> 23 | </div> 24 | <button id="mobile-menu-toggle" class="md:hidden p-2"> 25 | <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"> 26 | <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path> 27 | </svg> 28 | </button> 29 | </div> 30 | <div id="mobile-menu" class="md:hidden hidden"> 31 | <div class="px-4 py-3 space-y-4"> 32 | <Search /> 33 | <nav class="flex flex-col space-y-3"> 34 | <a href="/docs/introduction" class="text-slate-700 hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400">Documentation</a> 35 | <a href="/examples" class="text-slate-700 hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400">Examples</a> 36 | <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"> 37 | <svg class="h-5 w-5 mr-2" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> 38 | <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> 39 | </svg> 40 | GitHub 41 | </a> 42 | </nav> 43 | </div> 44 | </div> 45 | </header> 46 | 47 | <script> 48 | const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); 49 | const mobileMenu = document.getElementById('mobile-menu'); 50 | 51 | if (mobileMenuToggle && mobileMenu) { 52 | mobileMenuToggle.addEventListener('click', () => { 53 | mobileMenu.classList.toggle('hidden'); 54 | }); 55 | } 56 | </script> ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/claude/client.go: -------------------------------------------------------------------------------- ```go 1 | package claude 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" 13 | ) 14 | 15 | // Client handles communication with the Claude API 16 | type Client struct { 17 | apiKey string 18 | baseURL string 19 | modelID string 20 | maxTokens int 21 | temperature float64 22 | httpClient *http.Client 23 | logger *logging.Logger 24 | } 25 | 26 | // Message represents a message in the Claude conversation 27 | type Message struct { 28 | Role string `json:"role"` 29 | Content string `json:"content"` 30 | } 31 | 32 | // CompletionRequest represents a request to the Claude API 33 | type CompletionRequest struct { 34 | Model string `json:"model"` 35 | System string `json:"system,omitempty"` 36 | Messages []Message `json:"messages"` 37 | MaxTokens int `json:"max_tokens,omitempty"` 38 | Temperature float64 `json:"temperature,omitempty"` 39 | } 40 | 41 | // ContentItem represents an item in the content array of a response 42 | type ContentItem struct { 43 | Type string `json:"type"` 44 | Text string `json:"text"` 45 | } 46 | 47 | // CompletionResponse represents a response from the Claude API 48 | type CompletionResponse struct { 49 | ID string `json:"id"` 50 | Type string `json:"type"` 51 | Model string `json:"model"` 52 | Content []ContentItem `json:"content"` 53 | Usage Usage `json:"usage"` 54 | } 55 | 56 | // Usage represents token usage information 57 | type Usage struct { 58 | InputTokens int `json:"input_tokens"` 59 | OutputTokens int `json:"output_tokens"` 60 | } 61 | 62 | // NewClient creates a new Claude API client 63 | func NewClient(cfg ClaudeConfig, logger *logging.Logger) *Client { 64 | if logger == nil { 65 | logger = logging.NewLogger().Named("claude") 66 | } 67 | 68 | return &Client{ 69 | apiKey: cfg.APIKey, 70 | baseURL: cfg.BaseURL, 71 | modelID: cfg.ModelID, 72 | maxTokens: cfg.MaxTokens, 73 | temperature: cfg.Temperature, 74 | httpClient: &http.Client{ 75 | Timeout: 120 * time.Second, 76 | }, 77 | logger: logger, 78 | } 79 | } 80 | 81 | // ClaudeConfig holds configuration for the Claude API client 82 | type ClaudeConfig struct { 83 | APIKey string `yaml:"apiKey"` 84 | BaseURL string `yaml:"baseURL"` 85 | ModelID string `yaml:"modelID"` 86 | MaxTokens int `yaml:"maxTokens"` 87 | Temperature float64 `yaml:"temperature"` 88 | } 89 | 90 | // Complete sends a completion request to the Claude API 91 | func (c *Client) Complete(ctx context.Context, messages []Message) (string, error) { 92 | c.logger.Debug("Sending completion request", 93 | "model", c.modelID, 94 | "messageCount", len(messages)) 95 | 96 | // Extract system message if present 97 | var systemPrompt string 98 | var userMessages []Message 99 | 100 | for _, msg := range messages { 101 | if msg.Role == "system" { 102 | systemPrompt = msg.Content 103 | } else { 104 | userMessages = append(userMessages, msg) 105 | } 106 | } 107 | 108 | reqBody := CompletionRequest{ 109 | Model: c.modelID, 110 | System: systemPrompt, 111 | Messages: userMessages, 112 | MaxTokens: c.maxTokens, 113 | Temperature: c.temperature, 114 | } 115 | 116 | reqJSON, err := json.Marshal(reqBody) 117 | if err != nil { 118 | return "", fmt.Errorf("failed to marshal request: %w", err) 119 | } 120 | 121 | req, err := http.NewRequestWithContext( 122 | ctx, 123 | http.MethodPost, 124 | c.baseURL+"/v1/messages", 125 | bytes.NewBuffer(reqJSON), 126 | ) 127 | if err != nil { 128 | return "", fmt.Errorf("failed to create request: %w", err) 129 | } 130 | 131 | req.Header.Set("Content-Type", "application/json") 132 | req.Header.Set("x-api-key", c.apiKey) 133 | req.Header.Set("anthropic-version", "2023-06-01") 134 | 135 | resp, err := c.httpClient.Do(req) 136 | if err != nil { 137 | return "", fmt.Errorf("failed to send request: %w", err) 138 | } 139 | defer resp.Body.Close() 140 | 141 | body, err := io.ReadAll(resp.Body) 142 | if err != nil { 143 | return "", fmt.Errorf("failed to read response body: %w", err) 144 | } 145 | 146 | if resp.StatusCode != http.StatusOK { 147 | return "", fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, body) 148 | } 149 | 150 | var completionResponse CompletionResponse 151 | if err := json.Unmarshal(body, &completionResponse); err != nil { 152 | return "", fmt.Errorf("failed to unmarshal response: %w", err) 153 | } 154 | 155 | // Extract text from content array 156 | var responseText string 157 | for _, content := range completionResponse.Content { 158 | if content.Type == "text" { 159 | responseText += content.Text 160 | } 161 | } 162 | 163 | c.logger.Debug("Received completion response", 164 | "model", completionResponse.Model, 165 | "inputTokens", completionResponse.Usage.InputTokens, 166 | "outputTokens", completionResponse.Usage.OutputTokens) 167 | 168 | return responseText, nil 169 | } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/gitlab/pipelines.go: -------------------------------------------------------------------------------- ```go 1 | package gitlab 2 | 3 | import ( 4 | "io" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "net/url" 10 | 11 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" 12 | ) 13 | 14 | // ListPipelines returns a list of pipelines for a project 15 | func (c *Client) ListPipelines(ctx context.Context, projectID string) ([]models.GitLabPipeline, error) { 16 | c.logger.Debug("Listing pipelines", "projectID", projectID) 17 | 18 | endpoint := fmt.Sprintf("projects/%s/pipelines", url.PathEscape(projectID)) 19 | 20 | // Add query parameters for pagination 21 | u, err := url.Parse(endpoint) 22 | if err != nil { 23 | return nil, fmt.Errorf("invalid endpoint: %w", err) 24 | } 25 | 26 | q := u.Query() 27 | q.Set("per_page", "20") 28 | q.Set("order_by", "id") 29 | q.Set("sort", "desc") 30 | u.RawQuery = q.Encode() 31 | 32 | resp, err := c.doRequest(ctx, http.MethodGet, u.String(), nil) 33 | if err != nil { 34 | return nil, err 35 | } 36 | defer resp.Body.Close() 37 | 38 | var pipelines []models.GitLabPipeline 39 | if err := json.NewDecoder(resp.Body).Decode(&pipelines); err != nil { 40 | return nil, fmt.Errorf("failed to decode response: %w", err) 41 | } 42 | 43 | c.logger.Debug("Listed pipelines", "projectID", projectID, "count", len(pipelines)) 44 | return pipelines, nil 45 | } 46 | 47 | // GetPipeline returns details about a specific pipeline 48 | func (c *Client) GetPipeline(ctx context.Context, projectID string, pipelineID int) (*models.GitLabPipeline, error) { 49 | c.logger.Debug("Getting pipeline", "projectID", projectID, "pipelineID", pipelineID) 50 | 51 | endpoint := fmt.Sprintf("projects/%s/pipelines/%d", url.PathEscape(projectID), pipelineID) 52 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 53 | if err != nil { 54 | return nil, err 55 | } 56 | defer resp.Body.Close() 57 | 58 | var pipeline models.GitLabPipeline 59 | if err := json.NewDecoder(resp.Body).Decode(&pipeline); err != nil { 60 | return nil, fmt.Errorf("failed to decode response: %w", err) 61 | } 62 | 63 | return &pipeline, nil 64 | } 65 | 66 | // GetPipelineJobs returns jobs for a specific pipeline 67 | func (c *Client) GetPipelineJobs(ctx context.Context, projectID string, pipelineID int) ([]models.GitLabJob, error) { 68 | c.logger.Debug("Getting pipeline jobs", "projectID", projectID, "pipelineID", pipelineID) 69 | 70 | endpoint := fmt.Sprintf("projects/%s/pipelines/%d/jobs", url.PathEscape(projectID), pipelineID) 71 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 72 | if err != nil { 73 | return nil, err 74 | } 75 | defer resp.Body.Close() 76 | 77 | var jobs []models.GitLabJob 78 | if err := json.NewDecoder(resp.Body).Decode(&jobs); err != nil { 79 | return nil, fmt.Errorf("failed to decode response: %w", err) 80 | } 81 | 82 | c.logger.Debug("Got pipeline jobs", "projectID", projectID, "pipelineID", pipelineID, "count", len(jobs)) 83 | return jobs, nil 84 | } 85 | 86 | // FindRecentDeployments finds recent deployments to a specific environment 87 | func (c *Client) FindRecentDeployments(ctx context.Context, projectID, environment string) ([]models.GitLabDeployment, error) { 88 | c.logger.Debug("Finding recent deployments", 89 | "projectID", projectID, 90 | "environment", environment) 91 | 92 | // Create endpoint with query parameters 93 | endpoint := fmt.Sprintf("projects/%s/deployments", url.PathEscape(projectID)) 94 | 95 | u, err := url.Parse(endpoint) 96 | if err != nil { 97 | return nil, fmt.Errorf("invalid endpoint: %w", err) 98 | } 99 | 100 | q := u.Query() 101 | q.Set("environment", environment) 102 | q.Set("order_by", "created_at") 103 | q.Set("sort", "desc") 104 | q.Set("per_page", "10") 105 | u.RawQuery = q.Encode() 106 | 107 | resp, err := c.doRequest(ctx, http.MethodGet, u.String(), nil) 108 | if err != nil { 109 | return nil, err 110 | } 111 | defer resp.Body.Close() 112 | 113 | var deployments []models.GitLabDeployment 114 | if err := json.NewDecoder(resp.Body).Decode(&deployments); err != nil { 115 | return nil, fmt.Errorf("failed to decode response: %w", err) 116 | } 117 | 118 | c.logger.Debug("Found deployments", 119 | "projectID", projectID, 120 | "environment", environment, 121 | "count", len(deployments)) 122 | return deployments, nil 123 | } 124 | 125 | // GetJobLogs retrieves logs for a specific job 126 | func (c *Client) GetJobLogs(ctx context.Context, projectID string, jobID int) (string, error) { 127 | c.logger.Debug("Getting job logs", "projectID", projectID, "jobID", jobID) 128 | 129 | endpoint := fmt.Sprintf("projects/%s/jobs/%d/trace", url.PathEscape(projectID), jobID) 130 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 131 | if err != nil { 132 | return "", err 133 | } 134 | defer resp.Body.Close() 135 | 136 | logs, err := io.ReadAll(resp.Body) 137 | if err != nil { 138 | return "", fmt.Errorf("failed to read logs: %w", err) 139 | } 140 | 141 | return string(logs), nil 142 | } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/models/context.go: -------------------------------------------------------------------------------- ```go 1 | package models 2 | 3 | // ResourceContext combines information about a Kubernetes resource with GitOps context 4 | type ResourceContext struct { 5 | // Basic resource information 6 | Kind string `json:"kind"` 7 | Name string `json:"name"` 8 | Namespace string `json:"namespace"` 9 | APIVersion string `json:"apiVersion"` 10 | Metadata map[string]interface{} `json:"metadata,omitempty"` 11 | ResourceData string `json:"resourceData,omitempty"` 12 | 13 | // Related ArgoCD information 14 | ArgoApplication *ArgoApplication `json:"argoApplication,omitempty"` 15 | ArgoSyncStatus string `json:"argoSyncStatus,omitempty"` 16 | ArgoHealthStatus string `json:"argoHealthStatus,omitempty"` 17 | ArgoSyncHistory []ArgoApplicationHistory `json:"argoSyncHistory,omitempty"` 18 | 19 | // Related GitLab information 20 | GitLabProject *GitLabProject `json:"gitlabProject,omitempty"` 21 | LastPipeline *GitLabPipeline `json:"lastPipeline,omitempty"` 22 | LastDeployment *GitLabDeployment `json:"lastDeployment,omitempty"` 23 | RecentCommits []GitLabCommit `json:"recentCommits,omitempty"` 24 | 25 | // Additional context 26 | Events []K8sEvent `json:"events,omitempty"` 27 | RelatedResources []string `json:"relatedResources,omitempty"` 28 | Errors []string `json:"errors,omitempty"` 29 | } 30 | 31 | // Issue represents a discovered issue or potential problem 32 | type Issue struct { 33 | Title string `json:"title"` 34 | Category string `json:"category"` 35 | Severity string `json:"severity"` 36 | Source string `json:"source"` 37 | Description string `json:"description"` 38 | } 39 | 40 | // TroubleshootResult contains troubleshooting findings and recommendations 41 | type TroubleshootResult struct { 42 | ResourceContext ResourceContext `json:"resourceContext"` 43 | Issues []Issue `json:"issues"` 44 | Recommendations []string `json:"recommendations"` 45 | } 46 | 47 | // MCPRequest represents a request to the MCP server 48 | type MCPRequest struct { 49 | Action string `json:"action"` 50 | Resource string `json:"resource,omitempty"` 51 | Namespace string `json:"namespace,omitempty"` 52 | Name string `json:"name,omitempty"` 53 | Query string `json:"query,omitempty"` 54 | CommitSHA string `json:"commitSha,omitempty"` 55 | ProjectID string `json:"projectId,omitempty"` 56 | MergeRequestIID int `json:"mergeRequestIid,omitempty"` 57 | ResourceSpecs map[string]interface{} `json:"resourceSpecs,omitempty"` 58 | Context string `json:"context,omitempty"` 59 | } 60 | 61 | // ResourceRelationship represents a relationship between two resources 62 | type ResourceRelationship struct { 63 | SourceKind string `json:"sourceKind"` 64 | SourceName string `json:"sourceName"` 65 | SourceNamespace string `json:"sourceNamespace"` 66 | TargetKind string `json:"targetKind"` 67 | TargetName string `json:"targetName"` 68 | TargetNamespace string `json:"targetNamespace"` 69 | RelationType string `json:"relationType"` 70 | } 71 | 72 | // NamespaceAnalysisResult contains the analysis of a namespace's resources 73 | type NamespaceAnalysisResult struct { 74 | Namespace string `json:"namespace"` 75 | ResourceCounts map[string]int `json:"resourceCounts"` 76 | HealthStatus map[string]map[string]int `json:"healthStatus"` 77 | ResourceRelationships []ResourceRelationship `json:"resourceRelationships"` 78 | Issues []Issue `json:"issues"` 79 | Recommendations []string `json:"recommendations"` 80 | Analysis string `json:"analysis"` 81 | } 82 | 83 | // MCPResponse represents a response from the MCP server 84 | type MCPResponse struct { 85 | Success bool `json:"success"` 86 | Message string `json:"message,omitempty"` 87 | Analysis string `json:"analysis,omitempty"` 88 | Context ResourceContext `json:"context,omitempty"` 89 | Actions []string `json:"actions,omitempty"` 90 | ErrorDetails string `json:"errorDetails,omitempty"` 91 | TroubleshootResult *TroubleshootResult `json:"troubleshootResult,omitempty"` 92 | NamespaceAnalysis *NamespaceAnalysisResult `json:"namespaceAnalysis,omitempty"` 93 | } 94 | ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/auth/secrets.go: -------------------------------------------------------------------------------- ```go 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" 10 | ) 11 | 12 | // SecretsManager handles access to secrets stored in various backends 13 | type SecretsManager struct { 14 | logger *logging.Logger 15 | // Directory where secrets files are stored 16 | secretsDir string 17 | // Flag to indicate if secrets manager is available 18 | available bool 19 | } 20 | 21 | // NewSecretsManager creates a new secrets manager 22 | func NewSecretsManager(logger *logging.Logger) *SecretsManager { 23 | if logger == nil { 24 | logger = logging.NewLogger().Named("secrets") 25 | } 26 | 27 | // Default secrets directory is ./secrets 28 | secretsDir := os.Getenv("SECRETS_DIR") 29 | if secretsDir == "" { 30 | secretsDir = "./secrets" 31 | } 32 | 33 | // Check if secrets directory exists 34 | _, err := os.Stat(secretsDir) 35 | available := err == nil 36 | 37 | if !available { 38 | logger.Warn("Secrets directory not available", "directory", secretsDir) 39 | } 40 | 41 | return &SecretsManager{ 42 | logger: logger, 43 | secretsDir: secretsDir, 44 | available: available, 45 | } 46 | } 47 | 48 | // IsAvailable returns true if the secrets manager is available 49 | func (sm *SecretsManager) IsAvailable() bool { 50 | return sm.available 51 | } 52 | 53 | // GetCredentials retrieves credentials for a service from the secrets manager 54 | func (sm *SecretsManager) GetCredentials(ctx context.Context, service string) (*Credentials, error) { 55 | if !sm.available { 56 | return nil, fmt.Errorf("secrets manager not available") 57 | } 58 | 59 | // Build paths to potential secret files 60 | tokenPath := filepath.Join(sm.secretsDir, service, "token") 61 | apiKeyPath := filepath.Join(sm.secretsDir, service, "apikey") 62 | usernamePath := filepath.Join(sm.secretsDir, service, "username") 63 | passwordPath := filepath.Join(sm.secretsDir, service, "password") 64 | 65 | // Initialize credentials 66 | creds := &Credentials{} 67 | 68 | // Try to read token 69 | tokenBytes, err := os.ReadFile(tokenPath) 70 | if err == nil { 71 | creds.Token = string(tokenBytes) 72 | sm.logger.Debug("Loaded token from file", "service", service) 73 | } 74 | 75 | // Try to read API key 76 | apiKeyBytes, err := os.ReadFile(apiKeyPath) 77 | if err == nil { 78 | creds.APIKey = string(apiKeyBytes) 79 | sm.logger.Debug("Loaded API key from file", "service", service) 80 | } 81 | 82 | // Try to read username 83 | usernameBytes, err := os.ReadFile(usernamePath) 84 | if err == nil { 85 | creds.Username = string(usernameBytes) 86 | sm.logger.Debug("Loaded username from file", "service", service) 87 | } 88 | 89 | // Try to read password 90 | passwordBytes, err := os.ReadFile(passwordPath) 91 | if err == nil { 92 | creds.Password = string(passwordBytes) 93 | sm.logger.Debug("Loaded password from file", "service", service) 94 | } 95 | 96 | // Check if we loaded any credentials 97 | if creds.Token == "" && creds.APIKey == "" && creds.Username == "" && creds.Password == "" { 98 | return nil, fmt.Errorf("no credentials found for service: %s", service) 99 | } 100 | 101 | return creds, nil 102 | } 103 | 104 | // SaveCredentials saves credentials for a service to the secrets manager 105 | func (sm *SecretsManager) SaveCredentials(ctx context.Context, service string, creds *Credentials) error { 106 | if !sm.available { 107 | return fmt.Errorf("secrets manager not available") 108 | } 109 | 110 | // Create service directory if it doesn't exist 111 | serviceDir := filepath.Join(sm.secretsDir, service) 112 | if err := os.MkdirAll(serviceDir, 0700); err != nil { 113 | return fmt.Errorf("failed to create service directory: %w", err) 114 | } 115 | 116 | // Save token if provided 117 | if creds.Token != "" { 118 | tokenPath := filepath.Join(serviceDir, "token") 119 | if err := os.WriteFile(tokenPath, []byte(creds.Token), 0600); err != nil { 120 | return fmt.Errorf("failed to save token: %w", err) 121 | } 122 | sm.logger.Debug("Saved token to file", "service", service) 123 | } 124 | 125 | // Save API key if provided 126 | if creds.APIKey != "" { 127 | apiKeyPath := filepath.Join(serviceDir, "apikey") 128 | if err := os.WriteFile(apiKeyPath, []byte(creds.APIKey), 0600); err != nil { 129 | return fmt.Errorf("failed to save API key: %w", err) 130 | } 131 | sm.logger.Debug("Saved API key to file", "service", service) 132 | } 133 | 134 | // Save username if provided 135 | if creds.Username != "" { 136 | usernamePath := filepath.Join(serviceDir, "username") 137 | if err := os.WriteFile(usernamePath, []byte(creds.Username), 0600); err != nil { 138 | return fmt.Errorf("failed to save username: %w", err) 139 | } 140 | sm.logger.Debug("Saved username to file", "service", service) 141 | } 142 | 143 | // Save password if provided 144 | if creds.Password != "" { 145 | passwordPath := filepath.Join(serviceDir, "password") 146 | if err := os.WriteFile(passwordPath, []byte(creds.Password), 0600); err != nil { 147 | return fmt.Errorf("failed to save password: %w", err) 148 | } 149 | sm.logger.Debug("Saved password to file", "service", service) 150 | } 151 | 152 | return nil 153 | } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/argocd/applications.go: -------------------------------------------------------------------------------- ```go 1 | package argocd 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" 12 | ) 13 | 14 | // ListApplications returns a list of all ArgoCD applications 15 | func (c *Client) ListApplications(ctx context.Context) ([]models.ArgoApplication, error) { 16 | c.logger.Debug("Listing ArgoCD applications") 17 | 18 | // Try the v1 API path 19 | endpoint := "/api/v1/applications" 20 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 21 | if err != nil { 22 | return nil, err 23 | } 24 | defer resp.Body.Close() 25 | 26 | var result struct { 27 | Items []models.ArgoApplication `json:"items"` 28 | } 29 | 30 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 31 | return nil, fmt.Errorf("failed to decode response: %w", err) 32 | } 33 | 34 | c.logger.Debug("Listed ArgoCD applications", "count", len(result.Items)) 35 | return result.Items, nil 36 | } 37 | 38 | // GetApplication returns details about a specific ArgoCD application 39 | func (c *Client) GetApplication(ctx context.Context, name string) (*models.ArgoApplication, error) { 40 | c.logger.Debug("Getting ArgoCD application", "name", name) 41 | 42 | endpoint := fmt.Sprintf("/api/v1/applications/%s", url.PathEscape(name)) 43 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 44 | if err != nil { 45 | return nil, err 46 | } 47 | defer resp.Body.Close() 48 | 49 | var app models.ArgoApplication 50 | if err := json.NewDecoder(resp.Body).Decode(&app); err != nil { 51 | return nil, fmt.Errorf("failed to decode response: %w", err) 52 | } 53 | 54 | return &app, nil 55 | } 56 | 57 | // GetResourceTree returns the resource hierarchy for an application 58 | func (c *Client) GetResourceTree(ctx context.Context, name string) (*models.ArgoResourceTree, error) { 59 | c.logger.Debug("Getting resource tree for application", "name", name) 60 | 61 | endpoint := fmt.Sprintf("/api/v1/applications/%s/resource-tree", url.PathEscape(name)) 62 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 63 | if err != nil { 64 | return nil, err 65 | } 66 | defer resp.Body.Close() 67 | 68 | var tree models.ArgoResourceTree 69 | if err := json.NewDecoder(resp.Body).Decode(&tree); err != nil { 70 | return nil, fmt.Errorf("failed to decode response: %w", err) 71 | } 72 | 73 | c.logger.Debug("Retrieved resource tree", "name", name, "nodeCount", len(tree.Nodes)) 74 | return &tree, nil 75 | } 76 | 77 | // FindApplicationsByResource finds all ArgoCD applications that manage a specific Kubernetes resource 78 | func (c *Client) FindApplicationsByResource(ctx context.Context, kind, name, namespace string) ([]models.ArgoApplication, error) { 79 | c.logger.Debug("Finding applications by resource", 80 | "kind", kind, 81 | "name", name, 82 | "namespace", namespace) 83 | 84 | // First try to use the resource API endpoint if available 85 | endpoint := fmt.Sprintf("/api/v1/applications/resource/%s/%s/%s/%s/%s", 86 | url.PathEscape(""), 87 | url.PathEscape(kind), 88 | url.PathEscape(namespace), 89 | url.PathEscape(name), 90 | url.PathEscape(""), 91 | ) 92 | 93 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 94 | if err == nil { 95 | defer resp.Body.Close() 96 | 97 | var appRefs []struct { 98 | Name string `json:"name"` 99 | } 100 | 101 | if err := json.NewDecoder(resp.Body).Decode(&appRefs); err != nil { 102 | c.logger.Warn("Failed to decode application references", "error", err) 103 | } else if len(appRefs) > 0 { 104 | // Get full application details for each reference 105 | var apps []models.ArgoApplication 106 | for _, ref := range appRefs { 107 | app, err := c.GetApplication(ctx, ref.Name) 108 | if err != nil { 109 | c.logger.Warn("Failed to get application details", 110 | "name", ref.Name, 111 | "error", err) 112 | continue 113 | } 114 | apps = append(apps, *app) 115 | } 116 | 117 | c.logger.Debug("Found applications by resource API", 118 | "resourceKind", kind, 119 | "resourceName", name, 120 | "count", len(apps)) 121 | return apps, nil 122 | } 123 | } 124 | 125 | // Fallback: Get all applications and check their resource trees 126 | c.logger.Debug("Resource API failed, falling back to application scanning") 127 | apps, err := c.ListApplications(ctx) 128 | if err != nil { 129 | return nil, fmt.Errorf("failed to list applications: %w", err) 130 | } 131 | 132 | var matchingApps []models.ArgoApplication 133 | 134 | // For each application, check if it manages the specified resource 135 | for _, app := range apps { 136 | tree, err := c.GetResourceTree(ctx, app.Name) 137 | if err != nil { 138 | c.logger.Warn("Failed to get resource tree", 139 | "application", app.Name, 140 | "error", err) 141 | continue // Skip this app if we can't get its resource tree 142 | } 143 | 144 | for _, node := range tree.Nodes { 145 | // Match against the specified resource 146 | if strings.EqualFold(node.Kind, kind) && 147 | node.Name == name && 148 | (namespace == "" || node.Namespace == namespace) { 149 | matchingApps = append(matchingApps, app) 150 | break // Found a match in this app, move to the next app 151 | } 152 | } 153 | } 154 | 155 | c.logger.Debug("Found applications managing resource by scanning", 156 | "resourceKind", kind, 157 | "resourceName", name, 158 | "count", len(matchingApps)) 159 | return matchingApps, nil 160 | } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/cmd/server/main.go: -------------------------------------------------------------------------------- ```go 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/api" 12 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/argocd" 13 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/auth" 14 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/claude" 15 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/correlator" 16 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/gitlab" 17 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/k8s" 18 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/mcp" 19 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/config" 20 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" 21 | ) 22 | 23 | func main() { 24 | 25 | // Parse command line flags 26 | configPath := flag.String("config", "config.yaml", "path to config file") 27 | logLevel := flag.String("log-level", "info", "logging level (debug, info, warn, error)") 28 | flag.Parse() 29 | 30 | // Initialize logger 31 | os.Setenv("LOG_LEVEL", *logLevel) 32 | logger := logging.NewLogger() 33 | logger.Info("Starting Kubernetes Claude MCP server") 34 | 35 | // Load configuration 36 | logger.Info("Loading configuration", "path", *configPath) 37 | cfg, err := config.Load(*configPath) 38 | if err != nil { 39 | logger.Fatal("Failed to load configuration", "error", err) 40 | } 41 | 42 | // Validate configuration 43 | if err := cfg.Validate(); err != nil { 44 | logger.Fatal("Invalid configuration", "error", err) 45 | } 46 | 47 | // Set up context with cancellation 48 | ctx, cancel := context.WithCancel(context.Background()) 49 | defer cancel() 50 | 51 | // Initialize credential provider 52 | logger.Info("Initializing credential provider") 53 | credProvider := auth.NewCredentialProvider(cfg) 54 | if err := credProvider.LoadCredentials(ctx); err != nil { 55 | logger.Fatal("Failed to load credentials", "error", err) 56 | } 57 | 58 | // Initialize Kubernetes client 59 | logger.Info("Initializing Kubernetes client") 60 | k8sClient, err := k8s.NewClient(cfg.Kubernetes, logger.Named("k8s")) 61 | if err != nil { 62 | logger.Fatal("Failed to create Kubernetes client", "error", err) 63 | } 64 | 65 | // Check Kubernetes connectivity 66 | if err := k8sClient.CheckConnectivity(ctx); err != nil { 67 | logger.Warn("Kubernetes connectivity check failed", "error", err) 68 | } else { 69 | logger.Info("Kubernetes connectivity confirmed") 70 | } 71 | 72 | // Initialize ArgoCD client 73 | logger.Info("Initializing ArgoCD client") 74 | argoClient := argocd.NewClient(&cfg.ArgoCD, credProvider, logger.Named("argocd")) 75 | 76 | // Check ArgoCD connectivity (don't fail if unavailable) 77 | if err := argoClient.CheckConnectivity(ctx); err != nil { 78 | logger.Warn("ArgoCD connectivity check failed", "error", err) 79 | } else { 80 | logger.Info("ArgoCD connectivity confirmed") 81 | } 82 | 83 | // Initialize GitLab client 84 | logger.Info("Initializing GitLab client") 85 | gitlabClient := gitlab.NewClient(&cfg.GitLab, credProvider, logger.Named("gitlab")) 86 | 87 | // Check GitLab connectivity (don't fail if unavailable) 88 | if err := gitlabClient.CheckConnectivity(ctx); err != nil { 89 | logger.Warn("GitLab connectivity check failed", "error", err) 90 | } else { 91 | logger.Info("GitLab connectivity confirmed") 92 | } 93 | 94 | // Initialize Claude client 95 | logger.Info("Initializing Claude client") 96 | claudeConfig := claude.ClaudeConfig{ 97 | APIKey: cfg.Claude.APIKey, 98 | BaseURL: cfg.Claude.BaseURL, 99 | ModelID: cfg.Claude.ModelID, 100 | MaxTokens: cfg.Claude.MaxTokens, 101 | Temperature: cfg.Claude.Temperature, 102 | } 103 | claudeClient := claude.NewClient(claudeConfig, logger.Named("claude")) 104 | 105 | // Initialize GitOps correlator 106 | logger.Info("Initializing GitOps correlator") 107 | gitOpsCorrelator := correlator.NewGitOpsCorrelator( 108 | k8sClient, 109 | argoClient, 110 | gitlabClient, 111 | logger.Named("correlator"), 112 | ) 113 | 114 | // Initialize troubleshoot correlator 115 | troubleshootCorrelator := correlator.NewTroubleshootCorrelator( 116 | gitOpsCorrelator, 117 | k8sClient, 118 | logger.Named("troubleshoot"), 119 | ) 120 | 121 | // Initialize MCP protocol handler 122 | logger.Info("Initializing MCP protocol handler") 123 | mcpHandler := mcp.NewProtocolHandler( 124 | claudeClient, 125 | gitOpsCorrelator, 126 | k8sClient, 127 | logger.Named("mcp"), 128 | ) 129 | 130 | // Initialize API server 131 | logger.Info("Initializing API server") 132 | server := api.NewServer( 133 | cfg.Server, 134 | k8sClient, 135 | argoClient, 136 | gitlabClient, 137 | mcpHandler, 138 | troubleshootCorrelator, 139 | logger.Named("api"), 140 | ) 141 | 142 | // Handle graceful shutdown 143 | go func() { 144 | sigCh := make(chan os.Signal, 1) 145 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 146 | sig := <-sigCh 147 | logger.Info("Received shutdown signal", "signal", sig) 148 | 149 | // Create a timeout context for shutdown 150 | shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) 151 | defer shutdownCancel() 152 | 153 | logger.Info("Shutting down server...") 154 | cancel() // Cancel the main context 155 | 156 | // Wait for server to shut down or timeout 157 | <-shutdownCtx.Done() 158 | }() 159 | 160 | // Start server 161 | logger.Info("Starting MCP server", "address", cfg.Server.Address) 162 | if err := server.Start(ctx); err != nil { 163 | logger.Fatal("Server error", "error", err) 164 | } 165 | 166 | logger.Info("Server shutdown complete") 167 | } ``` -------------------------------------------------------------------------------- /docs/.astro/content.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | declare module 'astro:content' { 2 | interface Render { 3 | '.mdx': Promise<{ 4 | Content: import('astro').MarkdownInstance<{}>['Content']; 5 | headings: import('astro').MarkdownHeading[]; 6 | remarkPluginFrontmatter: Record<string, any>; 7 | components: import('astro').MDXInstance<{}>['components']; 8 | }>; 9 | } 10 | } 11 | 12 | declare module 'astro:content' { 13 | export interface RenderResult { 14 | Content: import('astro/runtime/server/index.js').AstroComponentFactory; 15 | headings: import('astro').MarkdownHeading[]; 16 | remarkPluginFrontmatter: Record<string, any>; 17 | } 18 | interface Render { 19 | '.md': Promise<RenderResult>; 20 | } 21 | 22 | export interface RenderedContent { 23 | html: string; 24 | metadata?: { 25 | imagePaths: Array<string>; 26 | [key: string]: unknown; 27 | }; 28 | } 29 | } 30 | 31 | declare module 'astro:content' { 32 | type Flatten<T> = T extends { [K: string]: infer U } ? U : never; 33 | 34 | export type CollectionKey = keyof AnyEntryMap; 35 | export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>; 36 | 37 | export type ContentCollectionKey = keyof ContentEntryMap; 38 | export type DataCollectionKey = keyof DataEntryMap; 39 | 40 | type AllValuesOf<T> = T extends any ? T[keyof T] : never; 41 | type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf< 42 | ContentEntryMap[C] 43 | >['slug']; 44 | 45 | export type ReferenceDataEntry< 46 | C extends CollectionKey, 47 | E extends keyof DataEntryMap[C] = string, 48 | > = { 49 | collection: C; 50 | id: E; 51 | }; 52 | export type ReferenceContentEntry< 53 | C extends keyof ContentEntryMap, 54 | E extends ValidContentEntrySlug<C> | (string & {}) = string, 55 | > = { 56 | collection: C; 57 | slug: E; 58 | }; 59 | 60 | /** @deprecated Use `getEntry` instead. */ 61 | export function getEntryBySlug< 62 | C extends keyof ContentEntryMap, 63 | E extends ValidContentEntrySlug<C> | (string & {}), 64 | >( 65 | collection: C, 66 | // Note that this has to accept a regular string too, for SSR 67 | entrySlug: E, 68 | ): E extends ValidContentEntrySlug<C> 69 | ? Promise<CollectionEntry<C>> 70 | : Promise<CollectionEntry<C> | undefined>; 71 | 72 | /** @deprecated Use `getEntry` instead. */ 73 | export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>( 74 | collection: C, 75 | entryId: E, 76 | ): Promise<CollectionEntry<C>>; 77 | 78 | export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>( 79 | collection: C, 80 | filter?: (entry: CollectionEntry<C>) => entry is E, 81 | ): Promise<E[]>; 82 | export function getCollection<C extends keyof AnyEntryMap>( 83 | collection: C, 84 | filter?: (entry: CollectionEntry<C>) => unknown, 85 | ): Promise<CollectionEntry<C>[]>; 86 | 87 | export function getEntry< 88 | C extends keyof ContentEntryMap, 89 | E extends ValidContentEntrySlug<C> | (string & {}), 90 | >( 91 | entry: ReferenceContentEntry<C, E>, 92 | ): E extends ValidContentEntrySlug<C> 93 | ? Promise<CollectionEntry<C>> 94 | : Promise<CollectionEntry<C> | undefined>; 95 | export function getEntry< 96 | C extends keyof DataEntryMap, 97 | E extends keyof DataEntryMap[C] | (string & {}), 98 | >( 99 | entry: ReferenceDataEntry<C, E>, 100 | ): E extends keyof DataEntryMap[C] 101 | ? Promise<DataEntryMap[C][E]> 102 | : Promise<CollectionEntry<C> | undefined>; 103 | export function getEntry< 104 | C extends keyof ContentEntryMap, 105 | E extends ValidContentEntrySlug<C> | (string & {}), 106 | >( 107 | collection: C, 108 | slug: E, 109 | ): E extends ValidContentEntrySlug<C> 110 | ? Promise<CollectionEntry<C>> 111 | : Promise<CollectionEntry<C> | undefined>; 112 | export function getEntry< 113 | C extends keyof DataEntryMap, 114 | E extends keyof DataEntryMap[C] | (string & {}), 115 | >( 116 | collection: C, 117 | id: E, 118 | ): E extends keyof DataEntryMap[C] 119 | ? string extends keyof DataEntryMap[C] 120 | ? Promise<DataEntryMap[C][E]> | undefined 121 | : Promise<DataEntryMap[C][E]> 122 | : Promise<CollectionEntry<C> | undefined>; 123 | 124 | /** Resolve an array of entry references from the same collection */ 125 | export function getEntries<C extends keyof ContentEntryMap>( 126 | entries: ReferenceContentEntry<C, ValidContentEntrySlug<C>>[], 127 | ): Promise<CollectionEntry<C>[]>; 128 | export function getEntries<C extends keyof DataEntryMap>( 129 | entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[], 130 | ): Promise<CollectionEntry<C>[]>; 131 | 132 | export function render<C extends keyof AnyEntryMap>( 133 | entry: AnyEntryMap[C][string], 134 | ): Promise<RenderResult>; 135 | 136 | export function reference<C extends keyof AnyEntryMap>( 137 | collection: C, 138 | ): import('astro/zod').ZodEffects< 139 | import('astro/zod').ZodString, 140 | C extends keyof ContentEntryMap 141 | ? ReferenceContentEntry<C, ValidContentEntrySlug<C>> 142 | : ReferenceDataEntry<C, keyof DataEntryMap[C]> 143 | >; 144 | // Allow generic `string` to avoid excessive type errors in the config 145 | // if `dev` is not running to update as you edit. 146 | // Invalid collection names will be caught at build time. 147 | export function reference<C extends string>( 148 | collection: C, 149 | ): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>; 150 | 151 | type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T; 152 | type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer< 153 | ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']> 154 | >; 155 | 156 | type ContentEntryMap = { 157 | 158 | }; 159 | 160 | type DataEntryMap = { 161 | "docs": Record<string, { 162 | id: string; 163 | render(): Render[".md"]; 164 | slug: string; 165 | body: string; 166 | collection: "docs"; 167 | data: InferEntrySchema<"docs">; 168 | rendered?: RenderedContent; 169 | filePath?: string; 170 | }>; 171 | 172 | }; 173 | 174 | type AnyEntryMap = ContentEntryMap & DataEntryMap; 175 | 176 | export type ContentConfig = typeof import("../src/content/config.js"); 177 | } 178 | ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/k8s/client.go: -------------------------------------------------------------------------------- ```go 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path/filepath" 7 | 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/client-go/discovery" 10 | "k8s.io/client-go/dynamic" 11 | "k8s.io/client-go/kubernetes" 12 | "k8s.io/client-go/rest" 13 | "k8s.io/client-go/tools/clientcmd" 14 | "k8s.io/client-go/util/homedir" 15 | 16 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/config" 17 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" 18 | ) 19 | 20 | // Client wraps the Kubernetes clientset and provides additional functionality 21 | type Client struct { 22 | clientset *kubernetes.Clientset 23 | dynamicClient dynamic.Interface 24 | discoveryClient *discovery.DiscoveryClient 25 | restConfig *rest.Config 26 | defaultNS string 27 | logger *logging.Logger 28 | ResourceMapper *ResourceMapper 29 | } 30 | 31 | // NewClient creates a new Kubernetes client based on the provided configuration 32 | func NewClient(cfg config.KubernetesConfig, logger *logging.Logger) (*Client, error) { 33 | if logger == nil { 34 | logger = logging.NewLogger().Named("k8s") 35 | } 36 | 37 | var restConfig *rest.Config 38 | var err error 39 | 40 | logger.Debug("Initializing Kubernetes client", 41 | "inCluster", cfg.InCluster, 42 | "kubeconfig", cfg.KubeConfig, 43 | "defaultNamespace", cfg.DefaultNamespace) 44 | 45 | if cfg.InCluster { 46 | // Use in-cluster config when deployed inside Kubernetes 47 | restConfig, err = rest.InClusterConfig() 48 | if err != nil { 49 | return nil, fmt.Errorf("failed to create in-cluster config: %w", err) 50 | } 51 | logger.Debug("Using in-cluster configuration") 52 | } else { 53 | // Use kubeconfig file 54 | kubeconfigPath := cfg.KubeConfig 55 | if kubeconfigPath == "" { 56 | // Try to use default location if not specified 57 | if home := homedir.HomeDir(); home != "" { 58 | kubeconfigPath = filepath.Join(home, ".kube", "config") 59 | logger.Debug("Using default kubeconfig path", "path", kubeconfigPath) 60 | } else { 61 | return nil, fmt.Errorf("kubeconfig not specified and home directory not found") 62 | } 63 | } 64 | 65 | // Build config from kubeconfig file 66 | configLoadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath} 67 | configOverrides := &clientcmd.ConfigOverrides{} 68 | 69 | if cfg.DefaultContext != "" { 70 | configOverrides.CurrentContext = cfg.DefaultContext 71 | logger.Debug("Using specified context", "context", cfg.DefaultContext) 72 | } 73 | 74 | kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 75 | configLoadingRules, 76 | configOverrides, 77 | ) 78 | 79 | restConfig, err = kubeConfig.ClientConfig() 80 | if err != nil { 81 | return nil, fmt.Errorf("failed to build kubeconfig: %w", err) 82 | } 83 | } 84 | 85 | // Increase QPS and Burst for better performance in busy environments 86 | restConfig.QPS = 100 87 | restConfig.Burst = 100 88 | 89 | // Create clientset 90 | clientset, err := kubernetes.NewForConfig(restConfig) 91 | if err != nil { 92 | return nil, fmt.Errorf("failed to create Kubernetes clientset: %w", err) 93 | } 94 | 95 | // Create dynamic client 96 | dynamicClient, err := dynamic.NewForConfig(restConfig) 97 | if err != nil { 98 | return nil, fmt.Errorf("failed to create dynamic client: %w", err) 99 | } 100 | 101 | // Create discovery client 102 | discoveryClient, err := discovery.NewDiscoveryClientForConfig(restConfig) 103 | if err != nil { 104 | return nil, fmt.Errorf("failed to create discovery client: %w", err) 105 | } 106 | 107 | defaultNamespace := cfg.DefaultNamespace 108 | if defaultNamespace == "" { 109 | defaultNamespace = "default" 110 | } 111 | 112 | logger.Info("Kubernetes client initialized", 113 | "defaultNamespace", defaultNamespace) 114 | 115 | // Create the client instance 116 | client := &Client{ 117 | clientset: clientset, 118 | dynamicClient: dynamicClient, 119 | discoveryClient: discoveryClient, 120 | restConfig: restConfig, 121 | defaultNS: defaultNamespace, 122 | logger: logger, 123 | } 124 | 125 | // Initialize the ResourceMapper (ensure NewResourceMapper is defined in your package) 126 | client.ResourceMapper = NewResourceMapper(client) 127 | 128 | return client, nil 129 | } 130 | 131 | // CheckConnectivity verifies connectivity to the Kubernetes API 132 | func (c *Client) CheckConnectivity(ctx context.Context) error { 133 | c.logger.Debug("Checking Kubernetes connectivity") 134 | 135 | // Try to get server version as a basic connectivity test 136 | _, err := c.clientset.Discovery().ServerVersion() 137 | if err != nil { 138 | c.logger.Warn("Kubernetes connectivity check failed", "error", err) 139 | return fmt.Errorf("failed to connect to Kubernetes API: %w", err) 140 | } 141 | 142 | c.logger.Debug("Kubernetes connectivity check successful") 143 | return nil 144 | } 145 | 146 | // GetNamespaces returns a list of all namespaces in the cluster 147 | func (c *Client) GetNamespaces(ctx context.Context) ([]string, error) { 148 | c.logger.Debug("Getting namespaces") 149 | 150 | namespaceList, err := c.clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) 151 | if err != nil { 152 | return nil, fmt.Errorf("failed to list namespaces: %w", err) 153 | } 154 | 155 | var namespaces []string 156 | for _, ns := range namespaceList.Items { 157 | namespaces = append(namespaces, ns.Name) 158 | } 159 | 160 | c.logger.Debug("Got namespaces", "count", len(namespaces)) 161 | return namespaces, nil 162 | } 163 | 164 | // GetDefaultNamespace returns the default namespace for operations 165 | func (c *Client) GetDefaultNamespace() string { 166 | return c.defaultNS 167 | } 168 | 169 | // GetRestConfig returns the Kubernetes REST configuration 170 | func (c *Client) GetRestConfig() *rest.Config { 171 | return c.restConfig 172 | } 173 | 174 | // GetClientset returns the Kubernetes clientset 175 | func (c *Client) GetClientset() *kubernetes.Clientset { 176 | return c.clientset 177 | } 178 | 179 | // GetDynamicClient returns the dynamic client 180 | func (c *Client) GetDynamicClient() dynamic.Interface { 181 | return c.dynamicClient 182 | } 183 | 184 | // GetDiscoveryClient returns the discovery client 185 | func (c *Client) GetDiscoveryClient() *discovery.DiscoveryClient { 186 | return c.discoveryClient 187 | } 188 | 189 | // GetNamespaceTopology returns the topology for a specific namespace 190 | func (c *Client) GetNamespaceTopology(ctx context.Context, namespace string) (*NamespaceTopology, error) { 191 | return c.ResourceMapper.GetNamespaceTopology(ctx, namespace) 192 | } 193 | ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/api/server.go: -------------------------------------------------------------------------------- ```go 1 | package api 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strings" 7 | "time" 8 | 9 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/argocd" 10 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/correlator" 11 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/gitlab" 12 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/k8s" 13 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/mcp" 14 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/config" 15 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" 16 | "github.com/gorilla/mux" 17 | ) 18 | 19 | // Server represents the API server 20 | type Server struct { 21 | router *mux.Router 22 | server *http.Server 23 | k8sClient *k8s.Client 24 | argoClient *argocd.Client 25 | gitlabClient *gitlab.Client 26 | mcpHandler *mcp.ProtocolHandler 27 | troubleshootCorrelator *correlator.TroubleshootCorrelator 28 | resourceMapper *k8s.ResourceMapper 29 | config config.ServerConfig 30 | logger *logging.Logger 31 | } 32 | 33 | // NewServer creates a new API server 34 | func NewServer( 35 | cfg config.ServerConfig, 36 | k8sClient *k8s.Client, 37 | argoClient *argocd.Client, 38 | gitlabClient *gitlab.Client, 39 | mcpHandler *mcp.ProtocolHandler, 40 | troubleshootCorrelator *correlator.TroubleshootCorrelator, 41 | logger *logging.Logger, 42 | ) *Server { 43 | if logger == nil { 44 | logger = logging.NewLogger().Named("api") 45 | } 46 | 47 | server := &Server{ 48 | router: mux.NewRouter(), 49 | k8sClient: k8sClient, 50 | argoClient: argoClient, 51 | gitlabClient: gitlabClient, 52 | mcpHandler: mcpHandler, 53 | troubleshootCorrelator: troubleshootCorrelator, 54 | config: cfg, 55 | logger: logger, 56 | } 57 | 58 | // Initialize resource mapper 59 | server.resourceMapper = server.k8sClient.ResourceMapper 60 | 61 | // Set up routes 62 | server.setupRoutes() 63 | server.setupNamespaceRoutes() 64 | 65 | return server 66 | } 67 | 68 | // Start starts the HTTP server 69 | func (s *Server) Start(ctx context.Context) error { 70 | s.server = &http.Server{ 71 | Addr: s.config.Address, 72 | Handler: s.loggingMiddleware(s.router), 73 | ReadTimeout: time.Duration(s.config.ReadTimeout) * time.Second, 74 | WriteTimeout: time.Duration(s.config.WriteTimeout) * time.Second, 75 | } 76 | 77 | // Channel for server errors 78 | errCh := make(chan error, 1) 79 | 80 | // Start server in a goroutine 81 | go func() { 82 | s.logger.Info("Starting HTTP server", "address", s.config.Address) 83 | if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 84 | errCh <- err 85 | } 86 | }() 87 | 88 | // Wait for context cancellation or server error 89 | select { 90 | case <-ctx.Done(): 91 | s.logger.Info("Context cancelled, shutting down server") 92 | return s.Shutdown(context.Background()) 93 | case err := <-errCh: 94 | return err 95 | } 96 | } 97 | 98 | // Shutdown gracefully shuts down the server 99 | func (s *Server) Shutdown(ctx context.Context) error { 100 | s.logger.Info("Shutting down HTTP server") 101 | return s.server.Shutdown(ctx) 102 | } 103 | 104 | // Middleware functions 105 | 106 | // loggingMiddleware logs information about each request 107 | func (s *Server) loggingMiddleware(next http.Handler) http.Handler { 108 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 109 | start := time.Now() 110 | 111 | // Create a response writer that captures status code 112 | rw := &responseWriter{w, http.StatusOK} 113 | 114 | // Call the next handler 115 | next.ServeHTTP(rw, r) 116 | 117 | // Log the request 118 | s.logger.Info("HTTP request", 119 | "method", r.Method, 120 | "path", r.URL.Path, 121 | "status", rw.statusCode, 122 | "duration", time.Since(start), 123 | "remote_addr", r.RemoteAddr, 124 | "user_agent", r.UserAgent(), 125 | ) 126 | }) 127 | } 128 | 129 | // authMiddleware checks for valid authentication 130 | func (s *Server) authMiddleware(next http.Handler) http.Handler { 131 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 132 | // Get API key from header 133 | apiKey := r.Header.Get("X-API-Key") 134 | 135 | // Check for bearer token if API key is not provided 136 | if apiKey == "" { 137 | authHeader := r.Header.Get("Authorization") 138 | if authHeader == "" { 139 | s.respondWithError(w, http.StatusUnauthorized, "Authentication required", nil) 140 | return 141 | } 142 | 143 | // Extract token 144 | parts := strings.Split(authHeader, " ") 145 | if len(parts) != 2 || parts[0] != "Bearer" { 146 | s.respondWithError(w, http.StatusUnauthorized, "Invalid authorization format", nil) 147 | return 148 | } 149 | 150 | apiKey = parts[1] 151 | } 152 | 153 | // Validate the API key against the configured key 154 | if apiKey != s.config.Auth.APIKey { 155 | s.respondWithError(w, http.StatusUnauthorized, "Invalid API key", nil) 156 | return 157 | } 158 | 159 | // Call the next handler 160 | next.ServeHTTP(w, r) 161 | }) 162 | } 163 | 164 | // Custom response writer to capture status code 165 | type responseWriter struct { 166 | http.ResponseWriter 167 | statusCode int 168 | } 169 | 170 | // WriteHeader captures the status code 171 | func (rw *responseWriter) WriteHeader(code int) { 172 | rw.statusCode = code 173 | rw.ResponseWriter.WriteHeader(code) 174 | } 175 | 176 | // Initialize the resourceMapper in NewServer 177 | func (s *Server) initResourceMapper() { 178 | if s.k8sClient != nil { 179 | s.resourceMapper = k8s.NewResourceMapper(s.k8sClient) 180 | s.logger.Info("Resource mapper initialized") 181 | } else { 182 | s.logger.Warn("Cannot initialize resource mapper - K8s client is nil") 183 | } 184 | } 185 | 186 | func (s *Server) corsMiddleware(next http.Handler) http.Handler { 187 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 188 | // Set CORS headers 189 | w.Header().Set("Access-Control-Allow-Origin", "*") // Allow all origins in development 190 | w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE") 191 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") 192 | 193 | // If this is a preflight request, respond with 200 OK 194 | if r.Method == "OPTIONS" { 195 | w.WriteHeader(http.StatusOK) 196 | return 197 | } 198 | 199 | // Call the next handler 200 | next.ServeHTTP(w, r) 201 | }) 202 | } 203 | ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/gitlab/repositories.go: -------------------------------------------------------------------------------- ```go 1 | package gitlab 2 | 3 | import ( 4 | "io" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "net/url" 10 | "time" 11 | 12 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" 13 | ) 14 | 15 | // ListProjects returns a list of GitLab projects 16 | func (c *Client) ListProjects(ctx context.Context) ([]models.GitLabProject, error) { 17 | c.logger.Debug("Listing projects") 18 | 19 | // Create endpoint with query parameters 20 | endpoint := "projects" 21 | 22 | u, err := url.Parse(endpoint) 23 | if err != nil { 24 | return nil, fmt.Errorf("invalid endpoint: %w", err) 25 | } 26 | 27 | q := u.Query() 28 | q.Set("membership", "true") 29 | q.Set("order_by", "updated_at") 30 | q.Set("sort", "desc") 31 | q.Set("per_page", "100") 32 | u.RawQuery = q.Encode() 33 | 34 | resp, err := c.doRequest(ctx, http.MethodGet, u.String(), nil) 35 | if err != nil { 36 | return nil, err 37 | } 38 | defer resp.Body.Close() 39 | 40 | var projects []models.GitLabProject 41 | if err := json.NewDecoder(resp.Body).Decode(&projects); err != nil { 42 | return nil, fmt.Errorf("failed to decode response: %w", err) 43 | } 44 | 45 | c.logger.Debug("Listed projects", "count", len(projects)) 46 | return projects, nil 47 | } 48 | 49 | // GetProject returns details about a specific GitLab project 50 | func (c *Client) GetProject(ctx context.Context, projectID string) (*models.GitLabProject, error) { 51 | c.logger.Debug("Getting project", "projectID", projectID) 52 | 53 | endpoint := fmt.Sprintf("projects/%s", url.PathEscape(projectID)) 54 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 55 | if err != nil { 56 | return nil, err 57 | } 58 | defer resp.Body.Close() 59 | 60 | var project models.GitLabProject 61 | if err := json.NewDecoder(resp.Body).Decode(&project); err != nil { 62 | return nil, fmt.Errorf("failed to decode response: %w", err) 63 | } 64 | 65 | return &project, nil 66 | } 67 | 68 | // GetProjectByPath returns a project by its path (namespace/project-name) 69 | func (c *Client) GetProjectByPath(ctx context.Context, path string) (*models.GitLabProject, error) { 70 | c.logger.Debug("Getting project by path", "path", path) 71 | 72 | // GitLab API requires path to be URL encoded 73 | encodedPath := url.QueryEscape(path) 74 | endpoint := fmt.Sprintf("projects/%s", encodedPath) 75 | 76 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 77 | if err != nil { 78 | return nil, err 79 | } 80 | defer resp.Body.Close() 81 | 82 | var project models.GitLabProject 83 | if err := json.NewDecoder(resp.Body).Decode(&project); err != nil { 84 | return nil, fmt.Errorf("failed to decode response: %w", err) 85 | } 86 | 87 | return &project, nil 88 | } 89 | 90 | // GetCommit returns details about a specific commit 91 | func (c *Client) GetCommit(ctx context.Context, projectID, sha string) (*models.GitLabCommit, error) { 92 | c.logger.Debug("Getting commit", "projectID", projectID, "sha", sha) 93 | 94 | endpoint := fmt.Sprintf("projects/%s/repository/commits/%s", url.PathEscape(projectID), url.PathEscape(sha)) 95 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 96 | if err != nil { 97 | return nil, err 98 | } 99 | defer resp.Body.Close() 100 | 101 | var commit models.GitLabCommit 102 | if err := json.NewDecoder(resp.Body).Decode(&commit); err != nil { 103 | return nil, fmt.Errorf("failed to decode response: %w", err) 104 | } 105 | 106 | return &commit, nil 107 | } 108 | 109 | // GetCommitDiff returns the changes in a specific commit 110 | func (c *Client) GetCommitDiff(ctx context.Context, projectID, sha string) ([]models.GitLabDiff, error) { 111 | c.logger.Debug("Getting commit diff", "projectID", projectID, "sha", sha) 112 | 113 | endpoint := fmt.Sprintf("projects/%s/repository/commits/%s/diff", url.PathEscape(projectID), url.PathEscape(sha)) 114 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 115 | if err != nil { 116 | return nil, err 117 | } 118 | defer resp.Body.Close() 119 | 120 | var diffs []models.GitLabDiff 121 | if err := json.NewDecoder(resp.Body).Decode(&diffs); err != nil { 122 | return nil, fmt.Errorf("failed to decode response: %w", err) 123 | } 124 | 125 | c.logger.Debug("Got commit diff", "projectID", projectID, "sha", sha, "count", len(diffs)) 126 | return diffs, nil 127 | } 128 | 129 | // GetFileContent returns the content of a file at a specific commit 130 | func (c *Client) GetFileContent(ctx context.Context, projectID, filePath, ref string) (string, error) { 131 | c.logger.Debug("Getting file content", 132 | "projectID", projectID, 133 | "filePath", filePath, 134 | "ref", ref) 135 | 136 | encodedFilePath := url.PathEscape(filePath) 137 | endpoint := fmt.Sprintf("projects/%s/repository/files/%s/raw", 138 | url.PathEscape(projectID), 139 | encodedFilePath) 140 | 141 | // Add ref parameter if provided 142 | if ref != "" { 143 | u, err := url.Parse(endpoint) 144 | if err != nil { 145 | return "", fmt.Errorf("invalid endpoint: %w", err) 146 | } 147 | 148 | q := u.Query() 149 | q.Set("ref", ref) 150 | u.RawQuery = q.Encode() 151 | endpoint = u.String() 152 | } 153 | 154 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 155 | if err != nil { 156 | return "", err 157 | } 158 | defer resp.Body.Close() 159 | 160 | content, err := io.ReadAll(resp.Body) 161 | if err != nil { 162 | return "", fmt.Errorf("failed to read file content: %w", err) 163 | } 164 | 165 | return string(content), nil 166 | } 167 | 168 | // FindRecentChanges finds recent changes (commits) for a project 169 | func (c *Client) FindRecentChanges(ctx context.Context, projectID string, since time.Time) ([]models.GitLabCommit, error) { 170 | c.logger.Debug("Finding recent changes", 171 | "projectID", projectID, 172 | "since", since.Format(time.RFC3339)) 173 | 174 | // Format time as ISO 8601 175 | sinceStr := since.Format(time.RFC3339) 176 | 177 | // Create endpoint with query parameters 178 | endpoint := fmt.Sprintf("projects/%s/repository/commits", url.PathEscape(projectID)) 179 | 180 | u, err := url.Parse(endpoint) 181 | if err != nil { 182 | return nil, fmt.Errorf("invalid endpoint: %w", err) 183 | } 184 | 185 | q := u.Query() 186 | q.Set("since", sinceStr) 187 | q.Set("per_page", "20") 188 | u.RawQuery = q.Encode() 189 | 190 | resp, err := c.doRequest(ctx, http.MethodGet, u.String(), nil) 191 | if err != nil { 192 | return nil, err 193 | } 194 | defer resp.Body.Close() 195 | 196 | var commits []models.GitLabCommit 197 | if err := json.NewDecoder(resp.Body).Decode(&commits); err != nil { 198 | return nil, fmt.Errorf("failed to decode response: %w", err) 199 | } 200 | 201 | c.logger.Debug("Found recent changes", 202 | "projectID", projectID, 203 | "count", len(commits)) 204 | return commits, nil 205 | } ``` -------------------------------------------------------------------------------- /docs/src/components/Footer.astro: -------------------------------------------------------------------------------- ``` 1 | --- 2 | const currentYear = new Date().getFullYear(); 3 | --- 4 | 5 | <footer class="bg-slate-100 dark:bg-slate-900 border-t border-slate-200 dark:border-slate-800 py-12"> 6 | <div class="container mx-auto px-4"> 7 | <div class="grid grid-cols-1 md:grid-cols-4 gap-8"> 8 | <div> 9 | <h3 class="font-semibold text-lg mb-4">Kubernetes Claude MCP</h3> 10 | <p class="text-slate-600 dark:text-slate-400"> 11 | An advanced Model Context Protocol server for Kubernetes, integrating Claude AI with GitOps workflows. 12 | </p> 13 | </div> 14 | 15 | <div> 16 | <h3 class="font-semibold text-lg mb-4">Documentation</h3> 17 | <ul class="space-y-2"> 18 | <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> 19 | <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> 20 | <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> 21 | <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> 22 | </ul> 23 | </div> 24 | 25 | <div> 26 | <h3 class="font-semibold text-lg mb-4">Community</h3> 27 | <ul class="space-y-2"> 28 | <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> 29 | <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> 30 | <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> 31 | <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> 32 | </ul> 33 | </div> 34 | 35 | <div> 36 | <h3 class="font-semibold text-lg mb-4">Legal</h3> 37 | <ul class="space-y-2"> 38 | <li><a href="/license" class="text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">License</a></li> 39 | <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> 40 | <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> 41 | </ul> 42 | </div> 43 | </div> 44 | 45 | <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"> 46 | <p class="text-slate-600 dark:text-slate-400 text-sm mb-4 sm:mb-0"> 47 | © {currentYear} Blank Cut Inc. All rights reserved. 48 | </p> 49 | <div class="flex space-x-4"> 50 | <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"> 51 | <span class="sr-only">GitHub</span> 52 | <svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> 53 | <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> 54 | </svg> 55 | </a> 56 | <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"> 57 | <span class="sr-only">Twitter</span> 58 | <svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> 59 | <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> 60 | </svg> 61 | </a> 62 | <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"> 63 | <span class="sr-only">LinkedIn</span> 64 | <svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> 65 | <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> 66 | </svg> 67 | </a> 68 | </div> 69 | </div> 70 | </div> 71 | </footer> ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/models/gitlab.go: -------------------------------------------------------------------------------- ```go 1 | package models 2 | 3 | // GitLabProject represents a GitLab project 4 | type GitLabProject struct { 5 | ID int `json:"id"` 6 | Name string `json:"name"` 7 | Path string `json:"path"` 8 | PathWithNamespace string `json:"path_with_namespace"` 9 | WebURL string `json:"web_url"` 10 | DefaultBranch string `json:"default_branch"` 11 | Visibility string `json:"visibility"` 12 | } 13 | 14 | // GitLabPipeline represents a GitLab CI/CD pipeline 15 | type GitLabPipeline struct { 16 | ID int `json:"id"` 17 | Status string `json:"status"` 18 | Ref string `json:"ref"` 19 | SHA string `json:"sha"` 20 | WebURL string `json:"web_url"` 21 | CreatedAt interface{} `json:"created_at"` 22 | UpdatedAt interface{} `json:"updated_at"` 23 | } 24 | 25 | // GitLabJob represents a job in a GitLab CI/CD pipeline 26 | type GitLabJob struct { 27 | ID int `json:"id"` 28 | Status string `json:"status"` 29 | Stage string `json:"stage"` 30 | Name string `json:"name"` 31 | Ref string `json:"ref"` 32 | CreatedAt int64 `json:"created_at"` 33 | StartedAt int64 `json:"started_at"` 34 | FinishedAt int64 `json:"finished_at"` 35 | Pipeline struct { 36 | ID int `json:"id"` 37 | } `json:"pipeline"` 38 | } 39 | 40 | // GitLabCommit represents a Git commit in GitLab 41 | type GitLabCommit struct { 42 | ID string `json:"id"` 43 | ShortID string `json:"short_id"` 44 | Title string `json:"title"` 45 | Message string `json:"message"` 46 | AuthorName string `json:"author_name"` 47 | AuthorEmail string `json:"author_email"` 48 | CommitterName string `json:"committer_name"` 49 | CommitterEmail string `json:"committer_email"` 50 | CreatedAt interface{} `json:"created_at"` 51 | ParentIDs []string `json:"parent_ids"` 52 | WebURL string `json:"web_url"` 53 | } 54 | 55 | // GitLabDiff represents a file diff in a commit 56 | type GitLabDiff struct { 57 | OldPath string `json:"old_path"` 58 | NewPath string `json:"new_path"` 59 | Diff string `json:"diff"` 60 | NewFile bool `json:"new_file"` 61 | RenamedFile bool `json:"renamed_file"` 62 | DeletedFile bool `json:"deleted_file"` 63 | } 64 | 65 | // GitLabDeployment represents a deployment in GitLab 66 | type GitLabDeployment struct { 67 | ID int `json:"id"` 68 | Status string `json:"status"` 69 | CreatedAt interface{} `json:"created_at"` 70 | UpdatedAt interface{} `json:"updated_at"` 71 | Environment struct { 72 | ID int `json:"id"` 73 | Name string `json:"name"` 74 | Slug string `json:"slug"` 75 | State string `json:"state"` 76 | } `json:"environment"` 77 | Deployable struct { 78 | ID int `json:"id"` 79 | Status string `json:"status"` 80 | Stage string `json:"stage"` 81 | Name string `json:"name"` 82 | Ref string `json:"ref"` 83 | Tag bool `json:"tag"` 84 | Pipeline struct { 85 | ID int `json:"id"` 86 | Status string `json:"status"` 87 | } `json:"pipeline"` 88 | } `json:"deployable"` 89 | Commit GitLabCommit `json:"commit"` 90 | } 91 | 92 | // GitLabRelease represents a release in GitLab 93 | type GitLabRelease struct { 94 | TagName string `json:"tag_name"` 95 | Description string `json:"description"` 96 | CreatedAt int64 `json:"created_at"` 97 | Assets struct { 98 | Links []struct { 99 | Name string `json:"name"` 100 | URL string `json:"url"` 101 | } `json:"links"` 102 | } `json:"assets"` 103 | } 104 | 105 | // GitLabMergeRequest represents a merge request in GitLab 106 | type GitLabMergeRequest struct { 107 | ID int `json:"id"` 108 | IID int `json:"iid"` 109 | ProjectID int `json:"project_id"` 110 | Title string `json:"title"` 111 | Description string `json:"description"` 112 | State string `json:"state"` 113 | MergedBy *struct { 114 | ID int `json:"id"` 115 | Username string `json:"username"` 116 | Name string `json:"name"` 117 | } `json:"merged_by,omitempty"` 118 | MergedAt interface{} `json:"merged_at"` 119 | CreatedAt interface{} `json:"created_at"` 120 | UpdatedAt interface{} `json:"updated_at"` 121 | TargetBranch string `json:"target_branch"` 122 | SourceBranch string `json:"source_branch"` 123 | Author struct { 124 | ID int `json:"id"` 125 | Username string `json:"username"` 126 | Name string `json:"name"` 127 | } `json:"author"` 128 | Assignees []struct { 129 | ID int `json:"id"` 130 | Username string `json:"username"` 131 | Name string `json:"name"` 132 | } `json:"assignees"` 133 | SourceProjectID int `json:"source_project_id"` 134 | TargetProjectID int `json:"target_project_id"` 135 | WebURL string `json:"web_url"` 136 | MergeStatus string `json:"merge_status"` 137 | Changes []struct { 138 | OldPath string `json:"old_path"` 139 | NewPath string `json:"new_path"` 140 | Diff string `json:"diff"` 141 | NewFile bool `json:"new_file"` 142 | RenamedFile bool `json:"renamed_file"` 143 | DeletedFile bool `json:"deleted_file"` 144 | } `json:"changes,omitempty"` 145 | DiffRefs struct { 146 | BaseSHA string `json:"base_sha"` 147 | HeadSHA string `json:"head_sha"` 148 | StartSHA string `json:"start_sha"` 149 | } `json:"diff_refs"` 150 | UserNotesCount int `json:"user_notes_count"` 151 | HasConflicts bool `json:"has_conflicts"` 152 | Pipelines []GitLabPipeline `json:"pipelines,omitempty"` 153 | MergeRequestContext struct { 154 | CommitMessages []string `json:"commit_messages,omitempty"` 155 | AffectedFiles []string `json:"affected_files,omitempty"` 156 | HelmChartAffected bool `json:"helm_chart_affected,omitempty"` 157 | KubernetesManifest bool `json:"kubernetes_manifests_affected,omitempty"` 158 | } `json:"merge_request_context,omitempty"` 159 | } 160 | 161 | // GitLabMergeRequestComment represents a comment on a GitLab merge request 162 | type GitLabMergeRequestComment struct { 163 | ID int `json:"id"` 164 | Body string `json:"body"` 165 | CreatedAt string `json:"created_at"` 166 | UpdatedAt string `json:"updated_at"` 167 | System bool `json:"system"` 168 | NoteableID int `json:"noteable_id"` 169 | NoteableType string `json:"noteable_type"` 170 | Author struct { 171 | ID int `json:"id"` 172 | Username string `json:"username"` 173 | Name string `json:"name"` 174 | } `json:"author"` 175 | } 176 | 177 | // GitLabMergeRequestApproval represents approval information for a merge request 178 | type GitLabMergeRequestApproval struct { 179 | ID int `json:"id"` 180 | ProjectID int `json:"project_id"` 181 | ApprovalRequired bool `json:"approval_required"` 182 | ApprovedBy []struct { 183 | User struct { 184 | ID int `json:"id"` 185 | Username string `json:"username"` 186 | Name string `json:"name"` 187 | } `json:"user"` 188 | } `json:"approved_by"` 189 | ApprovalsRequired int `json:"approvals_required"` 190 | ApprovalsLeft int `json:"approvals_left"` 191 | } 192 | ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/helm/parser.go: -------------------------------------------------------------------------------- ```go 1 | // internal/helm/parser.go 2 | 3 | package helm 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "strings" 13 | 14 | "gopkg.in/yaml.v2" 15 | 16 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" 17 | ) 18 | 19 | // Parser handles Helm chart parsing and analysis 20 | type Parser struct { 21 | workDir string 22 | logger *logging.Logger 23 | } 24 | 25 | // NewParser creates a new Helm chart parser 26 | func NewParser(logger *logging.Logger) *Parser { 27 | if logger == nil { 28 | logger = logging.NewLogger().Named("helm") 29 | } 30 | 31 | // Create a temporary working directory 32 | workDir, err := os.MkdirTemp("", "helm-parser-*") 33 | if err != nil { 34 | logger.Error("Failed to create working directory", "error", err) 35 | return nil 36 | } 37 | 38 | return &Parser{ 39 | workDir: workDir, 40 | logger: logger, 41 | } 42 | } 43 | 44 | // ParseChart renders a Helm chart and returns the resulting Kubernetes manifests 45 | func (p *Parser) ParseChart(ctx context.Context, chartPath string, valuesFiles []string, values map[string]interface{}) ([]string, error) { 46 | p.logger.Debug("Parsing Helm chart", "chartPath", chartPath, "valuesFiles", valuesFiles) 47 | 48 | // Check if helm command is available 49 | if _, err := exec.LookPath("helm"); err != nil { 50 | return nil, fmt.Errorf("helm command not found in PATH: %w", err) 51 | } 52 | 53 | // Prepare helm template command 54 | args := []string{"template", "release", chartPath} 55 | 56 | // Add values files 57 | for _, valuesFile := range valuesFiles { 58 | args = append(args, "-f", valuesFile) 59 | } 60 | 61 | // Add --set arguments for values 62 | for k, v := range values { 63 | args = append(args, "--set", fmt.Sprintf("%s=%v", k, v)) 64 | } 65 | 66 | // Execute helm template command 67 | cmd := exec.CommandContext(ctx, "helm", args...) 68 | var stdout, stderr bytes.Buffer 69 | cmd.Stdout = &stdout 70 | cmd.Stderr = &stderr 71 | 72 | p.logger.Debug("Executing helm template command", "args", args) 73 | err := cmd.Run() 74 | if err != nil { 75 | return nil, fmt.Errorf("failed to execute helm template: %s, error: %w", stderr.String(), err) 76 | } 77 | 78 | // Parse the rendered templates 79 | manifests := p.splitYAMLDocuments(stdout.String()) 80 | p.logger.Debug("Parsed Helm chart", "manifestCount", len(manifests)) 81 | 82 | return manifests, nil 83 | } 84 | 85 | // WriteChartFiles writes chart files to the working directory for processing 86 | func (p *Parser) WriteChartFiles(files map[string]string) (string, error) { 87 | chartDir := filepath.Join(p.workDir, "chart") 88 | 89 | // Create chart directory if not exists 90 | if err := os.MkdirAll(chartDir, 0755); err != nil { 91 | return "", fmt.Errorf("failed to create chart directory: %w", err) 92 | } 93 | 94 | // Write files 95 | for path, content := range files { 96 | fullPath := filepath.Join(chartDir, path) 97 | dirPath := filepath.Dir(fullPath) 98 | 99 | // Create directories 100 | if err := os.MkdirAll(dirPath, 0755); err != nil { 101 | return "", fmt.Errorf("failed to create directory %s: %w", dirPath, err) 102 | } 103 | 104 | // Write file 105 | if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil { 106 | return "", fmt.Errorf("failed to write file %s: %w", fullPath, err) 107 | } 108 | } 109 | 110 | return chartDir, nil 111 | } 112 | 113 | // WriteValuesFile writes a values file to the working directory 114 | func (p *Parser) WriteValuesFile(content string) (string, error) { 115 | valuesFile := filepath.Join(p.workDir, "values.yaml") 116 | 117 | if err := os.WriteFile(valuesFile, []byte(content), 0644); err != nil { 118 | return "", fmt.Errorf("failed to write values file: %w", err) 119 | } 120 | 121 | return valuesFile, nil 122 | } 123 | 124 | // ParseYAML parses a YAML file to extract Kubernetes resources 125 | func (p *Parser) ParseYAML(content string) ([]map[string]interface{}, error) { 126 | // Split YAML documents 127 | documents := p.splitYAMLDocuments(content) 128 | 129 | var resources []map[string]interface{} 130 | 131 | for _, doc := range documents { 132 | // Parse each document as YAML 133 | var resource map[string]interface{} 134 | 135 | // *** Add this line (or similar depending on your library) *** 136 | err := yaml.Unmarshal([]byte(doc), &resource) // Use your chosen library's unmarshal function 137 | if err != nil { 138 | // Handle the error appropriately, maybe log it and continue 139 | p.logger.Warn("Failed to unmarshal YAML document", "error", err) 140 | continue 141 | } 142 | 143 | // Add to resources if it's a valid Kubernetes resource (and not empty after parsing) 144 | if resource != nil { 145 | resources = append(resources, resource) 146 | } 147 | } 148 | 149 | return resources, nil 150 | } 151 | 152 | // splitYAMLDocuments splits multi-document YAML into individual documents 153 | func (p *Parser) splitYAMLDocuments(content string) []string { 154 | // Simple implementation - in a real system, use a proper YAML parser 155 | var documents []string 156 | 157 | // Split on document separator 158 | parts := strings.Split(content, "---") 159 | 160 | for _, part := range parts { 161 | // Trim whitespace 162 | trimmed := strings.TrimSpace(part) 163 | if trimmed != "" { 164 | documents = append(documents, trimmed) 165 | } 166 | } 167 | 168 | return documents 169 | } 170 | 171 | // Cleanup removes temporary files 172 | func (p *Parser) Cleanup() { 173 | if p.workDir != "" { 174 | p.logger.Debug("Cleaning up working directory", "path", p.workDir) 175 | os.RemoveAll(p.workDir) 176 | } 177 | } 178 | 179 | // DiffChartVersions compares two versions of a chart and returns resources that would be affected 180 | func (p *Parser) DiffChartVersions(ctx context.Context, chartPath1, chartPath2 string, valuesFiles []string) ([]string, error) { 181 | // Render both chart versions 182 | manifests1, err := p.ParseChart(ctx, chartPath1, valuesFiles, nil) 183 | if err != nil { 184 | return nil, fmt.Errorf("failed to parse first chart version: %w", err) 185 | } 186 | 187 | manifests2, err := p.ParseChart(ctx, chartPath2, valuesFiles, nil) 188 | if err != nil { 189 | return nil, fmt.Errorf("failed to parse second chart version: %w", err) 190 | } 191 | 192 | // Compare manifests to find differences 193 | diff := p.compareManifests(manifests1, manifests2) 194 | 195 | return diff, nil 196 | } 197 | 198 | // compareManifests compares two sets of manifests and returns the names of resources that differ 199 | func (p *Parser) compareManifests(manifests1, manifests2 []string) []string { 200 | // This is a simplified implementation 201 | // In a real system, you would parse the YAML and compare by resource identifiers 202 | 203 | var changedResources []string 204 | 205 | // For now, we just assume all manifests might be affected 206 | // In a real implementation, you'd compare name/kind/namespace 207 | 208 | for _, manifest := range manifests2 { 209 | // Extract resource name and kind 210 | if strings.Contains(manifest, "kind:") && strings.Contains(manifest, "name:") { 211 | // Very simplistic parsing - would need proper YAML parsing in real code 212 | lines := strings.Split(manifest, "\n") 213 | var kind, name string 214 | 215 | for _, line := range lines { 216 | line = strings.TrimSpace(line) 217 | if strings.HasPrefix(line, "kind:") { 218 | kind = strings.TrimSpace(strings.TrimPrefix(line, "kind:")) 219 | } else if strings.HasPrefix(line, "name:") { 220 | name = strings.TrimSpace(strings.TrimPrefix(line, "name:")) 221 | } 222 | 223 | if kind != "" && name != "" { 224 | changedResources = append(changedResources, fmt.Sprintf("%s/%s", kind, name)) 225 | break 226 | } 227 | } 228 | } 229 | } 230 | 231 | return changedResources 232 | } 233 | ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/k8s/events.go: -------------------------------------------------------------------------------- ```go 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | "time" 9 | 10 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" 11 | 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/fields" 14 | ) 15 | 16 | // GetResourceEvents returns events related to a specific resource 17 | func (c *Client) GetResourceEvents(ctx context.Context, namespace, kind, name string) ([]models.K8sEvent, error) { 18 | c.logger.Debug("Getting events for resource", "namespace", namespace, "kind", kind, "name", name) 19 | 20 | // Build field selector 21 | var fieldSelector fields.Selector 22 | if namespace != "" { 23 | // For namespaced resources 24 | fieldSelector = fields.AndSelectors( 25 | fields.OneTermEqualSelector("involvedObject.name", name), 26 | fields.OneTermEqualSelector("involvedObject.kind", kind), 27 | fields.OneTermEqualSelector("involvedObject.namespace", namespace), 28 | ) 29 | } else { 30 | // For cluster-scoped resources (no namespace) 31 | fieldSelector = fields.AndSelectors( 32 | fields.OneTermEqualSelector("involvedObject.name", name), 33 | fields.OneTermEqualSelector("involvedObject.kind", kind), 34 | ) 35 | } 36 | 37 | // Get events 38 | eventList, err := c.clientset.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{ 39 | FieldSelector: fieldSelector.String(), 40 | }) 41 | if err != nil { 42 | return nil, fmt.Errorf("failed to list events: %w", err) 43 | } 44 | 45 | // Convert to our model 46 | var events []models.K8sEvent 47 | for _, event := range eventList.Items { 48 | e := models.K8sEvent{ 49 | Reason: event.Reason, 50 | Message: event.Message, 51 | Type: event.Type, 52 | Count: int(event.Count), 53 | FirstTime: event.FirstTimestamp.Time, 54 | LastTime: event.LastTimestamp.Time, 55 | Object: struct { 56 | Kind string `json:"kind"` 57 | Name string `json:"name"` 58 | Namespace string `json:"namespace"` 59 | }{ 60 | Kind: event.InvolvedObject.Kind, 61 | Name: event.InvolvedObject.Name, 62 | Namespace: event.InvolvedObject.Namespace, 63 | }, 64 | } 65 | events = append(events, e) 66 | } 67 | 68 | // Sort events by last time, most recent first 69 | sort.Slice(events, func(i, j int) bool { 70 | return events[i].LastTime.After(events[j].LastTime) 71 | }) 72 | 73 | c.logger.Debug("Got events for resource", 74 | "namespace", namespace, 75 | "kind", kind, 76 | "name", name, 77 | "count", len(events)) 78 | return events, nil 79 | } 80 | 81 | // GetNamespaceEvents returns all events in a namespace 82 | func (c *Client) GetNamespaceEvents(ctx context.Context, namespace string) ([]models.K8sEvent, error) { 83 | c.logger.Debug("Getting events for namespace", "namespace", namespace) 84 | 85 | // Get events 86 | eventList, err := c.clientset.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{}) 87 | if err != nil { 88 | return nil, fmt.Errorf("failed to list events: %w", err) 89 | } 90 | 91 | // Convert to our model 92 | var events []models.K8sEvent 93 | for _, event := range eventList.Items { 94 | e := models.K8sEvent{ 95 | Reason: event.Reason, 96 | Message: event.Message, 97 | Type: event.Type, 98 | Count: int(event.Count), 99 | FirstTime: event.FirstTimestamp.Time, 100 | LastTime: event.LastTimestamp.Time, 101 | Object: struct { 102 | Kind string `json:"kind"` 103 | Name string `json:"name"` 104 | Namespace string `json:"namespace"` 105 | }{ 106 | Kind: event.InvolvedObject.Kind, 107 | Name: event.InvolvedObject.Name, 108 | Namespace: event.InvolvedObject.Namespace, 109 | }, 110 | } 111 | events = append(events, e) 112 | } 113 | 114 | // Sort events by last time, most recent first 115 | sort.Slice(events, func(i, j int) bool { 116 | return events[i].LastTime.After(events[j].LastTime) 117 | }) 118 | 119 | c.logger.Debug("Got events for namespace", "namespace", namespace, "count", len(events)) 120 | return events, nil 121 | } 122 | 123 | // GetRecentWarningEvents returns recent warning events across all namespaces 124 | func (c *Client) GetRecentWarningEvents(ctx context.Context, timeWindow time.Duration) ([]models.K8sEvent, error) { 125 | c.logger.Debug("Getting recent warning events", "timeWindow", timeWindow) 126 | 127 | // Calculate the cutoff time 128 | cutoffTime := time.Now().Add(-timeWindow) 129 | 130 | // Get events from all namespaces 131 | eventList, err := c.clientset.CoreV1().Events("").List(ctx, metav1.ListOptions{ 132 | FieldSelector: fields.OneTermEqualSelector("type", "Warning").String(), 133 | }) 134 | if err != nil { 135 | return nil, fmt.Errorf("failed to list warning events: %w", err) 136 | } 137 | 138 | // Filter and convert to our model 139 | var events []models.K8sEvent 140 | for _, event := range eventList.Items { 141 | // Skip events older than the cutoff time 142 | if event.LastTimestamp.Time.Before(cutoffTime) { 143 | continue 144 | } 145 | 146 | e := models.K8sEvent{ 147 | Reason: event.Reason, 148 | Message: event.Message, 149 | Type: event.Type, 150 | Count: int(event.Count), 151 | FirstTime: event.FirstTimestamp.Time, 152 | LastTime: event.LastTimestamp.Time, 153 | Object: struct { 154 | Kind string `json:"kind"` 155 | Name string `json:"name"` 156 | Namespace string `json:"namespace"` 157 | }{ 158 | Kind: event.InvolvedObject.Kind, 159 | Name: event.InvolvedObject.Name, 160 | Namespace: event.InvolvedObject.Namespace, 161 | }, 162 | } 163 | events = append(events, e) 164 | } 165 | 166 | // Sort events by last time, most recent first 167 | sort.Slice(events, func(i, j int) bool { 168 | return events[i].LastTime.After(events[j].LastTime) 169 | }) 170 | 171 | c.logger.Debug("Got recent warning events", "count", len(events), "timeWindow", timeWindow) 172 | return events, nil 173 | } 174 | 175 | // GetClusterHealthEvents returns events that might indicate cluster health issues 176 | func (c *Client) GetClusterHealthEvents(ctx context.Context) ([]models.K8sEvent, error) { 177 | c.logger.Debug("Getting cluster health events") 178 | 179 | // Define keywords that might indicate cluster health issues 180 | healthIssueKeywords := []string{ 181 | "Failed", "Error", "CrashLoopBackOff", "OOMKilled", "Evicted", 182 | "NodeNotReady", "Unhealthy", "OutOfDisk", "MemoryPressure", "DiskPressure", 183 | "NetworkUnavailable", "Unschedulable", 184 | } 185 | 186 | // Build field selector for warning events 187 | fieldSelector := fields.OneTermEqualSelector("type", "Warning") 188 | 189 | // Get events from all namespaces 190 | eventList, err := c.clientset.CoreV1().Events("").List(ctx, metav1.ListOptions{ 191 | FieldSelector: fieldSelector.String(), 192 | }) 193 | if err != nil { 194 | return nil, fmt.Errorf("failed to list warning events: %w", err) 195 | } 196 | 197 | // Filter and convert to our model 198 | var events []models.K8sEvent 199 | for _, event := range eventList.Items { 200 | // Check if the event matches any health issue keywords 201 | matchesKeyword := false 202 | for _, keyword := range healthIssueKeywords { 203 | if strings.Contains(event.Reason, keyword) || strings.Contains(event.Message, keyword) { 204 | matchesKeyword = true 205 | break 206 | } 207 | } 208 | 209 | if !matchesKeyword { 210 | continue 211 | } 212 | 213 | e := models.K8sEvent{ 214 | Reason: event.Reason, 215 | Message: event.Message, 216 | Type: event.Type, 217 | Count: int(event.Count), 218 | FirstTime: event.FirstTimestamp.Time, 219 | LastTime: event.LastTimestamp.Time, 220 | Object: struct { 221 | Kind string `json:"kind"` 222 | Name string `json:"name"` 223 | Namespace string `json:"namespace"` 224 | }{ 225 | Kind: event.InvolvedObject.Kind, 226 | Name: event.InvolvedObject.Name, 227 | Namespace: event.InvolvedObject.Namespace, 228 | }, 229 | } 230 | events = append(events, e) 231 | } 232 | 233 | // Sort events by last time, most recent first 234 | sort.Slice(events, func(i, j int) bool { 235 | return events[i].LastTime.After(events[j].LastTime) 236 | }) 237 | 238 | c.logger.Debug("Got cluster health events", "count", len(events)) 239 | return events, nil 240 | } 241 | ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/k8s/enhanced_client.go: -------------------------------------------------------------------------------- ```go 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | // NamespaceResourcesCollection contains all resources in a namespace 15 | type NamespaceResourcesCollection struct { 16 | Namespace string `json:"namespace"` 17 | Resources map[string][]unstructured.Unstructured `json:"resources"` 18 | Stats map[string]int `json:"stats"` 19 | } 20 | 21 | // ResourceDetails contains detailed information about a resource 22 | type ResourceDetails struct { 23 | Resource *unstructured.Unstructured `json:"resource"` 24 | Events []interface{} `json:"events"` 25 | Relationships []ResourceRelationship `json:"relationships"` 26 | Metrics map[string]interface{} `json:"metrics"` 27 | } 28 | 29 | // GetAllNamespaceResources retrieves all resources in a namespace 30 | func (c *Client) GetAllNamespaceResources(ctx context.Context, namespace string) (*NamespaceResourcesCollection, error) { 31 | c.logger.Info("Getting all resources in namespace", "namespace", namespace) 32 | 33 | collection := &NamespaceResourcesCollection{ 34 | Namespace: namespace, 35 | Resources: make(map[string][]unstructured.Unstructured), 36 | Stats: make(map[string]int), 37 | } 38 | 39 | // Discover all available resource types 40 | resources, err := c.discoveryClient.ServerPreferredResources() 41 | if err != nil { 42 | return nil, fmt.Errorf("failed to get server resources: %w", err) 43 | } 44 | 45 | // Use a wait group to parallelize resource collection 46 | var wg sync.WaitGroup 47 | var mu sync.Mutex // Mutex for safely updating the collection 48 | 49 | // Collect resources for each API group concurrently 50 | for _, resourceList := range resources { 51 | wg.Add(1) 52 | 53 | go func(resourceList *metav1.APIResourceList) { 54 | defer wg.Done() 55 | 56 | gv, err := schema.ParseGroupVersion(resourceList.GroupVersion) 57 | if err != nil { 58 | c.logger.Warn("Failed to parse group version", "groupVersion", resourceList.GroupVersion) 59 | return 60 | } 61 | 62 | for _, r := range resourceList.APIResources { 63 | // Skip resources that can't be listed or aren't namespaced 64 | if !strings.Contains(r.Verbs.String(), "list") || !r.Namespaced { 65 | continue 66 | } 67 | 68 | // Skip subresources (contains slash) 69 | if strings.Contains(r.Name, "/") { 70 | continue 71 | } 72 | 73 | // Build GVR for this resource type 74 | gvr := schema.GroupVersionResource{ 75 | Group: gv.Group, 76 | Version: gv.Version, 77 | Resource: r.Name, 78 | } 79 | 80 | // List resources of this type 81 | list, err := c.dynamicClient.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{}) 82 | if err != nil { 83 | c.logger.Warn("Failed to list resources", 84 | "namespace", namespace, 85 | "resource", r.Name, 86 | "error", err) 87 | continue 88 | } 89 | 90 | // Skip if no resources found 91 | if len(list.Items) == 0 { 92 | continue 93 | } 94 | 95 | // Add to collection with thread safety 96 | mu.Lock() 97 | collection.Resources[r.Kind] = list.Items 98 | collection.Stats[r.Kind] = len(list.Items) 99 | mu.Unlock() 100 | } 101 | }(resourceList) 102 | } 103 | 104 | // Wait for all resource collections to complete 105 | wg.Wait() 106 | 107 | c.logger.Info("Collected all namespace resources", 108 | "namespace", namespace, 109 | "resourceTypes", len(collection.Resources), 110 | "totalResources", c.countTotalResources(collection.Stats)) 111 | 112 | return collection, nil 113 | } 114 | 115 | // countTotalResources counts the total number of resources across all types 116 | func (c *Client) countTotalResources(stats map[string]int) int { 117 | total := 0 118 | for _, count := range stats { 119 | total += count 120 | } 121 | return total 122 | } 123 | 124 | // GetResourceDetails gets detailed information about a specific resource 125 | func (c *Client) GetResourceDetails(ctx context.Context, kind, namespace, name string) (*ResourceDetails, error) { 126 | c.logger.Info("Getting resource details", "kind", kind, "namespace", namespace, "name", name) 127 | 128 | // Get the resource 129 | resource, err := c.GetResource(ctx, kind, namespace, name) 130 | if err != nil { 131 | return nil, fmt.Errorf("failed to get resource: %w", err) 132 | } 133 | 134 | // Initialize resource details 135 | details := &ResourceDetails{ 136 | Resource: resource, 137 | Metrics: make(map[string]interface{}), 138 | } 139 | 140 | // Get resource events 141 | events, err := c.GetResourceEvents(ctx, namespace, kind, name) 142 | if err != nil { 143 | c.logger.Warn("Failed to get resource events", "error", err) 144 | } else { 145 | // Convert events to interface for JSON serialization 146 | eventsInterface := make([]interface{}, len(events)) 147 | for i, event := range events { 148 | eventMap := map[string]interface{}{ 149 | "reason": event.Reason, 150 | "message": event.Message, 151 | "type": event.Type, 152 | "count": event.Count, 153 | "firstTime": event.FirstTime, 154 | "lastTime": event.LastTime, 155 | "object": map[string]string{ 156 | "kind": event.Object.Kind, 157 | "name": event.Object.Name, 158 | "namespace": event.Object.Namespace, 159 | }, 160 | } 161 | eventsInterface[i] = eventMap 162 | } 163 | details.Events = eventsInterface 164 | } 165 | 166 | // Add resource-specific metrics 167 | c.addResourceMetrics(ctx, resource, details) 168 | 169 | return details, nil 170 | } 171 | 172 | // addResourceMetrics adds resource-specific metrics based on resource type 173 | func (c *Client) addResourceMetrics(ctx context.Context, resource *unstructured.Unstructured, details *ResourceDetails) { 174 | kind := resource.GetKind() 175 | 176 | switch kind { 177 | case "Pod": 178 | // Add container statuses 179 | containers, found, _ := unstructured.NestedSlice(resource.Object, "spec", "containers") 180 | if found { 181 | details.Metrics["containerCount"] = len(containers) 182 | } 183 | 184 | // Add status phase 185 | phase, found, _ := unstructured.NestedString(resource.Object, "status", "phase") 186 | if found { 187 | details.Metrics["phase"] = phase 188 | } 189 | 190 | // Add restart counts 191 | containerStatuses, found, _ := unstructured.NestedSlice(resource.Object, "status", "containerStatuses") 192 | if found { 193 | totalRestarts := 0 194 | for _, cs := range containerStatuses { 195 | containerStatus, ok := cs.(map[string]interface{}) 196 | if !ok { 197 | continue 198 | } 199 | 200 | restarts, found, _ := unstructured.NestedInt64(containerStatus, "restartCount") 201 | if found { 202 | totalRestarts += int(restarts) 203 | } 204 | } 205 | details.Metrics["totalRestarts"] = totalRestarts 206 | } 207 | 208 | case "Deployment", "StatefulSet", "DaemonSet", "ReplicaSet": 209 | // Add replica counts 210 | replicas, found, _ := unstructured.NestedInt64(resource.Object, "spec", "replicas") 211 | if found { 212 | details.Metrics["desiredReplicas"] = replicas 213 | } 214 | 215 | availableReplicas, found, _ := unstructured.NestedInt64(resource.Object, "status", "availableReplicas") 216 | if found { 217 | details.Metrics["availableReplicas"] = availableReplicas 218 | } 219 | 220 | readyReplicas, found, _ := unstructured.NestedInt64(resource.Object, "status", "readyReplicas") 221 | if found { 222 | details.Metrics["readyReplicas"] = readyReplicas 223 | } 224 | 225 | if kind == "Deployment" { 226 | // Add deployment strategy 227 | strategy, found, _ := unstructured.NestedString(resource.Object, "spec", "strategy", "type") 228 | if found { 229 | details.Metrics["strategy"] = strategy 230 | } 231 | } 232 | 233 | case "Service": 234 | // Add service type 235 | serviceType, found, _ := unstructured.NestedString(resource.Object, "spec", "type") 236 | if found { 237 | details.Metrics["type"] = serviceType 238 | } 239 | 240 | // Add port count 241 | ports, found, _ := unstructured.NestedSlice(resource.Object, "spec", "ports") 242 | if found { 243 | details.Metrics["portCount"] = len(ports) 244 | } 245 | 246 | case "PersistentVolumeClaim": 247 | // Add storage capacity 248 | capacity, found, _ := unstructured.NestedString(resource.Object, "spec", "resources", "requests", "storage") 249 | if found { 250 | details.Metrics["requestedStorage"] = capacity 251 | } 252 | 253 | // Add access modes 254 | accessModes, found, _ := unstructured.NestedStringSlice(resource.Object, "spec", "accessModes") 255 | if found { 256 | details.Metrics["accessModes"] = accessModes 257 | } 258 | 259 | // Add phase 260 | phase, found, _ := unstructured.NestedString(resource.Object, "status", "phase") 261 | if found { 262 | details.Metrics["phase"] = phase 263 | } 264 | } 265 | } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/correlator/helm_correlator.go: -------------------------------------------------------------------------------- ```go 1 | // internal/correlator/helm_correlator.go 2 | 3 | package correlator 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/gitlab" 12 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/helm" 13 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" 14 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" 15 | ) 16 | 17 | // HelmCorrelator correlates Helm charts with Kubernetes resources 18 | type HelmCorrelator struct { 19 | gitlabClient *gitlab.Client 20 | helmParser *helm.Parser 21 | logger *logging.Logger 22 | } 23 | 24 | // NewHelmCorrelator creates a new Helm correlator 25 | func NewHelmCorrelator(gitlabClient *gitlab.Client, logger *logging.Logger) *HelmCorrelator { 26 | if logger == nil { 27 | logger = logging.NewLogger().Named("helm-correlator") 28 | } 29 | 30 | return &HelmCorrelator{ 31 | gitlabClient: gitlabClient, 32 | helmParser: helm.NewParser(logger.Named("helm")), 33 | logger: logger, 34 | } 35 | } 36 | 37 | // AnalyzeCommitHelmChanges analyzes Helm changes in a commit 38 | func (c *HelmCorrelator) AnalyzeCommitHelmChanges(ctx context.Context, projectID string, commitSHA string) ([]string, error) { 39 | c.logger.Debug("Analyzing Helm changes in commit", "projectID", projectID, "commitSHA", commitSHA) 40 | 41 | // Get commit diff 42 | diffs, err := c.gitlabClient.GetCommitDiff(ctx, projectID, commitSHA) 43 | if err != nil { 44 | return nil, fmt.Errorf("failed to get commit diff: %w", err) 45 | } 46 | 47 | // Identify Helm chart changes 48 | helmCharts := c.identifyHelmCharts(diffs) 49 | if len(helmCharts) == 0 { 50 | c.logger.Debug("No Helm chart changes found in commit") 51 | return nil, nil 52 | } 53 | 54 | // Analyze each chart 55 | var affectedResources []string 56 | 57 | for chartPath, files := range helmCharts { 58 | resources, err := c.analyzeHelmChart(ctx, projectID, commitSHA, chartPath, files) 59 | if err != nil { 60 | c.logger.Warn("Failed to analyze Helm chart", "chartPath", chartPath, "error", err) 61 | continue 62 | } 63 | 64 | affectedResources = append(affectedResources, resources...) 65 | } 66 | 67 | return affectedResources, nil 68 | } 69 | 70 | // AnalyzeMergeRequestHelmChanges analyzes Helm changes in a merge request 71 | func (c *HelmCorrelator) AnalyzeMergeRequestHelmChanges(ctx context.Context, projectID string, mergeRequestIID int) ([]string, error) { 72 | c.logger.Debug("Analyzing Helm changes in merge request", "projectID", projectID, "mergeRequestIID", mergeRequestIID) 73 | 74 | // Get merge request changes 75 | mrChanges, err := c.gitlabClient.GetMergeRequestChanges(ctx, projectID, mergeRequestIID) 76 | if err != nil { 77 | return nil, fmt.Errorf("failed to get merge request changes: %w", err) 78 | } 79 | 80 | // Identify Helm chart changes 81 | var gitlabDiffs []models.GitLabDiff 82 | for _, change := range mrChanges.Changes { 83 | diff := models.GitLabDiff{ 84 | OldPath: change.OldPath, 85 | NewPath: change.NewPath, 86 | Diff: change.Diff, 87 | NewFile: change.NewFile, 88 | RenamedFile: change.RenamedFile, 89 | DeletedFile: change.DeletedFile, 90 | } 91 | gitlabDiffs = append(gitlabDiffs, diff) 92 | } 93 | helmCharts := c.identifyHelmCharts(gitlabDiffs) 94 | if len(helmCharts) == 0 { 95 | c.logger.Debug("No Helm chart changes found in merge request") 96 | return nil, nil 97 | } 98 | 99 | // Get commits in the merge request 100 | commits, err := c.gitlabClient.GetMergeRequestCommits(ctx, projectID, mergeRequestIID) 101 | if err != nil { 102 | return nil, fmt.Errorf("failed to get merge request commits: %w", err) 103 | } 104 | 105 | // Use the latest commit SHA for analysis 106 | var latestCommitSHA string 107 | if len(commits) > 0 { 108 | latestCommitSHA = commits[0].ID 109 | } else { 110 | latestCommitSHA = mrChanges.DiffRefs.HeadSHA 111 | } 112 | 113 | // Analyze each chart 114 | var affectedResources []string 115 | 116 | for chartPath, files := range helmCharts { 117 | resources, err := c.analyzeHelmChart(ctx, projectID, latestCommitSHA, chartPath, files) 118 | if err != nil { 119 | c.logger.Warn("Failed to analyze Helm chart", "chartPath", chartPath, "error", err) 120 | continue 121 | } 122 | 123 | affectedResources = append(affectedResources, resources...) 124 | } 125 | 126 | return affectedResources, nil 127 | } 128 | 129 | // identifyHelmCharts identifies Helm charts in changed files 130 | func (c *HelmCorrelator) identifyHelmCharts(diffs []models.GitLabDiff) map[string][]string { 131 | helmCharts := make(map[string][]string) 132 | 133 | for _, diff := range diffs { 134 | path := diff.NewPath 135 | 136 | // Skip deleted files 137 | if diff.DeletedFile { 138 | continue 139 | } 140 | 141 | // Check if it's a Helm-related file 142 | if strings.Contains(path, "Chart.yaml") || 143 | strings.Contains(path, "values.yaml") || 144 | (strings.Contains(path, "templates/") && strings.HasSuffix(path, ".yaml")) { 145 | 146 | // Extract chart path (parent directory of Chart.yaml or parent's parent for templates) 147 | chartPath := filepath.Dir(path) 148 | if strings.Contains(path, "templates/") { 149 | chartPath = filepath.Dir(filepath.Dir(path)) 150 | } 151 | 152 | // Add to chart files 153 | if _, exists := helmCharts[chartPath]; !exists { 154 | helmCharts[chartPath] = []string{} 155 | } 156 | 157 | helmCharts[chartPath] = append(helmCharts[chartPath], path) 158 | } 159 | } 160 | 161 | return helmCharts 162 | } 163 | 164 | // analyzeHelmChart analyzes changes in a Helm chart 165 | func (c *HelmCorrelator) analyzeHelmChart(ctx context.Context, projectID, commitSHA, chartPath string, changedFiles []string) ([]string, error) { 166 | c.logger.Debug("Analyzing Helm chart", "chartPath", chartPath, "changedFiles", changedFiles) 167 | 168 | // Determine chart structure 169 | chartFiles := make(map[string]string) 170 | 171 | // Get Chart.yaml 172 | chartYaml, err := c.gitlabClient.GetFileContent(ctx, projectID, fmt.Sprintf("%s/Chart.yaml", chartPath), commitSHA) 173 | if err != nil { 174 | c.logger.Warn("Failed to get Chart.yaml", "error", err) 175 | // Try to continue without Chart.yaml 176 | } else { 177 | chartFiles["Chart.yaml"] = chartYaml 178 | } 179 | 180 | // Get values.yaml 181 | valuesYaml, err := c.gitlabClient.GetFileContent(ctx, projectID, fmt.Sprintf("%s/values.yaml", chartPath), commitSHA) 182 | 183 | if err != nil { 184 | c.logger.Warn("Failed to get values.yaml", "error", err) 185 | // Try to continue without values.yaml 186 | } else { 187 | chartFiles["values.yaml"] = valuesYaml 188 | } 189 | 190 | // Get template files 191 | for _, file := range changedFiles { 192 | if strings.Contains(file, "templates/") { 193 | content, err := c.gitlabClient.GetFileContent(ctx, projectID, file, commitSHA) 194 | if err != nil { 195 | c.logger.Warn("Failed to get template file", "file", file, "error", err) 196 | continue 197 | } 198 | 199 | // Store template file relative to chart path 200 | relPath := strings.TrimPrefix(file, chartPath+"/") 201 | chartFiles[relPath] = content 202 | } 203 | } 204 | 205 | // Write chart files to disk for processing 206 | chartDir, err := c.helmParser.WriteChartFiles(chartFiles) 207 | if err != nil { 208 | return nil, fmt.Errorf("failed to write chart files: %w", err) 209 | } 210 | 211 | // Parse chart to get manifests 212 | manifests, err := c.helmParser.ParseChart(ctx, chartDir, nil, nil) 213 | if err != nil { 214 | return nil, fmt.Errorf("failed to parse chart: %w", err) 215 | } 216 | 217 | // Extract resources from manifests 218 | var resources []string 219 | for _, manifest := range manifests { 220 | // Extract resource information 221 | kind, name, namespace := c.extractResourceInfo(manifest) 222 | if kind != "" && name != "" { 223 | resource := fmt.Sprintf("%s/%s", kind, name) 224 | if namespace != "" { 225 | resource = fmt.Sprintf("%s/%s/%s", namespace, kind, name) 226 | } 227 | resources = append(resources, resource) 228 | } 229 | } 230 | 231 | c.logger.Debug("Analyzed Helm chart", "chartPath", chartPath, "resourceCount", len(resources)) 232 | return resources, nil 233 | } 234 | 235 | // extractResourceInfo extracts kind, name, and namespace from a YAML manifest 236 | func (c *HelmCorrelator) extractResourceInfo(manifest string) (kind, name, namespace string) { 237 | // Simple parsing - in a real implementation, use proper YAML parsing 238 | lines := strings.Split(manifest, "\n") 239 | 240 | for _, line := range lines { 241 | line = strings.TrimSpace(line) 242 | 243 | if strings.HasPrefix(line, "kind:") { 244 | kind = strings.TrimSpace(strings.TrimPrefix(line, "kind:")) 245 | } else if strings.HasPrefix(line, "name:") { 246 | name = strings.TrimSpace(strings.TrimPrefix(line, "name:")) 247 | } else if strings.HasPrefix(line, "namespace:") { 248 | namespace = strings.TrimSpace(strings.TrimPrefix(line, "namespace:")) 249 | } 250 | } 251 | 252 | return kind, name, namespace 253 | } 254 | 255 | // Cleanup cleans up temporary resources 256 | func (c *HelmCorrelator) Cleanup() { 257 | if c.helmParser != nil { 258 | c.helmParser.Cleanup() 259 | } 260 | } 261 | ``` -------------------------------------------------------------------------------- /docs/src/content/docs/model-context-protocol.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Model Context Protocol 3 | description: Learn about the Model Context Protocol (MCP) and how it powers AI-driven analysis of Kubernetes and GitOps workflows. 4 | date: 2025-03-01 5 | order: 7 6 | tags: ['concepts', 'architecture'] 7 | --- 8 | 9 | # Model Context Protocol 10 | 11 | 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. 12 | 13 | ## What is MCP? 14 | 15 | 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. 16 | 17 | In the context of Kubernetes and GitOps: 18 | 19 | - **Complete Context**: MCP gathers comprehensive information about resources, their relationships, history, and current state. 20 | - **Cross-System Correlation**: It correlates information from Kubernetes, ArgoCD, GitLab, and other systems. 21 | - **Intelligent Filtering**: It filters and prioritizes information to focus on what's most relevant. 22 | - **Structured Formatting**: It presents information in a way that maximizes Claude's understanding. 23 | 24 | ## Core Components of MCP 25 | 26 | The MCP framework consists of several key components: 27 | 28 | ### 1. Context Collection 29 | 30 | The first step of MCP is gathering comprehensive information about the system being analyzed. For Kubernetes environments, this includes: 31 | 32 | - **Resource Definitions**: The complete YAML/JSON specifications of resources 33 | - **Resource Status**: Current runtime status information 34 | - **Events**: Related Kubernetes events 35 | - **Logs**: Container logs for relevant pods 36 | - **Relationships**: Parent-child relationships between resources 37 | - **History**: Deployment history, changes, and previous states 38 | - **GitOps Context**: ArgoCD sync status, GitLab commits, pipelines 39 | 40 | ### 2. Context Correlation 41 | 42 | Once data is collected, MCP correlates information across different systems: 43 | 44 | - **Resource to Git**: Which Git repository, branch, and files define a resource 45 | - **Resource to CI/CD**: Which pipelines deployed a resource 46 | - **Resource to Owners**: Which teams or individuals own a resource 47 | - **Dependencies**: How resources depend on each other 48 | - **Change Impact**: How changes in one system affect others 49 | 50 | ### 3. Context Formatting 51 | 52 | MCP formats the correlated information in a standardized structure: 53 | 54 | - **Hierarchical Organization**: Information is organized in a logical hierarchy 55 | - **Relevance Sorting**: Most important information is presented first 56 | - **Cross-References**: Clear references between related pieces of information 57 | - **Compact Representation**: Information is presented efficiently to maximize context window usage 58 | 59 | ### 4. Context Presentation 60 | 61 | Finally, MCP presents the formatted context to Claude for analysis: 62 | 63 | - **System Prompt**: Instructs Claude on how to interpret the context 64 | - **User Query**: Focuses Claude's analysis on specific questions or issues 65 | - **Analysis Parameters**: Controls the depth, breadth, and style of analysis 66 | 67 | ## MCP in Action 68 | 69 | Here's a simplified view of how MCP works when troubleshooting a Kubernetes deployment: 70 | 71 | 1. **User Query**: "Why is my deployment not scaling?" 72 | 2. **Context Collection**: MCP gathers information about the deployment, related pods, events, logs, node resources, and GitOps configurations. 73 | 3. **Context Correlation**: MCP connects the deployment to its ArgoCD application and recent GitLab commits. 74 | 4. **Context Formatting**: The information is structured in a hierarchical format that prioritizes scaling-related details. 75 | 5. **Claude Analysis**: Claude analyzes the context and identifies that the deployment can't scale because of resource constraints. 76 | 6. **Response**: The user receives a detailed explanation and recommendations. 77 | 78 | ## Protocol Architecture 79 | 80 | The MCP implementation consists of several key components: 81 | 82 | ### 1. Collectors 83 | 84 | Collectors are responsible for gathering information from different sources: 85 | 86 | - **Kubernetes Collector**: Gathers resource definitions, status, and events 87 | - **ArgoCD Collector**: Gathers application definitions, sync status, and history 88 | - **GitLab Collector**: Gathers repository information, commits, and pipelines 89 | - **Log Collector**: Gathers container logs and application logs 90 | 91 | ### 2. Correlators 92 | 93 | Correlators connect information across different systems: 94 | 95 | - **GitOps Correlator**: Connects Kubernetes resources to their Git definitions 96 | - **Deployment Correlator**: Connects resources to their deployment pipelines 97 | - **Issue Correlator**: Connects observed issues to their potential causes 98 | - **Resource Correlator**: Connects resources to their related resources 99 | 100 | ### 3. Context Manager 101 | 102 | The Context Manager is responsible for organizing and formatting the context: 103 | 104 | - **Context Selection**: Determines what information to include 105 | - **Context Prioritization**: Prioritizes the most relevant information 106 | - **Context Formatting**: Formats the information for maximum effectiveness 107 | - **Context Truncation**: Ensures the context fits within Claude's context window 108 | 109 | ### 4. Protocol Handler 110 | 111 | The Protocol Handler handles the interaction with Claude: 112 | 113 | - **Prompt Generation**: Creates effective system and user prompts 114 | - **Response Processing**: Processes and formats Claude's responses 115 | - **Follow-up Management**: Handles follow-up queries and clarifications 116 | 117 | ## Example MCP Context 118 | 119 | Here's a simplified example of how MCP formats context for Claude: 120 | 121 | ``` 122 | # Kubernetes Resource: Deployment/my-app 123 | Namespace: default 124 | API Version: apps/v1 125 | 126 | ## Specification 127 | Replicas: 5 128 | Strategy: RollingUpdate 129 | Selector: app=my-app 130 | Template: 131 | ...truncated for brevity... 132 | 133 | ## Status 134 | Available Replicas: 3 135 | Ready Replicas: 3 136 | Updated Replicas: 3 137 | Conditions: 138 | - Type: Available, Status: True 139 | - Type: Progressing, Status: True 140 | 141 | ## Recent Events 142 | 1. [Warning] FailedCreate: pods "my-app-7b9d7f8d9-" failed to fit in any node 143 | 2. [Normal] ScalingReplicaSet: Scaled up replica set my-app-7b9d7f8d9 to 5 144 | 3. [Warning] FailedScheduling: 0/3 nodes are available: insufficient cpu 145 | 146 | ## ArgoCD Application 147 | Name: my-app 148 | Sync Status: Synced 149 | Health Status: Degraded 150 | Source: https://github.com/myorg/myrepo.git 151 | Path: applications/my-app 152 | Target Revision: main 153 | 154 | ## Recent GitLab Commits 155 | 1. [2025-03-01T10:15:30Z] 7a8b9c0d: Increase replicas from 3 to 5 (John Smith) 156 | 2. [2025-02-28T15:45:20Z] 1b2c3d4e: Update resource requests (Jane Doe) 157 | 158 | ## Node Resources 159 | Total CPU Capacity: 12 cores 160 | Used CPU: 10.5 cores 161 | Available CPU: 1.5 cores 162 | ``` 163 | 164 | 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. 165 | 166 | ## Benefits of MCP 167 | 168 | Using the Model Context Protocol provides several key benefits: 169 | 170 | 1. **Complete Understanding**: Claude gets a holistic view of your environment. 171 | 2. **Deeper Analysis**: With more context, Claude can provide more accurate and insightful analysis. 172 | 3. **Cross-System Correlation**: Issues that span multiple systems are easier to identify. 173 | 4. **Efficient Context Usage**: Structured information maximizes the use of Claude's context window. 174 | 5. **Consistent Analysis**: Standardized context leads to more consistent analysis over time. 175 | 176 | ## Extending MCP 177 | 178 | The Model Context Protocol is designed to be extensible. You can add support for additional systems and information sources: 179 | 180 | 1. **Custom Collectors**: Implement collectors for your specific systems. 181 | 2. **Custom Correlators**: Create correlators for your organization's workflows. 182 | 3. **Context Templates**: Define custom context templates for your use cases. 183 | 4. **Prompt Templates**: Customize prompts for your specific needs. 184 | 185 | For more information on extending MCP, see the [Custom Integrations](/docs/custom-integrations) guide. 186 | 187 | ## Next Steps 188 | 189 | Now that you understand the Model Context Protocol, you can: 190 | 191 | 1. [Explore GitOps Integration](/docs/gitops-integration) to learn how MCP connects with ArgoCD and GitLab. 192 | 2. [Try Troubleshooting Resources](/docs/troubleshooting-resources) to see MCP in action. 193 | 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 1 | package k8s 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "context" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" 11 | 12 | corev1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/api/errors" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 16 | "k8s.io/apimachinery/pkg/runtime/schema" 17 | ) 18 | 19 | // resourceMappings maps common resource types to their API versions and kinds 20 | var resourceMappings = map[string]schema.GroupVersionResource{ 21 | "pod": {Group: "", Version: "v1", Resource: "pods"}, 22 | "deployment": {Group: "apps", Version: "v1", Resource: "deployments"}, 23 | "service": {Group: "", Version: "v1", Resource: "services"}, 24 | "configmap": {Group: "", Version: "v1", Resource: "configmaps"}, 25 | "secret": {Group: "", Version: "v1", Resource: "secrets"}, 26 | "statefulset": {Group: "apps", Version: "v1", Resource: "statefulsets"}, 27 | "daemonset": {Group: "apps", Version: "v1", Resource: "daemonsets"}, 28 | "job": {Group: "batch", Version: "v1", Resource: "jobs"}, 29 | "cronjob": {Group: "batch", Version: "v1", Resource: "cronjobs"}, 30 | "ingress": {Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"}, 31 | "namespace": {Group: "", Version: "v1", Resource: "namespaces"}, 32 | "node": {Group: "", Version: "v1", Resource: "nodes"}, 33 | "pv": {Group: "", Version: "v1", Resource: "persistentvolumes"}, 34 | "pvc": {Group: "", Version: "v1", Resource: "persistentvolumeclaims"}, 35 | } 36 | 37 | // getGVR returns the GroupVersionResource for a given resource type 38 | func (c *Client) getGVR(resourceType string) (schema.GroupVersionResource, error) { 39 | // Check if it's in our pre-defined mappings 40 | resourceType = strings.ToLower(resourceType) 41 | if gvr, ok := resourceMappings[resourceType]; ok { 42 | return gvr, nil 43 | } 44 | 45 | // Try to get it from the API discovery 46 | c.logger.Debug("Resource not in predefined mappings, discovering from API", "resourceType", resourceType) 47 | resources, err := c.discoveryClient.ServerPreferredResources() 48 | if err != nil { 49 | return schema.GroupVersionResource{}, fmt.Errorf("failed to get server resources: %w", err) 50 | } 51 | 52 | for _, list := range resources { 53 | gv, err := schema.ParseGroupVersion(list.GroupVersion) 54 | if err != nil { 55 | continue 56 | } 57 | 58 | for _, r := range list.APIResources { 59 | if strings.EqualFold(r.Name, resourceType) || strings.EqualFold(r.SingularName, resourceType) { 60 | c.logger.Debug("Found resource via API discovery", 61 | "resourceType", resourceType, 62 | "group", gv.Group, 63 | "version", gv.Version, 64 | "resource", r.Name) 65 | return schema.GroupVersionResource{ 66 | Group: gv.Group, 67 | Version: gv.Version, 68 | Resource: r.Name, 69 | }, nil 70 | } 71 | } 72 | } 73 | 74 | return schema.GroupVersionResource{}, fmt.Errorf("unknown resource type: %s", resourceType) 75 | } 76 | 77 | // GetResource retrieves a specific resource by kind, namespace, and name 78 | func (c *Client) GetResource(ctx context.Context, kind, namespace, name string) (*unstructured.Unstructured, error) { 79 | c.logger.Debug("Getting resource", "kind", kind, "namespace", namespace, "name", name) 80 | 81 | gvr, err := c.getGVR(kind) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | var obj *unstructured.Unstructured 87 | if namespace != "" { 88 | obj, err = c.dynamicClient.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) 89 | } else { 90 | obj, err = c.dynamicClient.Resource(gvr).Get(ctx, name, metav1.GetOptions{}) 91 | } 92 | 93 | if err != nil { 94 | return nil, fmt.Errorf("failed to get %s %s/%s: %w", kind, namespace, name, err) 95 | } 96 | 97 | return obj, nil 98 | } 99 | 100 | // ListResources lists resources of a specific type, optionally filtered by namespace 101 | func (c *Client) ListResources(ctx context.Context, kind, namespace string) ([]unstructured.Unstructured, error) { 102 | c.logger.Debug("Listing resources", "kind", kind, "namespace", namespace) 103 | 104 | gvr, err := c.getGVR(kind) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | var list *unstructured.UnstructuredList 110 | if namespace != "" { 111 | list, err = c.dynamicClient.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{}) 112 | } else { 113 | list, err = c.dynamicClient.Resource(gvr).List(ctx, metav1.ListOptions{}) 114 | } 115 | 116 | if err != nil { 117 | return nil, fmt.Errorf("failed to list resources: %w", err) 118 | } 119 | 120 | c.logger.Debug("Listed resources", "kind", kind, "count", len(list.Items)) 121 | return list.Items, nil 122 | } 123 | 124 | // GetPodStatus returns detailed status information for a pod 125 | func (c *Client) GetPodStatus(ctx context.Context, namespace, name string) (*models.K8sPodStatus, error) { 126 | c.logger.Debug("Getting pod status", "namespace", namespace, "name", name) 127 | 128 | pod, err := c.clientset.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{}) 129 | if err != nil { 130 | return nil, fmt.Errorf("failed to get pod: %w", err) 131 | } 132 | 133 | status := &models.K8sPodStatus{ 134 | Phase: string(pod.Status.Phase), 135 | } 136 | 137 | for _, condition := range pod.Status.Conditions { 138 | status.Conditions = append(status.Conditions, struct { 139 | Type string `json:"type"` 140 | Status string `json:"status"` 141 | }{ 142 | Type: string(condition.Type), 143 | Status: string(condition.Status), 144 | }) 145 | } 146 | 147 | // Copy container statuses 148 | for _, containerStatus := range pod.Status.ContainerStatuses { 149 | cs := struct { 150 | Name string `json:"name"` 151 | Ready bool `json:"ready"` 152 | RestartCount int `json:"restartCount"` 153 | State struct { 154 | Running *struct{} `json:"running,omitempty"` 155 | Waiting *struct{} `json:"waiting,omitempty"` 156 | Terminated *struct{} `json:"terminated,omitempty"` 157 | } `json:"state"` 158 | LastState struct { 159 | Running *struct{} `json:"running,omitempty"` 160 | Waiting *struct{} `json:"waiting,omitempty"` 161 | Terminated *struct{} `json:"terminated,omitempty"` 162 | } `json:"lastState"` 163 | }{ 164 | Name: containerStatus.Name, 165 | Ready: containerStatus.Ready, 166 | RestartCount: int(containerStatus.RestartCount), 167 | } 168 | 169 | // Set state 170 | if containerStatus.State.Running != nil { 171 | cs.State.Running = &struct{}{} 172 | } 173 | if containerStatus.State.Waiting != nil { 174 | cs.State.Waiting = &struct{}{} 175 | } 176 | if containerStatus.State.Terminated != nil { 177 | cs.State.Terminated = &struct{}{} 178 | } 179 | 180 | // Set last state 181 | if containerStatus.LastTerminationState.Running != nil { 182 | cs.LastState.Running = &struct{}{} 183 | } 184 | if containerStatus.LastTerminationState.Waiting != nil { 185 | cs.LastState.Waiting = &struct{}{} 186 | } 187 | if containerStatus.LastTerminationState.Terminated != nil { 188 | cs.LastState.Terminated = &struct{}{} 189 | } 190 | 191 | status.ContainerStatuses = append(status.ContainerStatuses, cs) 192 | } 193 | 194 | return status, nil 195 | } 196 | 197 | // GetPodLogs returns logs for a specific container in a pod 198 | func (c *Client) GetPodLogs(ctx context.Context, namespace, name, container string, tailLines int64) (string, error) { 199 | c.logger.Debug("Getting pod logs", 200 | "namespace", namespace, 201 | "name", name, 202 | "container", container, 203 | "tailLines", tailLines) 204 | 205 | podLogOptions := corev1.PodLogOptions{ 206 | Container: container, 207 | } 208 | 209 | if tailLines > 0 { 210 | podLogOptions.TailLines = &tailLines 211 | } 212 | 213 | req := c.clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOptions) 214 | podLogs, err := req.Stream(ctx) 215 | if err != nil { 216 | return "", fmt.Errorf("failed to get pod logs: %w", err) 217 | } 218 | defer podLogs.Close() 219 | 220 | buf := new(bytes.Buffer) 221 | _, err = io.Copy(buf, podLogs) 222 | if err != nil { 223 | return "", fmt.Errorf("failed to read pod logs: %w", err) 224 | } 225 | 226 | return buf.String(), nil 227 | } 228 | 229 | // FindOwnerReferences finds the owner references for a resource 230 | func (c *Client) FindOwnerReferences(ctx context.Context, obj *unstructured.Unstructured) ([]unstructured.Unstructured, error) { 231 | c.logger.Debug("Finding owner references", 232 | "kind", obj.GetKind(), 233 | "name", obj.GetName(), 234 | "namespace", obj.GetNamespace()) 235 | 236 | ownerRefs := obj.GetOwnerReferences() 237 | if len(ownerRefs) == 0 { 238 | return nil, nil 239 | } 240 | 241 | var owners []unstructured.Unstructured 242 | for _, ref := range ownerRefs { 243 | c.logger.Debug("Found owner reference", 244 | "kind", ref.Kind, 245 | "name", ref.Name, 246 | "namespace", obj.GetNamespace()) 247 | 248 | gvr, err := c.getGVR(ref.Kind) 249 | if err != nil { 250 | c.logger.Warn("Failed to get GroupVersionResource for owner", 251 | "kind", ref.Kind, 252 | "error", err) 253 | continue 254 | } 255 | 256 | namespace := obj.GetNamespace() 257 | owner, err := c.dynamicClient.Resource(gvr).Namespace(namespace).Get(ctx, ref.Name, metav1.GetOptions{}) 258 | if err != nil { 259 | if errors.IsNotFound(err) { 260 | c.logger.Warn("Owner not found", 261 | "kind", ref.Kind, 262 | "name", ref.Name, 263 | "namespace", namespace) 264 | continue 265 | } 266 | return nil, fmt.Errorf("failed to get owner reference: %w", err) 267 | } 268 | 269 | owners = append(owners, *owner) 270 | } 271 | 272 | return owners, nil 273 | } ```