This is page 3 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/mergerequests.go: -------------------------------------------------------------------------------- ```go 1 | // internal/gitlab/mergerequests.go 2 | 3 | package gitlab 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | 13 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" 14 | ) 15 | 16 | // ListMergeRequests returns a list of merge requests for a project 17 | func (c *Client) ListMergeRequests(ctx context.Context, projectID string, state string) ([]models.GitLabMergeRequest, error) { 18 | c.logger.Debug("Listing merge requests", "projectID", projectID, "state", state) 19 | 20 | // Create endpoint with query parameters 21 | endpoint := fmt.Sprintf("projects/%s/merge_requests", url.PathEscape(projectID)) 22 | 23 | u, err := url.Parse(endpoint) 24 | if err != nil { 25 | return nil, fmt.Errorf("invalid endpoint: %w", err) 26 | } 27 | 28 | q := u.Query() 29 | if state != "" { 30 | q.Set("state", state) 31 | } 32 | q.Set("order_by", "updated_at") 33 | q.Set("sort", "desc") 34 | q.Set("per_page", "20") 35 | u.RawQuery = q.Encode() 36 | 37 | resp, err := c.doRequest(ctx, http.MethodGet, u.String(), nil) 38 | if err != nil { 39 | return nil, err 40 | } 41 | defer resp.Body.Close() 42 | 43 | var mergeRequests []models.GitLabMergeRequest 44 | if err := json.NewDecoder(resp.Body).Decode(&mergeRequests); err != nil { 45 | return nil, fmt.Errorf("failed to decode response: %w", err) 46 | } 47 | 48 | c.logger.Debug("Listed merge requests", "projectID", projectID, "count", len(mergeRequests)) 49 | return mergeRequests, nil 50 | } 51 | 52 | // GetMergeRequest returns details about a specific merge request 53 | func (c *Client) GetMergeRequest(ctx context.Context, projectID string, mergeRequestIID int) (*models.GitLabMergeRequest, error) { 54 | c.logger.Debug("Getting merge request", "projectID", projectID, "mergeRequestIID", mergeRequestIID) 55 | 56 | endpoint := fmt.Sprintf("projects/%s/merge_requests/%d", url.PathEscape(projectID), mergeRequestIID) 57 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 58 | if err != nil { 59 | return nil, err 60 | } 61 | defer resp.Body.Close() 62 | 63 | var mergeRequest models.GitLabMergeRequest 64 | if err := json.NewDecoder(resp.Body).Decode(&mergeRequest); err != nil { 65 | return nil, fmt.Errorf("failed to decode response: %w", err) 66 | } 67 | 68 | return &mergeRequest, nil 69 | } 70 | 71 | // GetMergeRequestChanges returns the changes in a specific merge request 72 | func (c *Client) GetMergeRequestChanges(ctx context.Context, projectID string, mergeRequestIID int) (*models.GitLabMergeRequest, error) { 73 | c.logger.Debug("Getting merge request changes", "projectID", projectID, "mergeRequestIID", mergeRequestIID) 74 | 75 | endpoint := fmt.Sprintf("projects/%s/merge_requests/%d/changes", url.PathEscape(projectID), mergeRequestIID) 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 mergeRequest models.GitLabMergeRequest 83 | if err := json.NewDecoder(resp.Body).Decode(&mergeRequest); err != nil { 84 | return nil, fmt.Errorf("failed to decode response: %w", err) 85 | } 86 | 87 | return &mergeRequest, nil 88 | } 89 | 90 | // GetMergeRequestApprovals returns approval information for a merge request 91 | func (c *Client) GetMergeRequestApprovals(ctx context.Context, projectID string, mergeRequestIID int) (*models.GitLabMergeRequestApproval, error) { 92 | c.logger.Debug("Getting merge request approvals", "projectID", projectID, "mergeRequestIID", mergeRequestIID) 93 | 94 | endpoint := fmt.Sprintf("projects/%s/merge_requests/%d/approvals", url.PathEscape(projectID), mergeRequestIID) 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 approvals models.GitLabMergeRequestApproval 102 | if err := json.NewDecoder(resp.Body).Decode(&approvals); err != nil { 103 | return nil, fmt.Errorf("failed to decode response: %w", err) 104 | } 105 | 106 | return &approvals, nil 107 | } 108 | 109 | // GetMergeRequestComments returns comments on a merge request 110 | func (c *Client) GetMergeRequestComments(ctx context.Context, projectID string, mergeRequestIID int) ([]models.GitLabMergeRequestComment, error) { 111 | c.logger.Debug("Getting merge request comments", "projectID", projectID, "mergeRequestIID", mergeRequestIID) 112 | 113 | endpoint := fmt.Sprintf("projects/%s/merge_requests/%d/notes", url.PathEscape(projectID), mergeRequestIID) 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 comments []models.GitLabMergeRequestComment 121 | if err := json.NewDecoder(resp.Body).Decode(&comments); err != nil { 122 | return nil, fmt.Errorf("failed to decode response: %w", err) 123 | } 124 | 125 | c.logger.Debug("Got merge request comments", "projectID", projectID, "mergeRequestIID", mergeRequestIID, "count", len(comments)) 126 | return comments, nil 127 | } 128 | 129 | // GetMergeRequestCommits returns the commits in a merge request 130 | func (c *Client) GetMergeRequestCommits(ctx context.Context, projectID string, mergeRequestIID int) ([]models.GitLabCommit, error) { 131 | c.logger.Debug("Getting merge request commits", "projectID", projectID, "mergeRequestIID", mergeRequestIID) 132 | 133 | endpoint := fmt.Sprintf("projects/%s/merge_requests/%d/commits", url.PathEscape(projectID), mergeRequestIID) 134 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 135 | if err != nil { 136 | return nil, err 137 | } 138 | defer resp.Body.Close() 139 | 140 | var commits []models.GitLabCommit 141 | if err := json.NewDecoder(resp.Body).Decode(&commits); err != nil { 142 | return nil, fmt.Errorf("failed to decode response: %w", err) 143 | } 144 | 145 | c.logger.Debug("Got merge request commits", "projectID", projectID, "mergeRequestIID", mergeRequestIID, "count", len(commits)) 146 | return commits, nil 147 | } 148 | 149 | // AnalyzeMergeRequest analyzes a merge request for Kubernetes/Helm changes 150 | func (c *Client) AnalyzeMergeRequest(ctx context.Context, projectID string, mergeRequestIID int) (*models.GitLabMergeRequest, error) { 151 | c.logger.Debug("Analyzing merge request", "projectID", projectID, "mergeRequestIID", mergeRequestIID) 152 | 153 | // Get basic merge request data 154 | mr, err := c.GetMergeRequest(ctx, projectID, mergeRequestIID) 155 | if err != nil { 156 | return nil, fmt.Errorf("failed to get merge request: %w", err) 157 | } 158 | 159 | // Get changes 160 | mrChanges, err := c.GetMergeRequestChanges(ctx, projectID, mergeRequestIID) 161 | if err != nil { 162 | return nil, fmt.Errorf("failed to get merge request changes: %w", err) 163 | } 164 | 165 | // Copy changes to the original merge request 166 | mr.Changes = mrChanges.Changes 167 | 168 | // Initialize context analysis 169 | mr.MergeRequestContext.AffectedFiles = make([]string, 0) 170 | mr.MergeRequestContext.HelmChartAffected = false 171 | mr.MergeRequestContext.KubernetesManifest = false 172 | 173 | // Analyze changes 174 | for _, change := range mr.Changes { 175 | mr.MergeRequestContext.AffectedFiles = append(mr.MergeRequestContext.AffectedFiles, change.NewPath) 176 | 177 | // Check for Helm charts 178 | if strings.Contains(change.NewPath, "Chart.yaml") || 179 | strings.Contains(change.NewPath, "values.yaml") || 180 | (strings.Contains(change.NewPath, "templates/") && strings.HasSuffix(change.NewPath, ".yaml")) { 181 | mr.MergeRequestContext.HelmChartAffected = true 182 | } 183 | 184 | // Check for Kubernetes manifests 185 | if strings.HasSuffix(change.NewPath, ".yaml") || strings.HasSuffix(change.NewPath, ".yml") { 186 | // Look for Kubernetes kind in the file content 187 | if strings.Contains(change.Diff, "kind:") && 188 | (strings.Contains(change.Diff, "Deployment") || 189 | strings.Contains(change.Diff, "Service") || 190 | strings.Contains(change.Diff, "ConfigMap") || 191 | strings.Contains(change.Diff, "Secret") || 192 | strings.Contains(change.Diff, "Pod")) { 193 | mr.MergeRequestContext.KubernetesManifest = true 194 | } 195 | } 196 | } 197 | 198 | // Get commits 199 | commits, err := c.GetMergeRequestCommits(ctx, projectID, mergeRequestIID) 200 | if err != nil { 201 | c.logger.Warn("Failed to get merge request commits", "error", err) 202 | } else { 203 | // Extract commit messages 204 | mr.MergeRequestContext.CommitMessages = make([]string, 0) 205 | for _, commit := range commits { 206 | mr.MergeRequestContext.CommitMessages = append(mr.MergeRequestContext.CommitMessages, commit.Title) 207 | } 208 | } 209 | 210 | return mr, nil 211 | } 212 | 213 | // CreateMergeRequestComment creates a new comment on a merge request 214 | func (c *Client) CreateMergeRequestComment(ctx context.Context, projectID string, mergeRequestIID int, body string) (*models.GitLabMergeRequestComment, error) { 215 | c.logger.Debug("Creating merge request comment", "projectID", projectID, "mergeRequestIID", mergeRequestIID) 216 | 217 | endpoint := fmt.Sprintf("projects/%s/merge_requests/%d/notes", url.PathEscape(projectID), mergeRequestIID) 218 | 219 | // Create request payload 220 | reqBody := map[string]string{ 221 | "body": body, 222 | } 223 | 224 | jsonBody, err := json.Marshal(reqBody) 225 | if err != nil { 226 | return nil, fmt.Errorf("failed to marshal request body: %w", err) 227 | } 228 | 229 | resp, err := c.doRequest(ctx, http.MethodPost, endpoint, strings.NewReader(string(jsonBody))) 230 | if err != nil { 231 | return nil, err 232 | } 233 | defer resp.Body.Close() 234 | 235 | var comment models.GitLabMergeRequestComment 236 | if err := json.NewDecoder(resp.Body).Decode(&comment); err != nil { 237 | return nil, fmt.Errorf("failed to decode response: %w", err) 238 | } 239 | 240 | return &comment, nil 241 | } 242 | ``` -------------------------------------------------------------------------------- /docs/src/pages/examples/index.astro: -------------------------------------------------------------------------------- ``` 1 | --- 2 | import BaseLayout from '../../layouts/BaseLayout.astro'; 3 | import CodeBlock from '../../components/CodeBlock.astro'; 4 | 5 | const exampleCategories = [ 6 | { 7 | title: "Basic API Usage", 8 | description: "Examples of common API calls for resource analysis", 9 | examples: [ 10 | { 11 | title: "Analyzing a Pod", 12 | description: "Query the status and health of a pod", 13 | code: `curl -X POST \\ 14 | -H "Content-Type: application/json" \\ 15 | -H "X-API-Key: your_api_key" \\ 16 | -d '{ 17 | "resource": "pod", 18 | "name": "my-app-pod", 19 | "namespace": "default", 20 | "query": "Is this pod healthy? What do the resource usage metrics show?" 21 | }' \\ 22 | http://mcp-server.example.com/api/v1/mcp/resource`, 23 | language: "bash" 24 | }, 25 | { 26 | title: "Checking Service Connectivity", 27 | description: "Investigate connectivity issues between services", 28 | code: `curl -X POST \\ 29 | -H "Content-Type: application/json" \\ 30 | -H "X-API-Key: your_api_key" \\ 31 | -d '{ 32 | "resource": "service", 33 | "name": "backend-service", 34 | "namespace": "default", 35 | "query": "Why can't my frontend pods connect to this service?" 36 | }' \\ 37 | http://mcp-server.example.com/api/v1/mcp/resource`, 38 | language: "bash" 39 | }, 40 | { 41 | title: "Deployment Analysis", 42 | description: "Understand why a deployment isn't scaling properly", 43 | code: `curl -X POST \\ 44 | -H "Content-Type: application/json" \\ 45 | -H "X-API-Key: your_api_key" \\ 46 | -d '{ 47 | "resource": "deployment", 48 | "name": "web-frontend", 49 | "namespace": "default", 50 | "query": "Why is this deployment not scaling to the requested replicas?" 51 | }' \\ 52 | http://mcp-server.example.com/api/v1/mcp/resource`, 53 | language: "bash" 54 | } 55 | ] 56 | }, 57 | { 58 | title: "Troubleshooting", 59 | description: "Examples for diagnosing and fixing common issues", 60 | examples: [ 61 | { 62 | title: "CrashLoopBackOff Investigation", 63 | description: "Troubleshoot a pod in CrashLoopBackOff state", 64 | code: `curl -X POST \\ 65 | -H "Content-Type: application/json" \\ 66 | -H "X-API-Key: your_api_key" \\ 67 | -d '{ 68 | "resource": "pod", 69 | "name": "crashing-pod", 70 | "namespace": "production", 71 | "query": "This pod is in CrashLoopBackOff. What's causing it and how can I fix it?" 72 | }' \\ 73 | http://mcp-server.example.com/api/v1/mcp/troubleshoot`, 74 | language: "bash" 75 | }, 76 | { 77 | title: "Ingress Issues", 78 | description: "Debug problems with an Ingress resource", 79 | code: `curl -X POST \\ 80 | -H "Content-Type: application/json" \\ 81 | -H "X-API-Key: your_api_key" \\ 82 | -d '{ 83 | "resource": "ingress", 84 | "name": "app-ingress", 85 | "namespace": "default", 86 | "query": "External users are getting 404 errors when accessing the application. What's wrong with this ingress?" 87 | }' \\ 88 | http://mcp-server.example.com/api/v1/mcp/troubleshoot`, 89 | language: "bash" 90 | }, 91 | { 92 | title: "Storage Problems", 93 | description: "Troubleshoot issues with PersistentVolumeClaims", 94 | code: `curl -X POST \\ 95 | -H "Content-Type: application/json" \\ 96 | -H "X-API-Key: your_api_key" \\ 97 | -d '{ 98 | "resource": "persistentvolumeclaim", 99 | "name": "database-storage", 100 | "namespace": "database", 101 | "query": "Why is this PVC stuck in pending state?" 102 | }' \\ 103 | http://mcp-server.example.com/api/v1/mcp/troubleshoot`, 104 | language: "bash" 105 | } 106 | ] 107 | }, 108 | { 109 | title: "GitOps Workflows", 110 | description: "Examples for CI/CD integration and GitOps analysis", 111 | examples: [ 112 | { 113 | title: "ArgoCD Application Analysis", 114 | description: "Check sync status and health of an ArgoCD application", 115 | code: `curl -X POST \\ 116 | -H "Content-Type: application/json" \\ 117 | -H "X-API-Key: your_api_key" \\ 118 | -d '{ 119 | "resource": "application", 120 | "name": "production-app", 121 | "namespace": "argocd", 122 | "query": "Is this application synced and healthy? If not, what are the issues?" 123 | }' \\ 124 | http://mcp-server.example.com/api/v1/mcp/resource`, 125 | language: "bash" 126 | }, 127 | { 128 | title: "Commit Impact Analysis", 129 | description: "Analyze how a specific commit affected the cluster", 130 | code: `curl -X POST \\ 131 | -H "Content-Type: application/json" \\ 132 | -H "X-API-Key: your_api_key" \\ 133 | -d '{ 134 | "projectId": "mygroup/myproject", 135 | "commitSha": "a1b2c3d4e5f6", 136 | "query": "What changes were made in this commit and how have they affected the deployed resources?" 137 | }' \\ 138 | http://mcp-server.example.com/api/v1/mcp/commit`, 139 | language: "bash" 140 | }, 141 | { 142 | title: "ArgoCD Sync Failure", 143 | description: "Troubleshoot why an ArgoCD application isn't syncing", 144 | code: `curl -X POST \\ 145 | -H "Content-Type: application/json" \\ 146 | -H "X-API-Key: your_api_key" \\ 147 | -d '{ 148 | "resource": "application", 149 | "name": "failing-app", 150 | "namespace": "argocd", 151 | "query": "Why is this application failing to sync? What specific errors are occurring?" 152 | }' \\ 153 | http://mcp-server.example.com/api/v1/mcp/troubleshoot`, 154 | language: "bash" 155 | } 156 | ] 157 | }, 158 | { 159 | title: "Advanced Usage", 160 | description: "Examples for more complex scenarios and integrations", 161 | examples: [ 162 | { 163 | title: "Resource Relationship Analysis", 164 | description: "Understanding dependencies between resources", 165 | code: `curl -X POST \\ 166 | -H "Content-Type: application/json" \\ 167 | -H "X-API-Key: your_api_key" \\ 168 | -d '{ 169 | "resource": "deployment", 170 | "name": "application", 171 | "namespace": "production", 172 | "query": "Create a map of all resources related to this deployment, including services, configmaps, secrets, and ingresses." 173 | }' \\ 174 | http://mcp-server.example.com/api/v1/mcp/resource`, 175 | language: "bash" 176 | }, 177 | { 178 | title: "Multi-Resource Correlation", 179 | description: "Analyze interactions between multiple resources", 180 | code: `curl -X POST \\ 181 | -H "Content-Type: application/json" \\ 182 | -H "X-API-Key: your_api_key" \\ 183 | -d '{ 184 | "query": "Analyze the connection between the frontend deployment, backend service, and redis statefulset in the web namespace. Are there any connectivity or configuration issues?" 185 | }' \\ 186 | http://mcp-server.example.com/api/v1/mcp`, 187 | language: "bash" 188 | }, 189 | { 190 | title: "GitOps Security Audit", 191 | description: "Audit resources for security issues and best practices", 192 | code: `curl -X POST \\ 193 | -H "Content-Type: application/json" \\ 194 | -H "X-API-Key: your_api_key" \\ 195 | -d '{ 196 | "resource": "namespace", 197 | "name": "production", 198 | "query": "Perform a security audit of all resources in this namespace. Check for security best practices, RBAC issues, and potential vulnerabilities." 199 | }' \\ 200 | http://mcp-server.example.com/api/v1/mcp/resource`, 201 | language: "bash" 202 | } 203 | ] 204 | } 205 | ]; 206 | --- 207 | 208 | <BaseLayout title="Examples | Kubernetes Claude MCP"> 209 | <div class="container mx-auto px-4 py-12"> 210 | <div class="max-w-5xl mx-auto"> 211 | <h1 class="text-4xl font-bold mb-6 text-primary-600">Examples</h1> 212 | <p class="text-xl text-slate-600 mb-10"> 213 | Explore practical examples of using the Kubernetes Claude MCP server for various use cases. 214 | These examples demonstrate how to leverage the API for resource analysis, troubleshooting, and GitOps workflows. 215 | </p> 216 | 217 | <div class="space-y-16"> 218 | {exampleCategories.map(category => ( 219 | <section class="example-category"> 220 | <h2 class="text-2xl font-bold mb-3 text-primary-600">{category.title}</h2> 221 | <p class="text-lg text-slate-600 mb-6">{category.description}</p> 222 | 223 | <div class="space-y-8"> 224 | {category.examples.map(example => ( 225 | <div class="example-card border border-secondary-300 rounded-lg overflow-hidden bg-secondary-50"> 226 | <div class="p-5 border-b border-secondary-300 bg-secondary-100"> 227 | <h3 class="text-xl font-semibold text-primary-600">{example.title}</h3> 228 | <p class="text-slate-600 mt-1">{example.description}</p> 229 | </div> 230 | <div class="p-5"> 231 | <CodeBlock 232 | code={example.code} 233 | lang={example.language} 234 | showLineNumbers={true} 235 | /> 236 | </div> 237 | </div> 238 | ))} 239 | </div> 240 | </section> 241 | ))} 242 | </div> 243 | 244 | <div class="mt-12 text-center"> 245 | <h2 class="text-2xl font-bold mb-4 text-primary-600">Need More Help?</h2> 246 | <p class="text-lg text-slate-600 mb-6"> 247 | Check out the detailed usage guides in the documentation or visit our GitHub repository. 248 | </p> 249 | <div class="flex flex-col sm:flex-row gap-4 justify-center"> 250 | <a href="/docs/api-overview" class="btn bg-primary-500 hover:bg-primary-600 text-white font-medium py-2 px-6 rounded-md"> 251 | API Reference 252 | </a> 253 | 254 | </div> 255 | </div> 256 | </div> 257 | </div> 258 | </BaseLayout> ``` -------------------------------------------------------------------------------- /docs/src/content/docs/api-overview.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: API Overview 3 | description: Comprehensive documentation of the Kubernetes Claude MCP REST API endpoints, parameters, and response formats. 4 | date: 2025-03-01 5 | order: 5 6 | tags: ['api', 'reference'] 7 | --- 8 | 9 | # API Overview 10 | 11 | Kubernetes Claude MCP provides a comprehensive REST API for interacting with Kubernetes resources, ArgoCD applications, GitLab repositories, and Claude's AI capabilities. This document provides an overview of all available endpoints, their parameters, and response formats. 12 | 13 | ## API Structure 14 | 15 | The API is organized into the following sections: 16 | 17 | - **General**: Health check and general information 18 | - **Kubernetes API**: Direct access to Kubernetes resources 19 | - **ArgoCD API**: Access to ArgoCD applications and sync status 20 | - **MCP API**: AI-powered analysis and troubleshooting 21 | 22 | All API calls require authentication using an API key, which is passed in the `X-API-Key` header or as a bearer token in the `Authorization` header. 23 | 24 | ```bash 25 | # Using X-API-Key header 26 | curl -H "X-API-Key: your_api_key" https://mcp.example.com/api/v1/health 27 | 28 | # Using Authorization header 29 | curl -H "Authorization: Bearer your_api_key" https://mcp.example.com/api/v1/health 30 | ``` 31 | 32 | ## Health Check 33 | 34 | ### GET /api/v1/health 35 | 36 | Check the health status of the server and its connected services. 37 | 38 | **Response:** 39 | 40 | ```json 41 | { 42 | "status": "ok", 43 | "services": { 44 | "kubernetes": "available", 45 | "argocd": "available", 46 | "gitlab": "available", 47 | "claude": "assumed available" 48 | } 49 | } 50 | ``` 51 | 52 | The `status` field will be `ok` if all required services are available, or `degraded` if some services are unavailable. 53 | 54 | ## Kubernetes API 55 | 56 | ### GET /api/v1/namespaces 57 | 58 | List all namespaces in the Kubernetes cluster. 59 | 60 | **Response:** 61 | 62 | ```json 63 | { 64 | "namespaces": [ 65 | "default", 66 | "kube-system", 67 | "monitoring", 68 | "argocd" 69 | ] 70 | } 71 | ``` 72 | 73 | ### GET /api/v1/resources/{kind}?namespace={ns} 74 | 75 | List all resources of a specific kind, optionally filtered by namespace. 76 | 77 | **Parameters:** 78 | - `kind`: The Kubernetes resource kind (e.g., `pod`, `deployment`, `service`) 79 | - `namespace`: (Optional) The namespace to filter by 80 | 81 | **Response:** 82 | 83 | ```json 84 | { 85 | "resources": [ 86 | { 87 | "apiVersion": "v1", 88 | "kind": "Pod", 89 | "metadata": { 90 | "name": "example-pod", 91 | "namespace": "default", 92 | "...": "..." 93 | }, 94 | "spec": { "...": "..." }, 95 | "status": { "...": "..." } 96 | }, 97 | // More resources... 98 | ] 99 | } 100 | ``` 101 | 102 | ### GET /api/v1/resources/{kind}/{name}?namespace={ns} 103 | 104 | Get a specific resource by kind, name, and namespace. 105 | 106 | **Parameters:** 107 | - `kind`: The Kubernetes resource kind 108 | - `name`: The resource name 109 | - `namespace`: (Optional) The namespace of the resource 110 | 111 | **Response:** 112 | 113 | ```json 114 | { 115 | "apiVersion": "v1", 116 | "kind": "Pod", 117 | "metadata": { 118 | "name": "example-pod", 119 | "namespace": "default", 120 | "...": "..." 121 | }, 122 | "spec": { "...": "..." }, 123 | "status": { "...": "..." } 124 | } 125 | ``` 126 | 127 | ### GET /api/v1/events?namespace={ns}&resource={kind}&name={name} 128 | 129 | Get events related to a specific resource. 130 | 131 | **Parameters:** 132 | - `namespace`: The namespace of the resource 133 | - `resource`: The resource kind 134 | - `name`: The resource name 135 | 136 | **Response:** 137 | 138 | ```json 139 | { 140 | "events": [ 141 | { 142 | "reason": "Created", 143 | "message": "Created container nginx", 144 | "type": "Normal", 145 | "count": 1, 146 | "firstTime": "2025-03-01T12:00:00Z", 147 | "lastTime": "2025-03-01T12:00:00Z", 148 | "object": { 149 | "kind": "Pod", 150 | "name": "example-pod", 151 | "namespace": "default" 152 | } 153 | }, 154 | // More events... 155 | ] 156 | } 157 | ``` 158 | 159 | ## ArgoCD API 160 | 161 | ### GET /api/v1/argocd/applications 162 | 163 | List all ArgoCD applications. 164 | 165 | **Response:** 166 | 167 | ```json 168 | { 169 | "applications": [ 170 | { 171 | "metadata": { 172 | "name": "example-app", 173 | "namespace": "argocd" 174 | }, 175 | "spec": { 176 | "source": { 177 | "repoURL": "https://github.com/example/repo.git", 178 | "path": "manifests", 179 | "targetRevision": "HEAD" 180 | }, 181 | "destination": { 182 | "server": "https://kubernetes.default.svc", 183 | "namespace": "default" 184 | } 185 | }, 186 | "status": { 187 | "sync": { 188 | "status": "Synced" 189 | }, 190 | "health": { 191 | "status": "Healthy" 192 | } 193 | } 194 | }, 195 | // More applications... 196 | ] 197 | } 198 | ``` 199 | 200 | ### GET /api/v1/argocd/applications/{name} 201 | 202 | Get a specific ArgoCD application by name. 203 | 204 | **Parameters:** 205 | - `name`: The ArgoCD application name 206 | 207 | **Response:** 208 | 209 | ```json 210 | { 211 | "metadata": { 212 | "name": "example-app", 213 | "namespace": "argocd" 214 | }, 215 | "spec": { 216 | "source": { 217 | "repoURL": "https://github.com/example/repo.git", 218 | "path": "manifests", 219 | "targetRevision": "HEAD" 220 | }, 221 | "destination": { 222 | "server": "https://kubernetes.default.svc", 223 | "namespace": "default" 224 | } 225 | }, 226 | "status": { 227 | "sync": { 228 | "status": "Synced" 229 | }, 230 | "health": { 231 | "status": "Healthy" 232 | } 233 | } 234 | } 235 | ``` 236 | 237 | ## MCP API 238 | 239 | The MCP API provides access to Claude's AI capabilities for analyzing Kubernetes resources and GitOps workflows. 240 | 241 | ### POST /api/v1/mcp 242 | 243 | Generic MCP request for Claude analysis. 244 | 245 | **Request:** 246 | 247 | ```json 248 | { 249 | "action": "string", 250 | "resource": "string", 251 | "name": "string", 252 | "namespace": "string", 253 | "query": "string", 254 | "commitSha": "string", 255 | "projectId": "string", 256 | "resourceSpecs": {}, 257 | "context": "string" 258 | } 259 | ``` 260 | 261 | **Response:** 262 | 263 | ```json 264 | { 265 | "success": true, 266 | "message": "Successfully processed request", 267 | "analysis": "Detailed analysis from Claude...", 268 | "actions": ["suggested actions..."], 269 | "context": {} 270 | } 271 | ``` 272 | 273 | ### POST /api/v1/mcp/resource 274 | 275 | Analyze a specific Kubernetes resource. 276 | 277 | **Request:** 278 | 279 | ```json 280 | { 281 | "resource": "pod", 282 | "name": "example-pod", 283 | "namespace": "default", 284 | "query": "Is this pod healthy? If not, what are the issues?" 285 | } 286 | ``` 287 | 288 | **Response:** 289 | 290 | ```json 291 | { 292 | "success": true, 293 | "message": "Successfully processed queryResource request", 294 | "analysis": "Detailed analysis of the pod's health status...", 295 | "context": { 296 | "kind": "Pod", 297 | "name": "example-pod", 298 | "namespace": "default", 299 | "argoApplication": {}, 300 | "gitlabProject": {}, 301 | "events": [] 302 | } 303 | } 304 | ``` 305 | 306 | ### POST /api/v1/mcp/commit 307 | 308 | Analyze the impact of a specific GitLab commit. 309 | 310 | **Request:** 311 | 312 | ```json 313 | { 314 | "projectId": "group/project", 315 | "commitSha": "abcdef123456", 316 | "query": "What changes were made in this commit and how do they affect the deployed resources?" 317 | } 318 | ``` 319 | 320 | **Response:** 321 | 322 | ```json 323 | { 324 | "success": true, 325 | "message": "Successfully processed queryCommit request", 326 | "analysis": "Detailed analysis of the commit and its impact...", 327 | "context": { 328 | "commit": {}, 329 | "affectedResources": [] 330 | } 331 | } 332 | ``` 333 | 334 | ### POST /api/v1/mcp/troubleshoot 335 | 336 | Troubleshoot a specific Kubernetes resource. 337 | 338 | **Request:** 339 | 340 | ```json 341 | { 342 | "resource": "deployment", 343 | "name": "example-deployment", 344 | "namespace": "default", 345 | "query": "Why is this deployment not scaling properly?" 346 | } 347 | ``` 348 | 349 | **Response:** 350 | 351 | ```json 352 | { 353 | "success": true, 354 | "message": "Successfully processed troubleshoot request", 355 | "analysis": "Detailed troubleshooting analysis...", 356 | "troubleshootResult": { 357 | "issues": [ 358 | { 359 | "title": "Resource Constraint", 360 | "category": "ResourceIssue", 361 | "severity": "Warning", 362 | "source": "Kubernetes", 363 | "description": "Deployment cannot scale due to insufficient CPU resources" 364 | } 365 | ], 366 | "recommendations": [ 367 | "Increase CPU request to allow for additional replicas", 368 | "Check node resources to ensure sufficient capacity" 369 | ] 370 | } 371 | } 372 | ``` 373 | 374 | ## Error Handling 375 | 376 | All API endpoints return standard HTTP status codes: 377 | 378 | - `200 OK`: Request was successful 379 | - `400 Bad Request`: Invalid request format or parameters 380 | - `401 Unauthorized`: Missing or invalid API key 381 | - `404 Not Found`: Resource not found 382 | - `500 Internal Server Error`: Server error 383 | 384 | Error responses include a JSON body with details: 385 | 386 | ```json 387 | { 388 | "error": "Failed to get resource", 389 | "details": "pod 'example-pod' not found in namespace 'default'" 390 | } 391 | ``` 392 | 393 | ## Pagination 394 | 395 | For endpoints that return collections, pagination is supported using the following query parameters: 396 | 397 | - `limit`: Maximum number of items to return (default: 100) 398 | - `page`: Page number to return (default: 1) 399 | 400 | Example: 401 | 402 | ``` 403 | GET /api/v1/resources/pods?namespace=default&limit=10&page=2 404 | ``` 405 | 406 | Response includes pagination metadata: 407 | 408 | ```json 409 | { 410 | "resources": [...], 411 | "pagination": { 412 | "total": 25, 413 | "pages": 3, 414 | "currentPage": 2, 415 | "limit": 10 416 | } 417 | } 418 | ``` 419 | 420 | ## API Versioning 421 | 422 | The API version is included in the URL path (`/api/v1/`). Future API versions will be made available at different paths (e.g., `/api/v2/`) to ensure backward compatibility. 423 | 424 | ## Rate Limiting 425 | 426 | The API implements rate limiting to prevent abuse. Rate limits vary by endpoint: 427 | 428 | - General endpoints: 100 requests per minute 429 | - Kubernetes endpoints: 60 requests per minute 430 | - ArgoCD endpoints: 60 requests per minute 431 | - MCP endpoints: 20 requests per minute 432 | 433 | Rate limit information is included in response headers: 434 | 435 | - `X-RateLimit-Limit`: Total requests allowed per minute 436 | - `X-RateLimit-Remaining`: Remaining requests in the current window 437 | - `X-RateLimit-Reset`: Seconds until the rate limit resets ``` -------------------------------------------------------------------------------- /docs/src/pages/index.astro: -------------------------------------------------------------------------------- ``` 1 | --- 2 | import BaseLayout from '../layouts/BaseLayout.astro'; 3 | --- 4 | 5 | <BaseLayout title="Kubernetes Claude MCP - AI-powered GitOps with Claude" 6 | description="The official documentation for Kubernetes Claude MCP Server, integrating Claude AI with Kubernetes, ArgoCD, and GitLab"> 7 | 8 | <!-- Hero Section --> 9 | <section class="py-20 px-4 bg-gradient-to-b from-secondary-200 to-secondary-100"> 10 | <div class="container mx-auto max-w-5xl"> 11 | <div class="flex flex-col items-center text-center"> 12 | <img src="/images/logo.svg" alt="Kubernetes Claude MCP Logo" class="w-48 h-48 mb-6"> 13 | <h1 class="text-5xl font-bold tracking-tight mb-6 text-primary-600"> 14 | Kubernetes Claude MCP 15 | </h1> 16 | <p class="text-xl text-slate-700 mb-8 max-w-2xl"> 17 | Model Context Protocol server for Kubernetes, integrating Claude AI with ArgoCD and GitLab for advanced GitOps workflows. 18 | </p> 19 | <div class="flex flex-col sm:flex-row gap-4"> 20 | <a href="/docs/introduction" class="btn bg-primary-500 hover:bg-primary-600 text-white font-medium py-2 px-6 rounded-md"> 21 | Get Started 22 | </a> 23 | <a href="https://github.com/blankcut/kubernetes-mcp-server" class="btn bg-secondary-200 hover:bg-secondary-300 text-primary-700 font-medium py-2 px-6 rounded-md" target="_blank" rel="noopener"> 24 | GitHub 25 | </a> 26 | </div> 27 | </div> 28 | </div> 29 | </section> 30 | 31 | <!-- Features Section --> 32 | <section class="py-16 px-4 bg-secondary-100"> 33 | <div class="container mx-auto max-w-6xl"> 34 | <h2 class="text-3xl font-bold text-center mb-12 text-primary-600">Key Features</h2> 35 | 36 | <div class="grid md:grid-cols-3 gap-8"> 37 | <!-- Feature 1 --> 38 | <div class="feature-card p-6 rounded-lg border border-secondary-200 shadow-sm bg-secondary-100"> 39 | <div class="mb-4 text-primary-500"> 40 | <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 41 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /> 42 | </svg> 43 | </div> 44 | <h3 class="text-xl font-semibold mb-2 text-primary-600">Kubernetes Integration</h3> 45 | <p class="text-slate-600"> 46 | Seamlessly connects to your Kubernetes cluster, providing automated analysis and troubleshooting of resources. 47 | </p> 48 | </div> 49 | 50 | <!-- Feature 2 --> 51 | <div class="feature-card p-6 rounded-lg border border-secondary-200 shadow-sm bg-secondary-100"> 52 | <div class="mb-4 text-primary-500"> 53 | <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 54 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /> 55 | </svg> 56 | </div> 57 | <h3 class="text-xl font-semibold mb-2 text-primary-600">Claude AI Powered</h3> 58 | <p class="text-slate-600"> 59 | Leverages Claude's AI capabilities to analyze configurations, explain issues, and recommend solutions for your cluster. 60 | </p> 61 | </div> 62 | 63 | <!-- Feature 3 --> 64 | <div class="feature-card p-6 rounded-lg border border-secondary-200 shadow-sm bg-secondary-100"> 65 | <div class="mb-4 text-primary-500"> 66 | <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 67 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" /> 68 | </svg> 69 | </div> 70 | <h3 class="text-xl font-semibold mb-2 text-primary-600">GitOps Integration</h3> 71 | <p class="text-slate-600"> 72 | Integrates with ArgoCD and GitLab to provide complete GitOps context for your deployments and configuration changes. 73 | </p> 74 | </div> 75 | </div> 76 | 77 | <div class="grid md:grid-cols-2 gap-8 mt-8"> 78 | <!-- Feature 4 --> 79 | <div class="feature-card p-6 rounded-lg border border-secondary-200 shadow-sm bg-secondary-100"> 80 | <div class="mb-4 text-primary-500"> 81 | <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 82 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /> 83 | </svg> 84 | </div> 85 | <h3 class="text-xl font-semibold mb-2 text-primary-600">Troubleshooting & Analysis</h3> 86 | <p class="text-slate-600"> 87 | Quickly identify issues in your cluster with comprehensive troubleshooting and analysis capabilities that trace through your entire deployment pipeline. 88 | </p> 89 | </div> 90 | 91 | <!-- Feature 5 --> 92 | <div class="feature-card p-6 rounded-lg border border-secondary-200 shadow-sm bg-secondary-100"> 93 | <div class="mb-4 text-primary-500"> 94 | <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 95 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" /> 96 | </svg> 97 | </div> 98 | <h3 class="text-xl font-semibold mb-2 text-primary-600">RESTful API</h3> 99 | <p class="text-slate-600"> 100 | Comprehensive API for integrating with your existing tools and workflows, with detailed documentation and examples. 101 | </p> 102 | </div> 103 | </div> 104 | </div> 105 | </section> 106 | 107 | <!-- How It Works Section --> 108 | <section class="py-16 px-4"> 109 | <div class="container mx-auto max-w-6xl"> 110 | <h2 class="text-3xl font-bold text-center mb-12 text-primary-600">How It Works</h2> 111 | 112 | <div class="grid md:grid-cols-3 gap-8 text-center"> 113 | <!-- Step 1 --> 114 | <div class="step"> 115 | <div class="bg-secondary-200 rounded-full h-16 w-16 flex items-center justify-center mx-auto mb-4 border border-secondary-300"> 116 | <span class="text-primary-600 text-xl font-bold">1</span> 117 | </div> 118 | <h3 class="text-xl font-semibold mb-2 text-primary-600">Connect</h3> 119 | <p class="text-slate-600"> 120 | Connect the server to your Kubernetes cluster, ArgoCD instance, and GitLab repositories. 121 | </p> 122 | </div> 123 | 124 | <!-- Step 2 --> 125 | <div class="step"> 126 | <div class="bg-secondary-200 rounded-full h-16 w-16 flex items-center justify-center mx-auto mb-4 border border-secondary-300"> 127 | <span class="text-primary-600 text-xl font-bold">2</span> 128 | </div> 129 | <h3 class="text-xl font-semibold mb-2 text-primary-600">Query</h3> 130 | <p class="text-slate-600"> 131 | Query resources, deployments, or commits through the API to get AI-powered analysis and context. 132 | </p> 133 | </div> 134 | 135 | <!-- Step 3 --> 136 | <div class="step"> 137 | <div class="bg-secondary-200 rounded-full h-16 w-16 flex items-center justify-center mx-auto mb-4 border border-secondary-300"> 138 | <span class="text-primary-600 text-xl font-bold">3</span> 139 | </div> 140 | <h3 class="text-xl font-semibold mb-2 text-primary-600">Analyze</h3> 141 | <p class="text-slate-600"> 142 | Receive detailed analysis, troubleshooting recommendations, and insights from Claude AI. 143 | </p> 144 | </div> 145 | </div> 146 | </div> 147 | </section> 148 | 149 | <!-- CTA Section --> 150 | <section class="py-16 px-4 bg-secondary-100"> 151 | <div class="container mx-auto max-w-5xl"> 152 | <div class="bg-primary-500 text-white rounded-lg p-8 md:p-12 shadow-lg"> 153 | <div class="text-center"> 154 | <h2 class="text-3xl font-bold mb-4">Ready to transform your Kubernetes experience?</h2> 155 | <p class="text-lg opacity-90 mb-8 max-w-2xl mx-auto"> 156 | Get started with Kubernetes Claude MCP today and enhance your GitOps workflows with AI-powered analysis and troubleshooting. 157 | </p> 158 | <div class="flex flex-col sm:flex-row gap-4 justify-center"> 159 | <a href="/docs/quick-start" class="btn bg-white text-primary-700 hover:bg-secondary-100 font-medium py-2 px-6 rounded-md"> 160 | Quick Start Guide 161 | </a> 162 | <a href="/docs/installation" class="btn bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-6 rounded-md border border-primary-400"> 163 | Installation Guide 164 | </a> 165 | </div> 166 | </div> 167 | </div> 168 | </div> 169 | </section> 170 | </BaseLayout> ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/argocd/client.go: -------------------------------------------------------------------------------- ```go 1 | package argocd 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "path" 13 | "strings" 14 | "time" 15 | 16 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/auth" 17 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/config" 18 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" 19 | ) 20 | 21 | // Client handles communication with the ArgoCD API 22 | type Client struct { 23 | baseURL string 24 | httpClient *http.Client 25 | credentialProvider *auth.CredentialProvider 26 | config *config.ArgoCDConfig 27 | logger *logging.Logger 28 | } 29 | 30 | // NewClient creates a new ArgoCD API client 31 | func NewClient(cfg *config.ArgoCDConfig, credProvider *auth.CredentialProvider, logger *logging.Logger) *Client { 32 | if logger == nil { 33 | logger = logging.NewLogger().Named("argocd") 34 | } 35 | 36 | // Create transport with optional insecure mode 37 | transport := &http.Transport{ 38 | TLSClientConfig: &tls.Config{ 39 | InsecureSkipVerify: cfg.Insecure, 40 | }, 41 | } 42 | 43 | return &Client{ 44 | baseURL: cfg.URL, 45 | httpClient: &http.Client{ 46 | Timeout: 30 * time.Second, 47 | Transport: transport, 48 | }, 49 | credentialProvider: credProvider, 50 | config: cfg, 51 | logger: logger, 52 | } 53 | } 54 | 55 | // CheckConnectivity tests the connection to the ArgoCD API 56 | func (c *Client) CheckConnectivity(ctx context.Context) error { 57 | c.logger.Debug("Checking ArgoCD connectivity") 58 | 59 | // Try to get ArgoCD version as a basic connectivity test 60 | endpoint := "/api/version" 61 | resp, err := c.doRequest(ctx, http.MethodGet, endpoint, nil) 62 | if err != nil { 63 | return fmt.Errorf("failed to connect to ArgoCD: %w", err) 64 | } 65 | defer resp.Body.Close() 66 | 67 | var version struct { 68 | Version string `json:"version"` 69 | } 70 | 71 | if err := json.NewDecoder(resp.Body).Decode(&version); err != nil { 72 | return fmt.Errorf("failed to decode ArgoCD version: %w", err) 73 | } 74 | 75 | c.logger.Debug("ArgoCD connectivity check successful", "version", version.Version) 76 | return nil 77 | } 78 | 79 | // doRequest performs an HTTP request to the ArgoCD API with authentication 80 | func (c *Client) doRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) { 81 | // Try the request with current credentials 82 | resp, err := c.attemptRequest(ctx, method, endpoint, body) 83 | 84 | // If we get a 401 unauthorized, try to refresh the token and retry once 85 | if err != nil && resp != nil && resp.StatusCode == http.StatusUnauthorized { 86 | c.logger.Debug("Received 401 from ArgoCD, attempting to refresh token") 87 | 88 | // Only try to refresh the token if we have username/password 89 | creds, err := c.credentialProvider.GetCredentials(auth.ServiceArgoCD) 90 | if err == nil && creds.Username != "" && creds.Password != "" { 91 | // Attempt to create a new session 92 | newToken, _, err := c.createSession(ctx, creds.Username, creds.Password) 93 | if err != nil { 94 | return nil, fmt.Errorf("failed to refresh ArgoCD token: %w", err) 95 | } 96 | 97 | // Update the credentials with the new token 98 | c.credentialProvider.UpdateArgoToken(ctx, newToken) 99 | 100 | // Retry the request with the new token 101 | return c.attemptRequest(ctx, method, endpoint, body) 102 | } 103 | } 104 | 105 | return resp, err 106 | } 107 | 108 | // attemptRequest makes a single request attempt 109 | func (c *Client) attemptRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) { 110 | // This contains the original doRequest logic 111 | u, err := url.Parse(c.baseURL) 112 | if err != nil { 113 | return nil, fmt.Errorf("invalid ArgoCD URL: %w", err) 114 | } 115 | u.Path = path.Join(u.Path, endpoint) 116 | 117 | req, err := http.NewRequestWithContext(ctx, method, u.String(), body) 118 | if err != nil { 119 | return nil, fmt.Errorf("failed to create request: %w", err) 120 | } 121 | 122 | if err := c.addAuth(req); err != nil { 123 | return nil, fmt.Errorf("failed to add authentication: %w", err) 124 | } 125 | 126 | req.Header.Set("Content-Type", "application/json") 127 | 128 | c.logger.Debug("Sending request to ArgoCD API", "method", method, "endpoint", endpoint) 129 | resp, err := c.httpClient.Do(req) 130 | if err != nil { 131 | return nil, fmt.Errorf("request failed: %w", err) 132 | } 133 | 134 | if resp.StatusCode >= 400 && resp.StatusCode != 401 { 135 | defer resp.Body.Close() 136 | body, _ := io.ReadAll(resp.Body) 137 | return nil, fmt.Errorf("ArgoCD API error (status %d): %s", resp.StatusCode, string(body)) 138 | } 139 | 140 | return resp, nil 141 | } 142 | 143 | // createSession creates a new ArgoCD session 144 | func (c *Client) createSession(ctx context.Context, username, password string) (string, time.Time, error) { 145 | // Create session request 146 | sessionReq := struct { 147 | Username string `json:"username"` 148 | Password string `json:"password"` 149 | }{ 150 | Username: username, 151 | Password: password, 152 | } 153 | 154 | // Convert to JSON 155 | sessionReqBody, err := json.Marshal(sessionReq) 156 | if err != nil { 157 | return "", time.Time{}, fmt.Errorf("failed to marshal session request: %w", err) 158 | } 159 | 160 | // Create a new HTTP client without authentication for this request 161 | u, err := url.Parse(c.baseURL) 162 | if err != nil { 163 | return "", time.Time{}, fmt.Errorf("invalid ArgoCD URL: %w", err) 164 | } 165 | u.Path = path.Join(u.Path, "/api/v1/session") 166 | 167 | req, err := http.NewRequestWithContext( 168 | ctx, 169 | http.MethodPost, 170 | u.String(), 171 | bytes.NewReader(sessionReqBody), 172 | ) 173 | if err != nil { 174 | return "", time.Time{}, fmt.Errorf("failed to create session request: %w", err) 175 | } 176 | 177 | req.Header.Set("Content-Type", "application/json") 178 | 179 | resp, err := c.httpClient.Do(req) 180 | if err != nil { 181 | return "", time.Time{}, fmt.Errorf("session request failed: %w", err) 182 | } 183 | defer resp.Body.Close() 184 | 185 | if resp.StatusCode != http.StatusOK { 186 | body, _ := io.ReadAll(resp.Body) 187 | return "", time.Time{}, fmt.Errorf("failed to create session (status %d): %s", resp.StatusCode, string(body)) 188 | } 189 | 190 | var sessionResp struct { 191 | Token string `json:"token"` 192 | } 193 | 194 | if err := json.NewDecoder(resp.Body).Decode(&sessionResp); err != nil { 195 | return "", time.Time{}, fmt.Errorf("failed to decode session response: %w", err) 196 | } 197 | 198 | // ArgoCD tokens will expire after 24 hours by default... 199 | expiry := time.Now().Add(24 * time.Hour) 200 | 201 | return sessionResp.Token, expiry, nil 202 | } 203 | 204 | // addAuth adds authentication to the request 205 | func (c *Client) addAuth(req *http.Request) error { 206 | creds, err := c.credentialProvider.GetCredentials(auth.ServiceArgoCD) 207 | if err != nil { 208 | return fmt.Errorf("failed to get ArgoCD credentials: %w", err) 209 | } 210 | 211 | if creds.Token != "" { 212 | // Set both header formats that ArgoCD might accept 213 | req.Header.Set("Authorization", "Bearer "+creds.Token) 214 | req.Header.Set("Cookie", "argocd.token="+creds.Token) 215 | return nil 216 | } 217 | 218 | if creds.Username != "" && creds.Password != "" { 219 | // We need to get a session token first 220 | token, _, err := c.createSession(req.Context(), creds.Username, creds.Password) 221 | if err != nil { 222 | return fmt.Errorf("failed to create ArgoCD session: %w", err) 223 | } 224 | 225 | // Update credentials with the new token 226 | c.credentialProvider.UpdateArgoToken(req.Context(), token) 227 | 228 | // Set both header formats 229 | req.Header.Set("Authorization", "Bearer "+token) 230 | req.Header.Set("Cookie", "argocd.token="+token) 231 | return nil 232 | } 233 | 234 | return fmt.Errorf("no valid ArgoCD credentials available") 235 | } 236 | 237 | // refreshToken gets a new token using username/password credentials 238 | func (c *Client) refreshToken(ctx context.Context) (string, time.Time, error) { 239 | creds, err := c.credentialProvider.GetCredentials(auth.ServiceArgoCD) 240 | if err != nil { 241 | return "", time.Time{}, fmt.Errorf("failed to get ArgoCD credentials: %w", err) 242 | } 243 | 244 | if creds.Username == "" || creds.Password == "" { 245 | return "", time.Time{}, fmt.Errorf("username/password required for token refresh") 246 | } 247 | 248 | // Create session request 249 | sessionReq := struct { 250 | Username string `json:"username"` 251 | Password string `json:"password"` 252 | }{ 253 | Username: creds.Username, 254 | Password: creds.Password, 255 | } 256 | 257 | // Convert to JSON 258 | sessionReqBody, err := json.Marshal(sessionReq) 259 | if err != nil { 260 | return "", time.Time{}, fmt.Errorf("failed to marshal session request: %w", err) 261 | } 262 | 263 | // Create a new HTTP client without authentication for this request 264 | req, err := http.NewRequestWithContext( 265 | ctx, 266 | http.MethodPost, 267 | fmt.Sprintf("%s/api/v1/session", c.baseURL), 268 | io.NopCloser(strings.NewReader(string(sessionReqBody))), 269 | ) 270 | if err != nil { 271 | return "", time.Time{}, fmt.Errorf("failed to create session request: %w", err) 272 | } 273 | 274 | req.Header.Set("Content-Type", "application/json") 275 | 276 | resp, err := c.httpClient.Do(req) 277 | if err != nil { 278 | return "", time.Time{}, fmt.Errorf("session request failed: %w", err) 279 | } 280 | defer resp.Body.Close() 281 | 282 | if resp.StatusCode != http.StatusOK { 283 | body, _ := io.ReadAll(resp.Body) 284 | return "", time.Time{}, fmt.Errorf("failed to create session (status %d): %s", resp.StatusCode, string(body)) 285 | } 286 | 287 | var sessionResp struct { 288 | Token string `json:"token"` 289 | } 290 | 291 | if err := json.NewDecoder(resp.Body).Decode(&sessionResp); err != nil { 292 | return "", time.Time{}, fmt.Errorf("failed to decode session response: %w", err) 293 | } 294 | 295 | // ArgoCD tokens typically expire after 24 hours 296 | expiry := time.Now().Add(24 * time.Hour) 297 | 298 | return sessionResp.Token, expiry, nil 299 | } 300 | ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/mcp/protocol.go: -------------------------------------------------------------------------------- ```go 1 | package mcp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/claude" 10 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/correlator" 11 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/k8s" 12 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" 13 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" 14 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/utils" 15 | 16 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 17 | ) 18 | 19 | // ProtocolHandler handles the Model Context Protocol for Kubernetes 20 | type ProtocolHandler struct { 21 | claudeClient *claude.Client 22 | claudeProtocol *claude.ProtocolHandler 23 | gitOpsCorrelator *correlator.GitOpsCorrelator 24 | k8sClient *k8s.Client 25 | contextManager *ContextManager 26 | promptGenerator *PromptGenerator 27 | logger *logging.Logger 28 | } 29 | 30 | // NewProtocolHandler creates a new MCP protocol handler 31 | func NewProtocolHandler( 32 | claudeClient *claude.Client, 33 | gitOpsCorrelator *correlator.GitOpsCorrelator, 34 | k8sClient *k8s.Client, 35 | logger *logging.Logger, 36 | ) *ProtocolHandler { 37 | if logger == nil { 38 | logger = logging.NewLogger().Named("mcp") 39 | } 40 | 41 | return &ProtocolHandler{ 42 | claudeClient: claudeClient, 43 | claudeProtocol: claude.NewProtocolHandler(claudeClient), 44 | gitOpsCorrelator: gitOpsCorrelator, 45 | k8sClient: k8sClient, 46 | contextManager: NewContextManager(100000, logger.Named("context")), 47 | promptGenerator: NewPromptGenerator(logger.Named("prompt")), 48 | logger: logger, 49 | } 50 | } 51 | 52 | // ProcessRequest processes an MCP request 53 | func (h *ProtocolHandler) ProcessRequest(ctx context.Context, request *models.MCPRequest) (*models.MCPResponse, error) { 54 | startTime := time.Now() 55 | h.logger.Info("Processing MCP request", "action", request.Action) 56 | 57 | var resourceContext string 58 | var err error 59 | 60 | // Handle different types of queries 61 | switch request.Action { 62 | case "queryResource": 63 | // If we have pre-populated context, use it 64 | if request.Context != "" { 65 | resourceContext = request.Context 66 | } else { 67 | // Trace deployment for a specific resource 68 | resourceInfo, err := h.gitOpsCorrelator.TraceResourceDeployment( 69 | ctx, 70 | request.Namespace, 71 | request.Resource, 72 | request.Name, 73 | ) 74 | if err != nil { 75 | return nil, fmt.Errorf("failed to trace resource deployment: %w", err) 76 | } 77 | 78 | // For non-namespace resources, enhance with the actual resource data 79 | if !strings.EqualFold(request.Resource, "namespace") { 80 | // Get the full resource details 81 | resource, err := h.k8sClient.GetResource(ctx, request.Resource, request.Namespace, request.Name) 82 | if err == nil && resource != nil { 83 | // Add the full resource details to the context 84 | resourceData, err := utils.ToJSON(resource.Object) 85 | if err == nil { 86 | resourceInfo.ResourceData = resourceData 87 | 88 | // Extract important deployment-specific information if available 89 | if strings.EqualFold(request.Resource, "deployment") { 90 | // Extract replicas info 91 | specReplicas, found, _ := unstructured.NestedInt64(resource.Object, "spec", "replicas") 92 | if found { 93 | if resourceInfo.Metadata == nil { 94 | resourceInfo.Metadata = make(map[string]interface{}) 95 | } 96 | resourceInfo.Metadata["desiredReplicas"] = specReplicas 97 | } 98 | 99 | // Extract status replica counts 100 | statusReplicas, found, _ := unstructured.NestedInt64(resource.Object, "status", "replicas") 101 | if found { 102 | if resourceInfo.Metadata == nil { 103 | resourceInfo.Metadata = make(map[string]interface{}) 104 | } 105 | resourceInfo.Metadata["currentReplicas"] = statusReplicas 106 | } 107 | 108 | // Extract readyReplicas 109 | readyReplicas, found, _ := unstructured.NestedInt64(resource.Object, "status", "readyReplicas") 110 | if found { 111 | if resourceInfo.Metadata == nil { 112 | resourceInfo.Metadata = make(map[string]interface{}) 113 | } 114 | resourceInfo.Metadata["readyReplicas"] = readyReplicas 115 | } 116 | 117 | // Extract availableReplicas 118 | availableReplicas, found, _ := unstructured.NestedInt64(resource.Object, "status", "availableReplicas") 119 | if found { 120 | if resourceInfo.Metadata == nil { 121 | resourceInfo.Metadata = make(map[string]interface{}) 122 | } 123 | resourceInfo.Metadata["availableReplicas"] = availableReplicas 124 | } 125 | 126 | // Extract container info 127 | containers, found, _ := unstructured.NestedSlice(resource.Object, "spec", "template", "spec", "containers") 128 | if found { 129 | var containerInfo []map[string]interface{} 130 | for _, c := range containers { 131 | container, ok := c.(map[string]interface{}) 132 | if !ok { 133 | continue 134 | } 135 | 136 | containerData := map[string]interface{}{ 137 | "name": container["name"], 138 | } 139 | 140 | if image, ok := container["image"].(string); ok { 141 | containerData["image"] = image 142 | } 143 | 144 | if resources, ok := container["resources"].(map[string]interface{}); ok { 145 | containerData["resources"] = resources 146 | } 147 | 148 | containerInfo = append(containerInfo, containerData) 149 | } 150 | 151 | if resourceInfo.Metadata == nil { 152 | resourceInfo.Metadata = make(map[string]interface{}) 153 | } 154 | resourceInfo.Metadata["containers"] = containerInfo 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | formattedContext, err := h.contextManager.FormatResourceContext(resourceInfo) 162 | if err != nil { 163 | return nil, fmt.Errorf("failed to format resource context: %w", err) 164 | } 165 | 166 | resourceContext = formattedContext 167 | } 168 | 169 | case "queryMergeRequest": 170 | // Analyze merge request 171 | resources, err := h.gitOpsCorrelator.AnalyzeMergeRequest( 172 | ctx, 173 | request.ProjectID, 174 | request.MergeRequestIID, 175 | ) 176 | if err != nil { 177 | return nil, fmt.Errorf("failed to analyze merge request: %w", err) 178 | } 179 | 180 | resourceContext, err = h.contextManager.CombineContexts(ctx, resources) 181 | if err != nil { 182 | return nil, fmt.Errorf("failed to combine resource contexts: %w", err) 183 | } 184 | 185 | default: 186 | return nil, fmt.Errorf("unsupported action: %s", request.Action) 187 | } 188 | 189 | // Generate prompts for Claude 190 | h.logger.Debug("Generating prompts for Claude") 191 | systemPrompt := h.promptGenerator.GenerateSystemPrompt() 192 | userPrompt := h.promptGenerator.GenerateUserPrompt(resourceContext, request.Query) 193 | 194 | // Get completion from Claude 195 | h.logger.Debug("Sending request to Claude", 196 | "systemPromptLength", len(systemPrompt), 197 | "userPromptLength", len(userPrompt)) 198 | 199 | analysis, err := h.claudeProtocol.GetCompletion(ctx, systemPrompt, userPrompt) 200 | if err != nil { 201 | return nil, fmt.Errorf("failed to get completion from Claude: %w", err) 202 | } 203 | 204 | // Build response 205 | response := &models.MCPResponse{ 206 | Success: true, 207 | Analysis: analysis, 208 | Message: fmt.Sprintf("Successfully processed %s request in %v", request.Action, time.Since(startTime)), 209 | } 210 | 211 | h.logger.Info("MCP request processed successfully", 212 | "action", request.Action, 213 | "duration", time.Since(startTime), 214 | "responseLength", len(analysis)) 215 | 216 | return response, nil 217 | } 218 | 219 | // ProcessTroubleshootRequest processes a troubleshooting request with detected issues 220 | func (h *ProtocolHandler) ProcessTroubleshootRequest(ctx context.Context, request *models.MCPRequest, troubleshootResult *models.TroubleshootResult) (*models.MCPResponse, error) { 221 | startTime := time.Now() 222 | h.logger.Debug("Processing troubleshoot request") 223 | 224 | // Extract issues and recommendations 225 | var issuesText string 226 | for i, issue := range troubleshootResult.Issues { 227 | issuesText += fmt.Sprintf("%d. %s (%s): %s\n", 228 | i+1, 229 | issue.Title, 230 | issue.Severity, 231 | issue.Description) 232 | } 233 | 234 | var recommendationsText string 235 | for i, rec := range troubleshootResult.Recommendations { 236 | recommendationsText += fmt.Sprintf("%d. %s\n", i+1, rec) 237 | } 238 | 239 | // Create a prompt for Claude with the troubleshooting results 240 | userPrompt := fmt.Sprintf( 241 | "I'm troubleshooting a Kubernetes %s named '%s' in namespace '%s'.\n\n"+ 242 | "The following issues were detected:\n%s\n"+ 243 | "General recommendations:\n%s\n\n"+ 244 | "Based on these detected issues, please provide specific kubectl commands "+ 245 | "that I can use to troubleshoot and fix the problems. %s", 246 | request.Resource, 247 | request.Name, 248 | request.Namespace, 249 | issuesText, 250 | recommendationsText, 251 | request.Query) 252 | 253 | // Generate system prompt 254 | systemPrompt := h.promptGenerator.GenerateSystemPrompt() 255 | 256 | // Get Claude's analysis 257 | h.logger.Debug("Sending troubleshoot request to Claude", 258 | "systemPromptLength", len(systemPrompt), 259 | "userPromptLength", len(userPrompt)) 260 | 261 | analysis, err := h.claudeProtocol.GetCompletion(ctx, systemPrompt, userPrompt) 262 | if err != nil { 263 | return nil, fmt.Errorf("failed to get completion for troubleshoot request: %w", err) 264 | } 265 | 266 | // Create response 267 | response := &models.MCPResponse{ 268 | Success: true, 269 | Analysis: analysis, 270 | Message: fmt.Sprintf("Successfully processed troubleshoot request in %v", time.Since(startTime)), 271 | } 272 | 273 | h.logger.Info("Troubleshoot request processed successfully", 274 | "duration", time.Since(startTime), 275 | "responseLength", len(analysis)) 276 | 277 | return response, nil 278 | } 279 | 280 | // WithCustomPrompt sets a custom base prompt template 281 | func (h *ProtocolHandler) WithCustomPrompt(template string) *ProtocolHandler { 282 | h.promptGenerator.WithBasePrompt(template) 283 | return h 284 | } 285 | 286 | // WithMaxContextSize sets the maximum context size 287 | func (h *ProtocolHandler) WithMaxContextSize(size int) *ProtocolHandler { 288 | h.contextManager = NewContextManager(size, h.logger.Named("context")) 289 | return h 290 | } 291 | ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/mcp/namespace_analyzer.go: -------------------------------------------------------------------------------- ```go 1 | package mcp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" 10 | k8s "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/k8s" 11 | ) 12 | 13 | // NamespaceAnalysisResult contains the analysis of a namespace's resources 14 | type NamespaceAnalysisResult struct { 15 | Namespace string `json:"namespace"` 16 | ResourceCounts map[string]int `json:"resourceCounts"` 17 | HealthStatus map[string]map[string]int `json:"healthStatus"` 18 | ResourceRelationships []k8s.ResourceRelationship `json:"resourceRelationships"` 19 | Issues []models.Issue `json:"issues"` 20 | Recommendations []string `json:"recommendations"` 21 | Analysis string `json:"analysis"` 22 | } 23 | 24 | // AnalyzeNamespace analyzes all resources in a namespace using Claude 25 | func (h *ProtocolHandler) AnalyzeNamespace(ctx context.Context, namespace string) (*models.NamespaceAnalysisResult, error) { 26 | startTime := time.Now() 27 | h.logger.Info("Analyzing namespace", "namespace", namespace) 28 | 29 | // Get namespace topology 30 | topology, err := h.k8sClient.GetNamespaceTopology(ctx, namespace) 31 | if err != nil { 32 | return nil, fmt.Errorf("failed to get namespace topology: %w", err) 33 | } 34 | 35 | // Initialize result 36 | result := &models.NamespaceAnalysisResult{ 37 | Namespace: namespace, 38 | ResourceCounts: make(map[string]int), 39 | HealthStatus: make(map[string]map[string]int), 40 | Issues: []models.Issue{}, 41 | Recommendations: []string{}, 42 | } 43 | 44 | // Extract resource counts 45 | for kind, resources := range topology.Resources { 46 | result.ResourceCounts[kind] = len(resources) 47 | } 48 | 49 | // Extract health status 50 | for kind, statusMap := range topology.Health { 51 | healthCounts := make(map[string]int) 52 | for _, status := range statusMap { 53 | healthCounts[status]++ 54 | } 55 | result.HealthStatus[kind] = healthCounts 56 | } 57 | 58 | // Add relationships - Convert from k8s.ResourceRelationship to models.ResourceRelationship 59 | for _, rel := range topology.Relationships { 60 | modelRel := models.ResourceRelationship{ 61 | SourceKind: rel.SourceKind, 62 | SourceName: rel.SourceName, 63 | SourceNamespace: rel.SourceNamespace, 64 | TargetKind: rel.TargetKind, 65 | TargetName: rel.TargetName, 66 | TargetNamespace: rel.TargetNamespace, 67 | RelationType: rel.RelationType, 68 | } 69 | result.ResourceRelationships = append(result.ResourceRelationships, modelRel) 70 | } 71 | 72 | // Get events for the namespace 73 | events, err := h.k8sClient.GetNamespaceEvents(ctx, namespace) 74 | if err != nil { 75 | h.logger.Warn("Failed to get namespace events", "error", err) 76 | } 77 | 78 | // Identify issues from events 79 | for _, event := range events { 80 | if event.Type == "Warning" { 81 | issue := models.Issue{ 82 | Source: "Kubernetes", 83 | Severity: "Warning", 84 | Description: fmt.Sprintf("%s: %s", event.Reason, event.Message), 85 | } 86 | 87 | // Categorize common issues 88 | switch { 89 | case strings.Contains(event.Reason, "Failed") && strings.Contains(event.Message, "ImagePull"): 90 | issue.Category = "ImagePullError" 91 | issue.Title = "Image Pull Failure" 92 | 93 | case strings.Contains(event.Reason, "Unhealthy"): 94 | issue.Category = "HealthCheckFailure" 95 | issue.Title = "Health Check Failure" 96 | 97 | case strings.Contains(event.Message, "memory"): 98 | issue.Category = "ResourceIssue" 99 | issue.Title = "Memory Resource Issue" 100 | 101 | case strings.Contains(event.Message, "cpu"): 102 | issue.Category = "ResourceIssue" 103 | issue.Title = "CPU Resource Issue" 104 | 105 | case strings.Contains(event.Reason, "BackOff"): 106 | issue.Category = "CrashLoopBackOff" 107 | issue.Title = "Container Crash Loop" 108 | 109 | default: 110 | issue.Category = "OtherWarning" 111 | issue.Title = "Kubernetes Warning" 112 | } 113 | 114 | result.Issues = append(result.Issues, issue) 115 | } 116 | } 117 | 118 | // Generate Claude analysis 119 | analysisPrompt := h.generateNamespaceAnalysisPrompt(namespace, topology, events) 120 | systemPrompt := h.promptGenerator.GenerateSystemPrompt() 121 | 122 | h.logger.Debug("Sending namespace analysis request to Claude", 123 | "namespace", namespace, 124 | "systemPromptLength", len(systemPrompt), 125 | "analysisPromptLength", len(analysisPrompt)) 126 | 127 | analysis, err := h.claudeProtocol.GetCompletion(ctx, systemPrompt, analysisPrompt) 128 | if err != nil { 129 | return nil, fmt.Errorf("failed to get completion for namespace analysis: %w", err) 130 | } 131 | 132 | // Extract recommendations from analysis 133 | lines := strings.Split(analysis, "\n") 134 | inRecommendations := false 135 | 136 | for _, line := range lines { 137 | if strings.Contains(strings.ToLower(line), "recommendation") || 138 | strings.Contains(strings.ToLower(line), "recommendations") || 139 | strings.Contains(strings.ToLower(line), "suggest") { 140 | inRecommendations = true 141 | continue 142 | } 143 | 144 | if inRecommendations && strings.TrimSpace(line) != "" && !strings.HasPrefix(line, "#") { 145 | // Remove leading dash or number if it exists 146 | cleanLine := strings.TrimSpace(line) 147 | if strings.HasPrefix(cleanLine, "- ") { 148 | cleanLine = cleanLine[2:] 149 | } else if len(cleanLine) > 2 && strings.HasPrefix(cleanLine, "* ") { 150 | cleanLine = cleanLine[2:] 151 | } else if len(cleanLine) > 3 && 152 | ((cleanLine[0] >= '1' && cleanLine[0] <= '9') && 153 | (cleanLine[1] == '.' || cleanLine[1] == ')') && 154 | (cleanLine[2] == ' ')) { 155 | cleanLine = cleanLine[3:] 156 | } 157 | 158 | if cleanLine != "" && len(result.Recommendations) < 10 { 159 | result.Recommendations = append(result.Recommendations, cleanLine) 160 | } 161 | } 162 | } 163 | 164 | result.Analysis = analysis 165 | 166 | h.logger.Info("Namespace analysis completed", 167 | "namespace", namespace, 168 | "duration", time.Since(startTime), 169 | "issueCount", len(result.Issues), 170 | "recommendationCount", len(result.Recommendations)) 171 | 172 | return result, nil 173 | } 174 | 175 | // generateNamespaceAnalysisPrompt creates a prompt for namespace analysis 176 | func (h *ProtocolHandler) generateNamespaceAnalysisPrompt(namespace string, topology *k8s.NamespaceTopology, events []models.K8sEvent) string { 177 | // Start with namespace overview 178 | prompt := fmt.Sprintf("# Namespace Analysis: %s\n\n", namespace) 179 | 180 | // Add resource summary 181 | prompt += "## Resource Summary\n\n" 182 | for kind, resources := range topology.Resources { 183 | prompt += fmt.Sprintf("- %s: %d resources\n", kind, len(resources)) 184 | } 185 | prompt += "\n" 186 | 187 | // Add health status summary 188 | prompt += "## Health Status\n\n" 189 | for kind, statusMap := range topology.Health { 190 | prompt += fmt.Sprintf("### %s Health\n", kind) 191 | 192 | // Count the statuses 193 | healthCounts := make(map[string]int) 194 | for _, status := range statusMap { 195 | healthCounts[status]++ 196 | } 197 | 198 | // List the counts 199 | for status, count := range healthCounts { 200 | prompt += fmt.Sprintf("- %s: %d resources\n", status, count) 201 | } 202 | 203 | // List unhealthy resources 204 | unhealthyResources := []string{} 205 | for name, status := range statusMap { 206 | if status == "unhealthy" { 207 | unhealthyResources = append(unhealthyResources, name) 208 | } 209 | } 210 | 211 | if len(unhealthyResources) > 0 { 212 | prompt += "\nUnhealthy resources:\n" 213 | for _, name := range unhealthyResources { 214 | prompt += fmt.Sprintf("- %s\n", name) 215 | } 216 | } 217 | 218 | prompt += "\n" 219 | } 220 | 221 | // Add relationship summary 222 | if len(topology.Relationships) > 0 { 223 | prompt += "## Resource Relationships\n\n" 224 | 225 | // Group by relationship type 226 | relationshipsByType := make(map[string][]string) 227 | for _, rel := range topology.Relationships { 228 | key := rel.RelationType 229 | relationshipsByType[key] = append( 230 | relationshipsByType[key], 231 | fmt.Sprintf("%s/%s -> %s/%s", 232 | rel.SourceKind, rel.SourceName, 233 | rel.TargetKind, rel.TargetName)) 234 | } 235 | 236 | // List relationships by type 237 | for relType, relations := range relationshipsByType { 238 | prompt += fmt.Sprintf("### %s Relationships\n", strings.Title(relType)) 239 | for _, rel := range relations { 240 | prompt += fmt.Sprintf("- %s\n", rel) 241 | } 242 | prompt += "\n" 243 | } 244 | } 245 | 246 | // Add recent events 247 | if len(events) > 0 { 248 | prompt += "## Recent Events\n\n" 249 | 250 | // Group events by type 251 | warningEvents := []models.K8sEvent{} 252 | normalEvents := []models.K8sEvent{} 253 | 254 | for _, event := range events { 255 | if event.Type == "Warning" { 256 | warningEvents = append(warningEvents, event) 257 | } else { 258 | normalEvents = append(normalEvents, event) 259 | } 260 | } 261 | 262 | // Add warning events first (limited to 10) 263 | if len(warningEvents) > 0 { 264 | prompt += "### Warning Events\n" 265 | count := 0 266 | for _, event := range warningEvents { 267 | if count >= 10 { 268 | break 269 | } 270 | prompt += fmt.Sprintf("- [%s] %s: %s (%s)\n", 271 | event.LastTime.Format(time.RFC3339), 272 | event.Reason, 273 | event.Message, 274 | fmt.Sprintf("%s/%s", event.Object.Kind, event.Object.Name)) 275 | count++ 276 | } 277 | prompt += "\n" 278 | } 279 | 280 | // Add a few normal events (limited to 5) 281 | if len(normalEvents) > 0 { 282 | prompt += "### Normal Events\n" 283 | count := 0 284 | for _, event := range normalEvents { 285 | if count >= 5 { 286 | break 287 | } 288 | prompt += fmt.Sprintf("- [%s] %s: %s (%s)\n", 289 | event.LastTime.Format(time.RFC3339), 290 | event.Reason, 291 | event.Message, 292 | fmt.Sprintf("%s/%s", event.Object.Kind, event.Object.Name)) 293 | count++ 294 | } 295 | prompt += "\n" 296 | } 297 | } 298 | 299 | // Add analysis request 300 | prompt += "## Analysis Request\n\n" 301 | prompt += "Based on the information above, please provide a comprehensive analysis of this Kubernetes namespace, including:\n\n" 302 | prompt += "1. Overall health assessment\n" 303 | prompt += "2. Identification of any issues or problems\n" 304 | prompt += "3. Analysis of resource relationships and dependencies\n" 305 | prompt += "4. Potential bottlenecks or misconfigurations\n" 306 | prompt += "5. Security concerns (if any can be identified)\n" 307 | prompt += "6. Specific recommendations for improvement\n\n" 308 | prompt += "Please format your analysis with clear sections and provide specific, actionable recommendations that would help improve the reliability, efficiency, and security of this namespace." 309 | 310 | return prompt 311 | } 312 | ``` -------------------------------------------------------------------------------- /docs/src/content/docs/installation.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Installation Guide 3 | description: Comprehensive guide for installing and configuring the Kubernetes Claude MCP server in various environments. 4 | date: 2025-03-01 5 | order: 3 6 | tags: ['installation', 'deployment'] 7 | --- 8 | 9 | # Installation Guide 10 | 11 | This guide provides detailed instructions for installing Kubernetes Claude MCP in different environments. Choose the method that best suits your needs. 12 | 13 | ## Prerequisites 14 | 15 | Before installing Kubernetes Claude MCP, ensure you have: 16 | 17 | - Access to a Kubernetes cluster (v1.19+) 18 | - kubectl configured to access your cluster 19 | - Claude API key from Anthropic 20 | - Optional: ArgoCD instance (for GitOps integration) 21 | - Optional: GitLab access (for commit analysis) 22 | 23 | ## Installation Methods 24 | 25 | There are several ways to install Kubernetes Claude MCP: 26 | 27 | 1. [Docker Compose](#docker-compose) (for development/testing) 28 | 2. [Kubernetes Deployment](#kubernetes-deployment) (recommended for production) 29 | 3. [Helm Chart](#helm-chart) (easiest for Kubernetes) 30 | 4. [Manual Binary](#manual-binary) (for custom environments) 31 | 32 | ## Docker Compose 33 | 34 | Docker Compose is ideal for local development and testing. 35 | 36 | ### Step 1: Clone the Repository 37 | 38 | ```bash 39 | git clone https://github.com/blankcut/kubernetes-mcp-server.git 40 | cd kubernetes-mcp-server 41 | ``` 42 | 43 | ### Step 2: Configure Environment Variables 44 | 45 | Create a `.env` file with your credentials: 46 | 47 | ```bash 48 | CLAUDE_API_KEY=your_claude_api_key 49 | ARGOCD_USERNAME=your_argocd_username 50 | ARGOCD_PASSWORD=your_argocd_password 51 | GITLAB_AUTH_TOKEN=your_gitlab_token 52 | API_KEY=your_api_key_for_server_access 53 | ``` 54 | 55 | ### Step 3: Configure the Server 56 | 57 | Create or modify `config.yaml`: 58 | 59 | ```yaml 60 | server: 61 | address: ":8080" 62 | readTimeout: 30 63 | writeTimeout: 60 64 | auth: 65 | apiKey: "${API_KEY}" 66 | 67 | kubernetes: 68 | kubeconfig: "" 69 | inCluster: false 70 | defaultContext: "" 71 | defaultNamespace: "default" 72 | 73 | argocd: 74 | url: "${ARGOCD_URL}" 75 | authToken: "${ARGOCD_AUTH_TOKEN}" 76 | username: "${ARGOCD_USERNAME}" 77 | password: "${ARGOCD_PASSWORD}" 78 | insecure: true 79 | 80 | gitlab: 81 | url: "${GITLAB_URL}" 82 | authToken: "${GITLAB_AUTH_TOKEN}" 83 | apiVersion: "v4" 84 | projectPath: "${PROJECT_PATH}" 85 | 86 | claude: 87 | apiKey: "${CLAUDE_API_KEY}" 88 | baseURL: "https://api.anthropic.com" 89 | modelID: "claude-3-haiku-20240307" 90 | maxTokens: 4096 91 | temperature: 0.7 92 | ``` 93 | 94 | ### Step 4: Start the Service 95 | 96 | ```bash 97 | docker-compose up -d 98 | ``` 99 | 100 | The server will be available at http://localhost:8080. 101 | 102 | ## Kubernetes Deployment 103 | 104 | For production environments, deploying to Kubernetes is recommended. 105 | 106 | ### Step 1: Create a Namespace 107 | 108 | ```bash 109 | kubectl create namespace mcp-system 110 | ``` 111 | 112 | ### Step 2: Create Secrets 113 | 114 | ```bash 115 | kubectl create secret generic mcp-secrets \ 116 | --namespace mcp-system \ 117 | --from-literal=claude-api-key=your_claude_api_key \ 118 | --from-literal=argocd-username=your_argocd_username \ 119 | --from-literal=argocd-password=your_argocd_password \ 120 | --from-literal=gitlab-token=your_gitlab_token \ 121 | --from-literal=api-key=your_api_key_for_server_access 122 | ``` 123 | 124 | ### Step 3: Create ConfigMap 125 | 126 | ```bash 127 | kubectl create configmap mcp-config \ 128 | --namespace mcp-system \ 129 | --from-file=config.yaml 130 | ``` 131 | 132 | ### Step 4: Apply Deployment Manifest 133 | 134 | Create a file named `deployment.yaml`: 135 | 136 | ```yaml 137 | apiVersion: apps/v1 138 | kind: Deployment 139 | metadata: 140 | name: kubernetes-mcp-server 141 | namespace: mcp-system 142 | labels: 143 | app: kubernetes-mcp-server 144 | spec: 145 | replicas: 1 146 | selector: 147 | matchLabels: 148 | app: kubernetes-mcp-server 149 | template: 150 | metadata: 151 | labels: 152 | app: kubernetes-mcp-server 153 | spec: 154 | serviceAccountName: mcp-service-account 155 | containers: 156 | - name: server 157 | image: blankcut/kubernetes-mcp-server:latest 158 | imagePullPolicy: Always 159 | ports: 160 | - containerPort: 8080 161 | env: 162 | - name: CLAUDE_API_KEY 163 | valueFrom: 164 | secretKeyRef: 165 | name: mcp-secrets 166 | key: claude-api-key 167 | - name: ARGOCD_USERNAME 168 | valueFrom: 169 | secretKeyRef: 170 | name: mcp-secrets 171 | key: argocd-username 172 | optional: true 173 | - name: ARGOCD_PASSWORD 174 | valueFrom: 175 | secretKeyRef: 176 | name: mcp-secrets 177 | key: argocd-password 178 | optional: true 179 | - name: GITLAB_AUTH_TOKEN 180 | valueFrom: 181 | secretKeyRef: 182 | name: mcp-secrets 183 | key: gitlab-token 184 | optional: true 185 | - name: API_KEY 186 | valueFrom: 187 | secretKeyRef: 188 | name: mcp-secrets 189 | key: api-key 190 | volumeMounts: 191 | - name: config 192 | mountPath: /app/config.yaml 193 | subPath: config.yaml 194 | volumes: 195 | - name: config 196 | configMap: 197 | name: mcp-config 198 | --- 199 | apiVersion: v1 200 | kind: Service 201 | metadata: 202 | name: kubernetes-mcp-server 203 | namespace: mcp-system 204 | spec: 205 | selector: 206 | app: kubernetes-mcp-server 207 | ports: 208 | - port: 80 209 | targetPort: 8080 210 | type: ClusterIP 211 | --- 212 | apiVersion: v1 213 | kind: ServiceAccount 214 | metadata: 215 | name: mcp-service-account 216 | namespace: mcp-system 217 | --- 218 | apiVersion: rbac.authorization.k8s.io/v1 219 | kind: ClusterRole 220 | metadata: 221 | name: mcp-cluster-role 222 | rules: 223 | - apiGroups: [""] 224 | resources: ["pods", "services", "events", "configmaps", "secrets", "namespaces", "nodes"] 225 | verbs: ["get", "list", "watch"] 226 | - apiGroups: ["apps"] 227 | resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] 228 | verbs: ["get", "list", "watch"] 229 | - apiGroups: ["batch"] 230 | resources: ["jobs", "cronjobs"] 231 | verbs: ["get", "list", "watch"] 232 | - apiGroups: ["networking.k8s.io"] 233 | resources: ["ingresses"] 234 | verbs: ["get", "list", "watch"] 235 | --- 236 | apiVersion: rbac.authorization.k8s.io/v1 237 | kind: ClusterRoleBinding 238 | metadata: 239 | name: mcp-role-binding 240 | subjects: 241 | - kind: ServiceAccount 242 | name: mcp-service-account 243 | namespace: mcp-system 244 | roleRef: 245 | kind: ClusterRole 246 | name: mcp-cluster-role 247 | apiGroup: rbac.authorization.k8s.io 248 | ``` 249 | 250 | Apply the configuration: 251 | 252 | ```bash 253 | kubectl apply -f deployment.yaml 254 | ``` 255 | 256 | ### Step 5: Access the Server 257 | 258 | Create an Ingress or port-forward to access the server: 259 | 260 | ```bash 261 | kubectl port-forward -n mcp-system svc/kubernetes-mcp-server 8080:80 262 | ``` 263 | 264 | ## Helm Chart 265 | 266 | For Kubernetes users, the Helm chart provides the easiest installation method. 267 | 268 | ### Step 1: Add the Helm Repository 269 | 270 | ```bash 271 | helm repo add blankcut https://blankcut.github.io/helm-charts 272 | helm repo update 273 | ``` 274 | 275 | ### Step 2: Configure Values 276 | 277 | Create a `values.yaml` file: 278 | 279 | ```yaml 280 | image: 281 | repository: blankcut/kubernetes-mcp-server 282 | tag: latest 283 | 284 | config: 285 | server: 286 | address: ":8080" 287 | kubernetes: 288 | inCluster: true 289 | defaultNamespace: "default" 290 | argocd: 291 | url: "https://argocd.example.com" 292 | gitlab: 293 | url: "https://gitlab.com" 294 | claude: 295 | modelID: "claude-3-haiku-20240307" 296 | 297 | secrets: 298 | claude: 299 | apiKey: "your_claude_api_key" 300 | argocd: 301 | username: "your_argocd_username" 302 | password: "your_argocd_password" 303 | gitlab: 304 | authToken: "your_gitlab_token" 305 | 306 | service: 307 | type: ClusterIP 308 | 309 | ingress: 310 | enabled: false 311 | # Uncomment to enable ingress 312 | # hosts: 313 | # - host: mcp.example.com 314 | # paths: 315 | # - path: / 316 | # pathType: Prefix 317 | ``` 318 | 319 | ### Step 3: Install the Chart 320 | 321 | ```bash 322 | helm install kubernetes-mcp-server blankcut/kubernetes-claude-mcp -f values.yaml -n mcp-system 323 | ``` 324 | 325 | ### Step 4: Verify the Installation 326 | 327 | ```bash 328 | kubectl get pods -n mcp-system 329 | ``` 330 | 331 | ## Manual Binary 332 | 333 | For environments where Docker or Kubernetes is not available, you can run the binary directly. 334 | 335 | ### Step 1: Download the Latest Release 336 | 337 | Visit the [Releases page](https://github.com/blankcut/kubernetes-mcp-server/releases) and download the appropriate binary for your platform. 338 | 339 | ### Step 2: Make the Binary Executable 340 | 341 | ```bash 342 | chmod +x mcp-server 343 | ``` 344 | 345 | ### Step 3: Create Configuration File 346 | 347 | Create a `config.yaml` file in the same directory: 348 | 349 | ```yaml 350 | server: 351 | address: ":8080" 352 | readTimeout: 30 353 | writeTimeout: 60 354 | auth: 355 | apiKey: "your_api_key_for_server_access" 356 | 357 | kubernetes: 358 | kubeconfig: "/path/to/.kube/config" # Path to your kubeconfig file 359 | inCluster: false 360 | defaultContext: "" 361 | defaultNamespace: "default" 362 | 363 | argocd: 364 | url: "https://argocd.example.com" 365 | username: "your_argocd_username" 366 | password: "your_argocd_password" 367 | insecure: true 368 | 369 | gitlab: 370 | url: "https://gitlab.com" 371 | authToken: "your_gitlab_token" 372 | apiVersion: "v4" 373 | projectPath: "" 374 | 375 | claude: 376 | apiKey: "your_claude_api_key" 377 | baseURL: "https://api.anthropic.com" 378 | modelID: "claude-3-haiku-20240307" 379 | maxTokens: 4096 380 | temperature: 0.7 381 | ``` 382 | 383 | ### Step 4: Run the Server 384 | 385 | ```bash 386 | export CLAUDE_API_KEY=your_claude_api_key 387 | export API_KEY=your_api_key_for_server 388 | ./mcp-server --config config.yaml 389 | ``` 390 | 391 | ## Verifying the Installation 392 | 393 | To verify your installation is working correctly: 394 | 395 | 1. Check the health endpoint: 396 | 397 | ```bash 398 | curl http://localhost:8080/api/v1/health 399 | ``` 400 | 401 | 2. List Kubernetes namespaces: 402 | 403 | ```bash 404 | curl -H "X-API-Key: your_api_key" http://localhost:8080/api/v1/namespaces 405 | ``` 406 | 407 | 3. Test a resource query: 408 | 409 | ```bash 410 | curl -X POST \ 411 | -H "Content-Type: application/json" \ 412 | -H "X-API-Key: your_api_key" \ 413 | -d '{ 414 | "action": "queryResource", 415 | "resource": "pod", 416 | "name": "example-pod", 417 | "namespace": "default", 418 | "query": "Is this pod healthy?" 419 | }' \ 420 | http://localhost:8080/api/v1/mcp/resource 421 | ``` 422 | 423 | ## Security Considerations 424 | 425 | When deploying Kubernetes Claude MCP, consider the following security best practices: 426 | 427 | 1. **API Access**: Use a strong API key and restrict access to the server. 428 | 2. **Kubernetes Permissions**: Use a service account with the minimum required permissions. 429 | 3. **Secrets Management**: Store credentials in Kubernetes Secrets or a secure vault. 430 | 4. **Network Isolation**: Consider network policies to limit access to the server. 431 | 5. **TLS**: Use TLS to encrypt connections to the server. 432 | 433 | For more security recommendations, see the [Security Best Practices](/docs/security-best-practices) guide. 434 | 435 | ## Troubleshooting 436 | 437 | If you encounter issues during installation, check: 438 | 439 | 1. **Logs**: View server logs for error messages 440 | ```bash 441 | # For Docker Compose 442 | docker-compose logs 443 | 444 | # For Kubernetes 445 | kubectl logs -n mcp-system deployment/kubernetes-mcp-server 446 | ``` 447 | 448 | 2. **Configuration**: Verify your `config.yaml` has the correct settings 449 | 3. **Connectivity**: Ensure the server can connect to Kubernetes, ArgoCD, and GitLab 450 | 4. **API Key**: Verify you're using the correct API key in requests 451 | 452 | For more troubleshooting tips, see the [Troubleshooting](/docs/troubleshooting-resources) guide. 453 | 454 | ## Next Steps 455 | 456 | After successful installation, continue with: 457 | 458 | - [Configuration Guide](/docs/configuration) - Configure the server for your environment 459 | - [API Reference](/docs/api-overview) - Explore the API endpoints 460 | - [Examples](/docs/examples/basic-usage) - See examples of common use cases ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/auth/credentials.go: -------------------------------------------------------------------------------- ```go 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "sync" 8 | "time" 9 | 10 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/config" 11 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" 12 | ) 13 | 14 | // ServiceType represents the type of service requiring credentials 15 | type ServiceType string 16 | 17 | const ( 18 | ServiceKubernetes ServiceType = "kubernetes" 19 | ServiceArgoCD ServiceType = "argocd" 20 | ServiceGitLab ServiceType = "gitlab" 21 | ServiceClaude ServiceType = "claude" 22 | ) 23 | 24 | // Credentials stores authentication information for various services 25 | type Credentials struct { 26 | // API tokens, oauth tokens, etc. 27 | Token string 28 | APIKey string 29 | Username string 30 | Password string 31 | Certificate []byte 32 | PrivateKey []byte 33 | ExpiresAt time.Time 34 | } 35 | 36 | // IsExpired checks if the credentials are expired 37 | func (c *Credentials) IsExpired() bool { 38 | // If no expiration time is set, we'll assume credentials don't expire 39 | if c.ExpiresAt.IsZero() { 40 | return false 41 | } 42 | 43 | // Check if current time is past the expiration time 44 | return time.Now().After(c.ExpiresAt) 45 | } 46 | 47 | // CredentialProvider manages credentials for various services 48 | type CredentialProvider struct { 49 | mu sync.RWMutex 50 | credentials map[ServiceType]*Credentials 51 | config *config.Config 52 | logger *logging.Logger 53 | secretsManager *SecretsManager 54 | vaultManager *VaultManager 55 | } 56 | 57 | // NewCredentialProvider creates a new credential provider 58 | func NewCredentialProvider(cfg *config.Config) *CredentialProvider { 59 | logger := logging.NewLogger().Named("auth") 60 | 61 | return &CredentialProvider{ 62 | credentials: make(map[ServiceType]*Credentials), 63 | config: cfg, 64 | logger: logger, 65 | secretsManager: NewSecretsManager(logger), 66 | vaultManager: NewVaultManager(logger), 67 | } 68 | } 69 | 70 | // LoadCredentials loads all service credentials based on configuration 71 | func (p *CredentialProvider) LoadCredentials(ctx context.Context) error { 72 | // Load credentials for each service type based on config 73 | if err := p.loadKubernetesCredentials(ctx); err != nil { 74 | return fmt.Errorf("failed to load Kubernetes credentials: %w", err) 75 | } 76 | 77 | if err := p.loadArgoCDCredentials(ctx); err != nil { 78 | return fmt.Errorf("failed to load ArgoCD credentials: %w", err) 79 | } 80 | 81 | if err := p.loadGitLabCredentials(ctx); err != nil { 82 | return fmt.Errorf("failed to load GitLab credentials: %w", err) 83 | } 84 | 85 | if err := p.loadClaudeCredentials(ctx); err != nil { 86 | return fmt.Errorf("failed to load Claude credentials: %w", err) 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // GetCredentials returns credentials for the specified service 93 | func (p *CredentialProvider) GetCredentials(serviceType ServiceType) (*Credentials, error) { 94 | p.mu.RLock() 95 | defer p.mu.RUnlock() 96 | 97 | creds, ok := p.credentials[serviceType] 98 | if !ok { 99 | return nil, fmt.Errorf("credentials not found for service: %s", serviceType) 100 | } 101 | 102 | // Check if credentials are expired and need refresh 103 | if creds.IsExpired() { 104 | p.mu.RUnlock() // Release read lock 105 | 106 | // Acquire write lock for refresh 107 | p.mu.Lock() 108 | defer p.mu.Unlock() 109 | 110 | // Check again in case another goroutine refreshed while we were waiting 111 | if creds.IsExpired() { 112 | p.logger.Info("Refreshing expired credentials", "serviceType", serviceType) 113 | if err := p.RefreshCredentials(context.Background(), serviceType); err != nil { 114 | return nil, fmt.Errorf("failed to refresh expired credentials: %w", err) 115 | } 116 | creds = p.credentials[serviceType] 117 | } 118 | } 119 | 120 | return creds, nil 121 | } 122 | 123 | // loadKubernetesCredentials loads Kubernetes authentication credentials 124 | func (p *CredentialProvider) loadKubernetesCredentials(ctx context.Context) error { 125 | p.mu.Lock() 126 | defer p.mu.Unlock() 127 | 128 | // For Kubernetes, we primarily rely on kubeconfig or in-cluster config 129 | // We won't need to store explicit credentials 130 | p.credentials[ServiceKubernetes] = &Credentials{} 131 | return nil 132 | } 133 | 134 | // loadArgoCDCredentials loads ArgoCD authentication credentials 135 | func (p *CredentialProvider) loadArgoCDCredentials(ctx context.Context) error { 136 | p.mu.Lock() 137 | defer p.mu.Unlock() 138 | 139 | // Try to load from secrets manager if available 140 | if p.secretsManager != nil && p.secretsManager.IsAvailable() { 141 | creds, err := p.secretsManager.GetCredentials(ctx, "argocd") 142 | if err == nil && creds != nil { 143 | p.credentials[ServiceArgoCD] = creds 144 | p.logger.Info("Loaded ArgoCD credentials from secrets manager") 145 | return nil 146 | } 147 | } 148 | 149 | // Try to load from vault if available 150 | if p.vaultManager != nil && p.vaultManager.IsAvailable() { 151 | creds, err := p.vaultManager.GetCredentials(ctx, "argocd") 152 | if err == nil && creds != nil { 153 | p.credentials[ServiceArgoCD] = creds 154 | p.logger.Info("Loaded ArgoCD credentials from vault") 155 | return nil 156 | } 157 | } 158 | 159 | // Primary source: Environment variables 160 | token := os.Getenv("ARGOCD_AUTH_TOKEN") 161 | if token != "" { 162 | p.credentials[ServiceArgoCD] = &Credentials{ 163 | Token: token, 164 | } 165 | p.logger.Info("Loaded ArgoCD credentials from environment") 166 | return nil 167 | } 168 | 169 | // Secondary source: Config file 170 | if p.config.ArgoCD.AuthToken != "" { 171 | p.credentials[ServiceArgoCD] = &Credentials{ 172 | Token: p.config.ArgoCD.AuthToken, 173 | } 174 | p.logger.Info("Loaded ArgoCD credentials from config file") 175 | return nil 176 | } 177 | 178 | // Tertiary source: Username/password... 179 | username := os.Getenv("ARGOCD_USERNAME") 180 | password := os.Getenv("ARGOCD_PASSWORD") 181 | if username != "" && password != "" { 182 | p.credentials[ServiceArgoCD] = &Credentials{ 183 | Username: username, 184 | Password: password, 185 | } 186 | p.logger.Info("Loaded ArgoCD username/password from environment") 187 | return nil 188 | } 189 | 190 | // Final fallback to config 191 | if p.config.ArgoCD.Username != "" && p.config.ArgoCD.Password != "" { 192 | p.credentials[ServiceArgoCD] = &Credentials{ 193 | Username: p.config.ArgoCD.Username, 194 | Password: p.config.ArgoCD.Password, 195 | } 196 | p.logger.Info("Loaded ArgoCD username/password from config file") 197 | return nil 198 | } 199 | 200 | p.logger.Warn("No ArgoCD credentials found, continuing without them") 201 | // We don't want to fail if ArgoCD credentials are not found 202 | // since ArgoCD integration is optional 203 | p.credentials[ServiceArgoCD] = &Credentials{} 204 | return nil 205 | } 206 | 207 | // loadGitLabCredentials loads GitLab authentication credentials 208 | func (p *CredentialProvider) loadGitLabCredentials(ctx context.Context) error { 209 | p.mu.Lock() 210 | defer p.mu.Unlock() 211 | 212 | // Try to load from secrets manager if available 213 | if p.secretsManager != nil && p.secretsManager.IsAvailable() { 214 | creds, err := p.secretsManager.GetCredentials(ctx, "gitlab") 215 | if err == nil && creds != nil { 216 | p.credentials[ServiceGitLab] = creds 217 | p.logger.Info("Loaded GitLab credentials from secrets manager") 218 | return nil 219 | } 220 | } 221 | 222 | // Try to load from vault if available 223 | if p.vaultManager != nil && p.vaultManager.IsAvailable() { 224 | creds, err := p.vaultManager.GetCredentials(ctx, "gitlab") 225 | if err == nil && creds != nil { 226 | p.credentials[ServiceGitLab] = creds 227 | p.logger.Info("Loaded GitLab credentials from vault") 228 | return nil 229 | } 230 | } 231 | 232 | // Primary source: Environment variables 233 | token := os.Getenv("GITLAB_AUTH_TOKEN") 234 | if token != "" { 235 | p.credentials[ServiceGitLab] = &Credentials{ 236 | Token: token, 237 | } 238 | p.logger.Info("Loaded GitLab credentials from environment") 239 | return nil 240 | } 241 | 242 | // Secondary source: Config file 243 | if p.config.GitLab.AuthToken != "" { 244 | p.credentials[ServiceGitLab] = &Credentials{ 245 | Token: p.config.GitLab.AuthToken, 246 | } 247 | p.logger.Info("Loaded GitLab credentials from config file") 248 | return nil 249 | } 250 | 251 | p.logger.Warn("No GitLab credentials found, continuing without them") 252 | // We don't want to fail if GitLab credentials are not found 253 | // since GitLab integration is optional 254 | p.credentials[ServiceGitLab] = &Credentials{} 255 | return nil 256 | } 257 | 258 | // loadClaudeCredentials loads Claude API credentials 259 | func (p *CredentialProvider) loadClaudeCredentials(ctx context.Context) error { 260 | p.mu.Lock() 261 | defer p.mu.Unlock() 262 | 263 | // Try to load from secrets manager if available 264 | if p.secretsManager != nil && p.secretsManager.IsAvailable() { 265 | creds, err := p.secretsManager.GetCredentials(ctx, "claude") 266 | if err == nil && creds != nil { 267 | p.credentials[ServiceClaude] = creds 268 | p.logger.Info("Loaded Claude credentials from secrets manager") 269 | return nil 270 | } 271 | } 272 | 273 | // Try to load from vault if available 274 | if p.vaultManager != nil && p.vaultManager.IsAvailable() { 275 | creds, err := p.vaultManager.GetCredentials(ctx, "claude") 276 | if err == nil && creds != nil { 277 | p.credentials[ServiceClaude] = creds 278 | p.logger.Info("Loaded Claude credentials from vault") 279 | return nil 280 | } 281 | } 282 | 283 | // Primary source: Environment variables 284 | apiKey := os.Getenv("CLAUDE_API_KEY") 285 | if apiKey != "" { 286 | p.credentials[ServiceClaude] = &Credentials{ 287 | APIKey: apiKey, 288 | } 289 | p.logger.Info("Loaded Claude credentials from environment") 290 | return nil 291 | } 292 | 293 | // Secondary source: Config file 294 | if p.config.Claude.APIKey != "" { 295 | p.credentials[ServiceClaude] = &Credentials{ 296 | APIKey: p.config.Claude.APIKey, 297 | } 298 | p.logger.Info("Loaded Claude credentials from config file") 299 | return nil 300 | } 301 | 302 | p.logger.Warn("No Claude API key found") 303 | return fmt.Errorf("no Claude API key found") 304 | } 305 | 306 | // RefreshCredentials refreshes credentials for a specific service (for tokens that expire) 307 | func (p *CredentialProvider) RefreshCredentials(ctx context.Context, serviceType ServiceType) error { 308 | // Implement credential refresh logic based on service type 309 | switch serviceType { 310 | case ServiceArgoCD: 311 | return p.refreshArgoCDToken(ctx) 312 | default: 313 | p.logger.Debug("No refresh needed for service", "serviceType", serviceType) 314 | return nil // No refresh needed for other services 315 | } 316 | } 317 | 318 | // refreshArgoCDToken refreshes the ArgoCD token if using username/password auth 319 | func (p *CredentialProvider) refreshArgoCDToken(ctx context.Context) error { 320 | p.mu.Lock() 321 | defer p.mu.Unlock() 322 | 323 | creds, ok := p.credentials[ServiceArgoCD] 324 | if !ok { 325 | return fmt.Errorf("ArgoCD credentials not found") 326 | } 327 | 328 | // If using token authentication and it's not expired, no refresh needed 329 | if creds.Token != "" && !creds.IsExpired() { 330 | return nil 331 | } 332 | 333 | // If using username/password, we would implement logic to get a new token 334 | if creds.Username != "" && creds.Password != "" { 335 | p.logger.Info("Refreshing ArgoCD token using username/password") 336 | p.logger.Info("Successfully refreshed ArgoCD token") 337 | return nil 338 | } 339 | 340 | return fmt.Errorf("unable to refresh ArgoCD token: invalid credential type") 341 | } 342 | 343 | // UpdateArgoToken updates the ArgoCD token 344 | func (p *CredentialProvider) UpdateArgoToken(ctx context.Context, token string) { 345 | p.mu.Lock() 346 | defer p.mu.Unlock() 347 | 348 | if creds, ok := p.credentials[ServiceArgoCD]; ok { 349 | creds.Token = token 350 | creds.ExpiresAt = time.Now().Add(24 * time.Hour) 351 | p.logger.Info("Updated ArgoCD token") 352 | } else { 353 | p.credentials[ServiceArgoCD] = &Credentials{ 354 | Token: token, 355 | ExpiresAt: time.Now().Add(24 * time.Hour), 356 | } 357 | p.logger.Info("Created new ArgoCD token") 358 | } 359 | } ``` -------------------------------------------------------------------------------- /docs/src/content/docs/configuration.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Configuration Guide 3 | description: Learn how to configure and customize the Kubernetes Claude MCP server to suit your needs and environment. 4 | date: 2025-03-01 5 | order: 4 6 | tags: ['configuration', 'setup'] 7 | --- 8 | 9 | # Configuration Guide 10 | 11 | This guide explains how to configure Kubernetes Claude MCP to work optimally in your environment. The server is highly configurable, allowing you to customize its behavior and integrations. 12 | 13 | ## Configuration File 14 | 15 | Kubernetes Claude MCP is primarily configured using a YAML file (`config.yaml`). This file contains settings for the server, Kubernetes connection, ArgoCD integration, GitLab integration, and Claude AI. 16 | 17 | Here's a complete example of the configuration file with explanations: 18 | 19 | ```yaml 20 | # Server configuration 21 | server: 22 | # Address to bind the server on (host:port) 23 | address: ":8080" 24 | # Read timeout in seconds 25 | readTimeout: 30 26 | # Write timeout in seconds 27 | writeTimeout: 60 28 | # Authentication settings 29 | auth: 30 | # API key for authenticating requests 31 | apiKey: "your_api_key_here" 32 | 33 | # Kubernetes connection settings 34 | kubernetes: 35 | # Path to kubeconfig file (leave empty for in-cluster) 36 | kubeconfig: "" 37 | # Whether to use in-cluster config 38 | inCluster: false 39 | # Default Kubernetes context (leave empty for current) 40 | defaultContext: "" 41 | # Default namespace 42 | defaultNamespace: "default" 43 | 44 | # ArgoCD integration settings 45 | argocd: 46 | # ArgoCD server URL 47 | url: "https://argocd.example.com" 48 | # ArgoCD auth token (optional if using username/password) 49 | authToken: "" 50 | # ArgoCD username (optional if using token) 51 | username: "admin" 52 | # ArgoCD password (optional if using token) 53 | password: "password" 54 | # Whether to allow insecure connections 55 | insecure: false 56 | 57 | # GitLab integration settings 58 | gitlab: 59 | # GitLab server URL 60 | url: "https://gitlab.com" 61 | # GitLab personal access token 62 | authToken: "your_gitlab_token" 63 | # GitLab API version 64 | apiVersion: "v4" 65 | # Default project path 66 | projectPath: "namespace/project" 67 | 68 | # Claude AI settings 69 | claude: 70 | # Claude API key 71 | apiKey: "your_claude_api_key" 72 | # Claude API base URL 73 | baseURL: "https://api.anthropic.com" 74 | # Claude model ID 75 | modelID: "claude-3-haiku-20240307" 76 | # Maximum tokens for Claude responses 77 | maxTokens: 4096 78 | # Temperature for Claude responses (0.0-1.0) 79 | temperature: 0.7 80 | ``` 81 | 82 | ## Configuration Options 83 | 84 | ### Server Configuration 85 | 86 | | Option | Description | Default | 87 | |--------|-------------|---------| 88 | | `address` | Host and port to bind the server (":8080" means all interfaces, port 8080) | ":8080" | 89 | | `readTimeout` | HTTP read timeout in seconds | 30 | 90 | | `writeTimeout` | HTTP write timeout in seconds | 60 | 91 | | `auth.apiKey` | API key for authenticating requests | - | 92 | 93 | ### Kubernetes Configuration 94 | 95 | | Option | Description | Default | 96 | |--------|-------------|---------| 97 | | `kubeconfig` | Path to kubeconfig file | "" (auto-detect) | 98 | | `inCluster` | Whether to use in-cluster configuration | false | 99 | | `defaultContext` | Default Kubernetes context | "" (current context) | 100 | | `defaultNamespace` | Default namespace for operations | "default" | 101 | 102 | ### ArgoCD Configuration 103 | 104 | | Option | Description | Default | 105 | |--------|-------------|---------| 106 | | `url` | ArgoCD server URL | - | 107 | | `authToken` | ArgoCD auth token | "" | 108 | | `username` | ArgoCD username | "" | 109 | | `password` | ArgoCD password | "" | 110 | | `insecure` | Allow insecure connections to ArgoCD | false | 111 | 112 | ### GitLab Configuration 113 | 114 | | Option | Description | Default | 115 | |--------|-------------|---------| 116 | | `url` | GitLab server URL | "https://gitlab.com" | 117 | | `authToken` | GitLab personal access token | - | 118 | | `apiVersion` | GitLab API version | "v4" | 119 | | `projectPath` | Default project path | "" | 120 | 121 | ### Claude Configuration 122 | 123 | | Option | Description | Default | 124 | |--------|-------------|---------| 125 | | `apiKey` | Claude API key | - | 126 | | `baseURL` | Claude API base URL | "https://api.anthropic.com" | 127 | | `modelID` | Claude model ID | "claude-3-haiku-20240307" | 128 | | `maxTokens` | Maximum tokens for response | 4096 | 129 | | `temperature` | Temperature for responses (0.0-1.0) | 0.7 | 130 | 131 | ## Environment Variables 132 | 133 | In addition to the configuration file, you can use environment variables to override any configuration option. This is especially useful for secrets and credentials. 134 | 135 | Environment variables follow this pattern: 136 | 137 | - For server options: `SERVER_OPTION_NAME` 138 | - For Kubernetes options: `KUBERNETES_OPTION_NAME` 139 | - For ArgoCD options: `ARGOCD_OPTION_NAME` 140 | - For GitLab options: `GITLAB_OPTION_NAME` 141 | - For Claude options: `CLAUDE_OPTION_NAME` 142 | 143 | Common examples: 144 | 145 | ```bash 146 | # API keys 147 | export CLAUDE_API_KEY=your_claude_api_key 148 | export API_KEY=your_api_key_for_server 149 | 150 | # ArgoCD credentials 151 | export ARGOCD_USERNAME=your_argocd_username 152 | export ARGOCD_PASSWORD=your_argocd_password 153 | 154 | # GitLab credentials 155 | export GITLAB_AUTH_TOKEN=your_gitlab_token 156 | ``` 157 | 158 | ## Variable Interpolation 159 | 160 | The configuration file supports variable interpolation, allowing you to reference environment variables in your config. This is useful for injecting secrets: 161 | 162 | ```yaml 163 | server: 164 | auth: 165 | apiKey: "${API_KEY}" 166 | 167 | claude: 168 | apiKey: "${CLAUDE_API_KEY}" 169 | ``` 170 | 171 | ## Configuration Hierarchy 172 | 173 | The server reads configuration in the following order (later overrides earlier): 174 | 175 | 1. Default values 176 | 2. Configuration file 177 | 3. Environment variables 178 | 179 | This allows you to have a base configuration file and override specific settings with environment variables. 180 | 181 | ## ArgoCD Integration 182 | 183 | ### Authentication Methods 184 | 185 | There are two ways to authenticate with ArgoCD: 186 | 187 | 1. **Token-based authentication**: Provide an auth token in `argocd.authToken`. 188 | 2. **Username/password authentication**: Provide username and password in `argocd.username` and `argocd.password`. 189 | 190 | For production environments, token-based authentication is recommended for security. 191 | 192 | ### Insecure Mode 193 | 194 | If you're using a self-signed certificate for ArgoCD, you can set `argocd.insecure` to `true` to skip certificate validation. However, this is not recommended for production environments. 195 | 196 | ## GitLab Integration 197 | 198 | ### Personal Access Token 199 | 200 | To integrate with GitLab, you need a personal access token with the following scopes: 201 | 202 | - `read_api` - For accessing repository information 203 | - `read_repository` - For accessing repository content 204 | - `read_registry` - For accessing container registry (if needed) 205 | 206 | ### Self-hosted GitLab 207 | 208 | If you're using a self-hosted GitLab instance, set the `gitlab.url` to your GitLab URL: 209 | 210 | ```yaml 211 | gitlab: 212 | url: "https://gitlab.your-company.com" 213 | # Other GitLab settings... 214 | ``` 215 | 216 | ## Claude AI Configuration 217 | 218 | ### Model Selection 219 | 220 | Kubernetes Claude MCP supports different Claude model variants. The default is `claude-3-haiku-20240307`, but you can choose others based on your needs: 221 | 222 | - `claude-3-opus-20240229` - Most capable model, best for complex analysis 223 | - `claude-3-sonnet-20240229` - Balanced performance and speed 224 | - `claude-3-haiku-20240307` - Fastest model, suitable for most use cases 225 | 226 | ### Response Parameters 227 | 228 | You can adjust two parameters that affect Claude's responses: 229 | 230 | 1. `maxTokens` - Maximum number of tokens in the response (1-4096) 231 | 2. `temperature` - Controls randomness in responses (0.0-1.0) 232 | - Lower values (e.g., 0.3) make responses more deterministic 233 | - Higher values (e.g., 0.7) make responses more creative 234 | 235 | For troubleshooting and analysis, a temperature of 0.3-0.5 is recommended. 236 | 237 | ## Advanced Configuration 238 | 239 | ### Running Behind a Proxy 240 | 241 | If the server needs to connect to external services through a proxy, set the standard HTTP proxy environment variables: 242 | 243 | ```bash 244 | export HTTP_PROXY=http://proxy.example.com:8080 245 | export HTTPS_PROXY=http://proxy.example.com:8080 246 | export NO_PROXY=localhost,127.0.0.1,.cluster.local 247 | ``` 248 | 249 | ### TLS Configuration 250 | 251 | For production deployments, it's recommended to use TLS. This is typically handled by your ingress controller, load balancer, or API gateway. 252 | 253 | If you need to terminate TLS at the server (not recommended for production), you can use a reverse proxy like Nginx or Traefik. 254 | 255 | ### Logging Configuration 256 | 257 | The logging level can be controlled with the `LOG_LEVEL` environment variable: 258 | 259 | ```bash 260 | export LOG_LEVEL=debug # debug, info, warn, error 261 | ``` 262 | 263 | For production, `info` is recommended. Use `debug` only for troubleshooting. 264 | 265 | ## Configuration Examples 266 | 267 | ### Minimal Configuration 268 | 269 | ```yaml 270 | server: 271 | address: ":8080" 272 | auth: 273 | apiKey: "your_api_key_here" 274 | 275 | kubernetes: 276 | inCluster: false 277 | 278 | claude: 279 | apiKey: "your_claude_api_key" 280 | modelID: "claude-3-haiku-20240307" 281 | ``` 282 | 283 | ### Production Kubernetes Configuration 284 | 285 | ```yaml 286 | server: 287 | address: ":8080" 288 | readTimeout: 60 289 | writeTimeout: 120 290 | auth: 291 | apiKey: "${API_KEY}" 292 | 293 | kubernetes: 294 | inCluster: true 295 | defaultNamespace: "default" 296 | 297 | argocd: 298 | url: "https://argocd.example.com" 299 | authToken: "${ARGOCD_AUTH_TOKEN}" 300 | insecure: false 301 | 302 | gitlab: 303 | url: "https://gitlab.example.com" 304 | authToken: "${GITLAB_AUTH_TOKEN}" 305 | apiVersion: "v4" 306 | 307 | claude: 308 | apiKey: "${CLAUDE_API_KEY}" 309 | baseURL: "https://api.anthropic.com" 310 | modelID: "claude-3-haiku-20240307" 311 | maxTokens: 4096 312 | temperature: 0.5 313 | ``` 314 | 315 | ## Troubleshooting Configuration 316 | 317 | If you encounter issues with your configuration: 318 | 319 | 1. Check that all required fields are set correctly 320 | 2. Verify that environment variables are correctly set and accessible to the server 321 | 3. Test connectivity to external services (Kubernetes, ArgoCD, GitLab) 322 | 4. Check the server logs for error messages 323 | 5. Ensure your Claude API key is valid and has sufficient quota 324 | 325 | ### Common Issues 326 | 327 | #### "Failed to create Kubernetes client" 328 | 329 | This usually indicates an issue with the Kubernetes configuration: 330 | 331 | - Check if the kubeconfig file exists and is accessible 332 | - Verify the permissions of the kubeconfig file 333 | - For in-cluster config, ensure the pod has the proper service account 334 | 335 | #### "Failed to connect to ArgoCD" 336 | 337 | ArgoCD connectivity issues are typically related to: 338 | 339 | - Incorrect URL or credentials 340 | - Network connectivity issues 341 | - Certificate validation (if `insecure: false`) 342 | 343 | Try using the `--log-level=debug` flag to get more details: 344 | 345 | ```bash 346 | LOG_LEVEL=debug ./mcp-server --config config.yaml 347 | ``` 348 | 349 | #### "Failed to connect to GitLab" 350 | 351 | GitLab connectivity issues may be due to: 352 | 353 | - Invalid personal access token 354 | - Insufficient permissions for the token 355 | - Network connectivity issues 356 | 357 | #### "Claude API error" 358 | 359 | Claude API errors usually indicate: 360 | 361 | - Invalid API key 362 | - Rate limiting or quota issues 363 | - Incorrect model ID 364 | 365 | ## Updating Configuration 366 | 367 | You can update the configuration without restarting the server by sending a SIGHUP signal: 368 | 369 | ```bash 370 | # Find the process ID 371 | ps aux | grep mcp-server 372 | 373 | # Send SIGHUP signal 374 | kill -HUP <process_id> 375 | ``` 376 | 377 | For containerized deployments, you'll need to restart the container to apply configuration changes. 378 | 379 | ## Next Steps 380 | 381 | Now that you've configured Kubernetes Claude MCP, you can: 382 | 383 | - [Explore the API](/docs/api-overview) to learn how to interact with the server 384 | - [Try some examples](/docs/examples/basic-usage) to see common use cases 385 | - [Learn about troubleshooting](/docs/troubleshooting-resources) to diagnose issues in your cluster ``` -------------------------------------------------------------------------------- /docs/src/content/docs/troubleshooting-resources.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Troubleshooting Resources 3 | description: Learn how to use Kubernetes Claude MCP to diagnose and solve problems with your Kubernetes resources and applications. 4 | date: 2025-03-01 5 | order: 6 6 | tags: ['troubleshooting', 'guides'] 7 | --- 8 | 9 | # Troubleshooting Resources 10 | 11 | Kubernetes Claude MCP is a powerful tool for diagnosing and resolving issues in your Kubernetes environment. This guide will walk you through common troubleshooting scenarios and how to use the MCP server to address them. 12 | 13 | ## Getting Started with Troubleshooting 14 | 15 | The `/api/v1/mcp/troubleshoot` endpoint is specifically designed for troubleshooting. It automatically: 16 | 17 | 1. Collects all relevant information about a resource 18 | 2. Detects common issues and their severity 19 | 3. Correlates information across systems (Kubernetes, ArgoCD, GitLab) 20 | 4. Generates recommendations for fixing the issues 21 | 5. Provides Claude AI-powered analysis of the problems 22 | 23 | ## Troubleshooting Common Resource Types 24 | 25 | ### Troubleshooting Pods 26 | 27 | Pods are often the first place to look when troubleshooting application issues. 28 | 29 | **Example Request:** 30 | 31 | ```bash 32 | curl -X POST \ 33 | -H "Content-Type: application/json" \ 34 | -H "X-API-Key: your_api_key" \ 35 | -d '{ 36 | "resource": "pod", 37 | "name": "my-app-pod", 38 | "namespace": "default", 39 | "query": "Why is this pod not starting?" 40 | }' \ 41 | http://localhost:8080/api/v1/mcp/troubleshoot 42 | ``` 43 | 44 | **What MCP Detects:** 45 | 46 | - Pod status issues (Pending, CrashLoopBackOff, ImagePullBackOff, etc.) 47 | - Container status and restart counts 48 | - Resource constraints (CPU/memory limits) 49 | - Volume mounting issues 50 | - Init container failures 51 | - Image pull errors 52 | - Scheduling problems 53 | - Events related to the pod 54 | 55 | **Example Troubleshooting Output:** 56 | 57 | ```json 58 | { 59 | "success": true, 60 | "analysis": "The pod 'my-app-pod' is failing to start due to an ImagePullBackOff error. The container runtime is unable to pull the image 'myregistry.com/my-app:v1.2.3' because of authentication issues with the private registry. Looking at the events, there was an 'ErrImagePull' error with the message 'unauthorized: authentication required'...", 61 | "troubleshootResult": { 62 | "issues": [ 63 | { 64 | "title": "Image Pull Error", 65 | "category": "ImagePullError", 66 | "severity": "Error", 67 | "source": "Kubernetes", 68 | "description": "Failed to pull image 'myregistry.com/my-app:v1.2.3': unauthorized: authentication required" 69 | } 70 | ], 71 | "recommendations": [ 72 | "Create or update the ImagePullSecret for the private registry", 73 | "Verify the image name and tag are correct", 74 | "Check that the ServiceAccount has access to the ImagePullSecret" 75 | ] 76 | } 77 | } 78 | ``` 79 | 80 | ### Troubleshooting Deployments 81 | 82 | Deployments manage replica sets and pods, so issues can occur at multiple levels. 83 | 84 | **Example Request:** 85 | 86 | ```bash 87 | curl -X POST \ 88 | -H "Content-Type: application/json" \ 89 | -H "X-API-Key: your_api_key" \ 90 | -d '{ 91 | "resource": "deployment", 92 | "name": "my-app", 93 | "namespace": "default", 94 | "query": "Why are pods not scaling up?" 95 | }' \ 96 | http://localhost:8080/api/v1/mcp/troubleshoot 97 | ``` 98 | 99 | **What MCP Detects:** 100 | 101 | - ReplicaSet creation issues 102 | - Pod scaling issues 103 | - Resource quotas preventing scaling 104 | - Node capacity issues 105 | - Pod disruption budgets 106 | - Deployment strategy issues 107 | - Resource constraints on pods 108 | - Health check configuration issues 109 | 110 | **Example Troubleshooting Output:** 111 | 112 | ```json 113 | { 114 | "success": true, 115 | "analysis": "The deployment 'my-app' is unable to scale up because the pods are requesting more CPU resources than are available in the cluster. The deployment is configured to request 2 CPU cores per pod, but the nodes in your cluster only have 1.8 cores available per node...", 116 | "troubleshootResult": { 117 | "issues": [ 118 | { 119 | "title": "Insufficient CPU Resources", 120 | "category": "ResourceConstraint", 121 | "severity": "Warning", 122 | "source": "Kubernetes", 123 | "description": "Insufficient CPU resources available to schedule pods (requested: 2, available: 1.8)" 124 | } 125 | ], 126 | "recommendations": [ 127 | "Reduce the CPU request in the deployment specification", 128 | "Add more nodes to the cluster or use nodes with more CPU capacity", 129 | "Check if there are any resource quotas preventing the scaling" 130 | ] 131 | } 132 | } 133 | ``` 134 | 135 | ### Troubleshooting Services 136 | 137 | Services provide network connectivity between components, and issues often relate to selector mismatches or port configurations. 138 | 139 | **Example Request:** 140 | 141 | ```bash 142 | curl -X POST \ 143 | -H "Content-Type: application/json" \ 144 | -H "X-API-Key: your_api_key" \ 145 | -d '{ 146 | "resource": "service", 147 | "name": "my-app-service", 148 | "namespace": "default", 149 | "query": "Why can't I connect to this service?" 150 | }' \ 151 | http://localhost:8080/api/v1/mcp/troubleshoot 152 | ``` 153 | 154 | **What MCP Detects:** 155 | 156 | - Selector mismatches between service and pods 157 | - Port configuration issues 158 | - Endpoint availability 159 | - Pod readiness issues 160 | - Network policy restrictions 161 | - Service type misconfigurations 162 | - External name resolution issues (for ExternalName services) 163 | 164 | **Example Troubleshooting Output:** 165 | 166 | ```json 167 | { 168 | "success": true, 169 | "analysis": "The service 'my-app-service' is not working correctly because there are no endpoints being selected. The service uses the selector 'app=my-app,tier=frontend', but examining the pods in the namespace, I can see that the pods have the labels 'app=my-app,tier=web'. The mismatch in the 'tier' label (frontend vs web) is preventing the service from selecting any pods...", 170 | "troubleshootResult": { 171 | "issues": [ 172 | { 173 | "title": "Selector Mismatch", 174 | "category": "ServiceSelectorIssue", 175 | "severity": "Error", 176 | "source": "Kubernetes", 177 | "description": "Service selector 'app=my-app,tier=frontend' doesn't match any pods (pods have 'app=my-app,tier=web')" 178 | } 179 | ], 180 | "recommendations": [ 181 | "Update the service selector to match the actual pod labels: 'app=my-app,tier=web'", 182 | "Alternatively, update the pod labels to match the service selector", 183 | "Verify that pods are in the 'Running' state and passing readiness probes" 184 | ] 185 | } 186 | } 187 | ``` 188 | 189 | ### Troubleshooting Ingresses 190 | 191 | Ingress resources configure external access to services, and issues often relate to hostname mismatches or TLS configuration. 192 | 193 | **Example Request:** 194 | 195 | ```bash 196 | curl -X POST \ 197 | -H "Content-Type: application/json" \ 198 | -H "X-API-Key: your_api_key" \ 199 | -d '{ 200 | "resource": "ingress", 201 | "name": "my-app-ingress", 202 | "namespace": "default", 203 | "query": "Why is this ingress returning 404 errors?" 204 | }' \ 205 | http://localhost:8080/api/v1/mcp/troubleshoot 206 | ``` 207 | 208 | **What MCP Detects:** 209 | 210 | - Backend service existence and configuration 211 | - Path routing rules 212 | - TLS certificate issues 213 | - Ingress controller availability 214 | - Host name configurations 215 | - Annotation misconfigurations 216 | - Service port mappings 217 | 218 | ## Troubleshooting GitOps Resources 219 | 220 | Kubernetes Claude MCP excels at diagnosing issues in GitOps workflows by correlating information between Kubernetes, ArgoCD, and GitLab. 221 | 222 | ### Troubleshooting ArgoCD Applications 223 | 224 | **Example Request:** 225 | 226 | ```bash 227 | curl -X POST \ 228 | -H "Content-Type: application/json" \ 229 | -H "X-API-Key: your_api_key" \ 230 | -d '{ 231 | "resource": "application", 232 | "name": "my-argocd-app", 233 | "namespace": "argocd", 234 | "query": "Why is this application out of sync?" 235 | }' \ 236 | http://localhost:8080/api/v1/mcp/troubleshoot 237 | ``` 238 | 239 | **What MCP Detects:** 240 | 241 | - Sync status issues 242 | - Sync history and recent failures 243 | - Git repository connectivity issues 244 | - Manifest validation errors 245 | - Resource differences between desired and actual state 246 | - Health status issues 247 | - Related Kubernetes resources 248 | 249 | **Example Troubleshooting Output:** 250 | 251 | ```json 252 | { 253 | "success": true, 254 | "analysis": "The ArgoCD application 'my-argocd-app' is out of sync because there are local changes to the Deployment resource that differ from the version in Git. Specifically, someone has manually scaled the deployment from 3 replicas (as defined in Git) to 5 replicas using kubectl...", 255 | "troubleshootResult": { 256 | "issues": [ 257 | { 258 | "title": "Manual Modification", 259 | "category": "SyncIssue", 260 | "severity": "Warning", 261 | "source": "ArgoCD", 262 | "description": "Deployment 'my-app' was manually modified: replicas changed from 3 to 5" 263 | } 264 | ], 265 | "recommendations": [ 266 | "Use 'argocd app sync my-argocd-app' to revert to the state defined in Git", 267 | "Update the Git repository to reflect the desired replica count", 268 | "Enable self-healing in the ArgoCD application to prevent manual modifications" 269 | ] 270 | } 271 | } 272 | ``` 273 | 274 | ### Investigating Commit Impact 275 | 276 | When a deployment fails after a GitLab commit, you can analyze the commit's impact: 277 | 278 | **Example Request:** 279 | 280 | ```bash 281 | curl -X POST \ 282 | -H "Content-Type: application/json" \ 283 | -H "X-API-Key: your_api_key" \ 284 | -d '{ 285 | "projectId": "mygroup/myproject", 286 | "commitSha": "abcdef1234567890", 287 | "query": "How has this commit affected Kubernetes resources and what issues has it caused?" 288 | }' \ 289 | http://localhost:8080/api/v1/mcp/commit 290 | ``` 291 | 292 | **What MCP Analyzes:** 293 | 294 | - Files changed in the commit 295 | - Connected ArgoCD applications 296 | - Affected Kubernetes resources 297 | - Subsequent pipeline results 298 | - Changes in resource configurations 299 | - Introduction of new errors or warnings 300 | 301 | ## Advanced Troubleshooting Scenarios 302 | 303 | ### Multi-Resource Analysis 304 | 305 | You can troubleshoot complex issues by instructing Claude to correlate multiple resources: 306 | 307 | **Example Request:** 308 | 309 | ```bash 310 | curl -X POST \ 311 | -H "Content-Type: application/json" \ 312 | -H "X-API-Key: your_api_key" \ 313 | -d '{ 314 | "query": "Analyze the connectivity issue between the frontend deployment and the backend service in the 'myapp' namespace. Check both the deployment and the service configurations." 315 | }' \ 316 | http://localhost:8080/api/v1/mcp 317 | ``` 318 | 319 | ### Diagram Generation 320 | 321 | For complex troubleshooting scenarios, you can request diagram generation to visualize relationships: 322 | 323 | **Example Request:** 324 | 325 | ```bash 326 | curl -X POST \ 327 | -H "Content-Type: application/json" \ 328 | -H "X-API-Key: your_api_key" \ 329 | -d '{ 330 | "resource": "deployment", 331 | "name": "my-app", 332 | "namespace": "default", 333 | "query": "Create a diagram showing this deployment's relationship to all associated resources, including services, ingresses, configmaps, and secrets." 334 | }' \ 335 | http://localhost:8080/api/v1/mcp/resource 336 | ``` 337 | 338 | Claude can generate Mermaid diagrams within its response to visualize the relationships. 339 | 340 | ## Troubleshooting Best Practices 341 | 342 | When using Kubernetes Claude MCP for troubleshooting: 343 | 344 | 1. **Start specific**: Begin with the resource that's showing symptoms 345 | 2. **Go broad**: If needed, expand to related resources 346 | 3. **Use specific queries**: The more specific your query, the better Claude can help 347 | 4. **Include context**: Mention what you've already tried or specific symptoms 348 | 5. **Follow recommendations**: Try the recommended fixes one at a time 349 | 6. **Iterate**: Use follow-up queries to dive deeper 350 | 351 | ## Real-Time Troubleshooting 352 | 353 | For ongoing issues, you can set up continuous monitoring: 354 | 355 | ```bash 356 | # Watch a resource and get alerts when issues are detected 357 | watch -n 30 'curl -s -X POST \ 358 | -H "Content-Type: application/json" \ 359 | -H "X-API-Key: your_api_key" \ 360 | -d "{\"resource\":\"deployment\",\"name\":\"my-app\",\"namespace\":\"default\",\"query\":\"Report any new issues\"}" \ 361 | http://localhost:8080/api/v1/mcp/troubleshoot | jq .troubleshootResult.issues' 362 | ``` 363 | 364 | ## Troubleshooting Reference 365 | 366 | Here's a quick reference of what to check for common Kubernetes issues: 367 | 368 | | Symptom | Resource to Check | Common Issues | 369 | |---------|-------------------|---------------| 370 | | Application not starting | Pod | Image pull errors, resource constraints, configuration issues | 371 | | Cannot connect to app | Service | Selector mismatch, port configuration, pod health | 372 | | External access failing | Ingress | Path configuration, backend service, TLS issues | 373 | | Scaling issues | Deployment | Resource constraints, pod disruption budgets, affinity rules | 374 | | Configuration issues | ConfigMap/Secret | Missing keys, invalid format, mounting issues | 375 | | Persistent storage issues | PVC | Storage class, capacity issues, access modes | 376 | | GitOps sync failures | ArgoCD Application | Git repo issues, manifest errors, resource conflicts | ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/mcp/context.go: -------------------------------------------------------------------------------- ```go 1 | package mcp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" 10 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/logging" 11 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/pkg/utils" 12 | ) 13 | 14 | // ContextManager handles the creation and management of context for Claude 15 | type ContextManager struct { 16 | maxContextSize int 17 | logger *logging.Logger 18 | } 19 | 20 | // NewContextManager creates a new context manager 21 | func NewContextManager(maxContextSize int, logger *logging.Logger) *ContextManager { 22 | if maxContextSize <= 0 { 23 | maxContextSize = 100000 24 | } 25 | 26 | if logger == nil { 27 | logger = logging.NewLogger().Named("context") 28 | } 29 | 30 | return &ContextManager{ 31 | maxContextSize: maxContextSize, 32 | logger: logger, 33 | } 34 | } 35 | 36 | // FormatResourceContext formats a resource context for Claude 37 | func (cm *ContextManager) FormatResourceContext(rc models.ResourceContext) (string, error) { 38 | cm.logger.Debug("Formatting resource context", 39 | "kind", rc.Kind, 40 | "name", rc.Name, 41 | "namespace", rc.Namespace) 42 | 43 | var formattedContext string 44 | 45 | // Format the basic resource information 46 | formattedContext += fmt.Sprintf("# Kubernetes Resource: %s/%s\n", rc.Kind, rc.Name) 47 | if rc.Namespace != "" { 48 | formattedContext += fmt.Sprintf("Namespace: %s\n", rc.Namespace) 49 | } 50 | formattedContext += fmt.Sprintf("API Version: %s\n\n", rc.APIVersion) 51 | 52 | // Add the full resource data if available 53 | if rc.ResourceData != "" { 54 | formattedContext += "## Resource Details\n```json\n" 55 | formattedContext += rc.ResourceData 56 | formattedContext += "\n```\n\n" 57 | } 58 | 59 | // Add resource-specific metadata if available 60 | if rc.Metadata != nil { 61 | // Add deployment-specific information 62 | if strings.EqualFold(rc.Kind, "deployment") { 63 | formattedContext += "## Deployment Status\n" 64 | 65 | // Add replica information 66 | if desiredReplicas, ok := rc.Metadata["desiredReplicas"].(int64); ok { 67 | formattedContext += fmt.Sprintf("Desired Replicas: %d\n", desiredReplicas) 68 | } 69 | 70 | if currentReplicas, ok := rc.Metadata["currentReplicas"].(int64); ok { 71 | formattedContext += fmt.Sprintf("Current Replicas: %d\n", currentReplicas) 72 | } 73 | 74 | if readyReplicas, ok := rc.Metadata["readyReplicas"].(int64); ok { 75 | formattedContext += fmt.Sprintf("Ready Replicas: %d\n", readyReplicas) 76 | } 77 | 78 | if availableReplicas, ok := rc.Metadata["availableReplicas"].(int64); ok { 79 | formattedContext += fmt.Sprintf("Available Replicas: %d\n", availableReplicas) 80 | } 81 | 82 | // Add container information 83 | if containers, ok := rc.Metadata["containers"].([]map[string]interface{}); ok && len(containers) > 0 { 84 | formattedContext += "\n### Containers\n" 85 | for i, container := range containers { 86 | formattedContext += fmt.Sprintf("%d. Name: %s\n", i+1, container["name"]) 87 | 88 | if image, ok := container["image"].(string); ok { 89 | formattedContext += fmt.Sprintf(" Image: %s\n", image) 90 | } 91 | 92 | if resources, ok := container["resources"].(map[string]interface{}); ok { 93 | formattedContext += " Resources:\n" 94 | 95 | if requests, ok := resources["requests"].(map[string]interface{}); ok { 96 | formattedContext += " Requests:\n" 97 | for k, v := range requests { 98 | formattedContext += fmt.Sprintf(" %s: %v\n", k, v) 99 | } 100 | } 101 | 102 | if limits, ok := resources["limits"].(map[string]interface{}); ok { 103 | formattedContext += " Limits:\n" 104 | for k, v := range limits { 105 | formattedContext += fmt.Sprintf(" %s: %v\n", k, v) 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | formattedContext += "\n" 113 | } 114 | } 115 | 116 | // If this is a namespace, add namespace-specific information 117 | if strings.EqualFold(rc.Kind, "namespace") { 118 | // Add resource metadata if available 119 | if rc.Metadata != nil { 120 | if resourceCounts, ok := rc.Metadata["resourceCounts"].(map[string][]string); ok { 121 | formattedContext += "## Resources in Namespace\n" 122 | for kind, resources := range resourceCounts { 123 | formattedContext += fmt.Sprintf("- %s: %d resources\n", kind, len(resources)) 124 | 125 | // List up to 5 resources of each kind 126 | if len(resources) > 0 { 127 | formattedContext += " - " 128 | for i, name := range resources { 129 | if i > 4 { 130 | formattedContext += fmt.Sprintf("and %d more...", len(resources)-5) 131 | break 132 | } 133 | if i > 0 { 134 | formattedContext += ", " 135 | } 136 | formattedContext += name 137 | } 138 | formattedContext += "\n" 139 | } 140 | } 141 | formattedContext += "\n" 142 | } 143 | 144 | if health, ok := rc.Metadata["health"].(map[string]map[string]string); ok { 145 | formattedContext += "## Health Status\n" 146 | for kind, statuses := range health { 147 | healthy := 0 148 | unhealthy := 0 149 | progressing := 0 150 | unknown := 0 151 | 152 | for _, status := range statuses { 153 | switch status { 154 | case "healthy": 155 | healthy++ 156 | case "unhealthy": 157 | unhealthy++ 158 | case "progressing": 159 | progressing++ 160 | default: 161 | unknown++ 162 | } 163 | } 164 | 165 | formattedContext += fmt.Sprintf("- %s: %d healthy, %d unhealthy, %d progressing, %d unknown\n", 166 | kind, healthy, unhealthy, progressing, unknown) 167 | 168 | // List unhealthy resources 169 | unhealthyResources := []string{} 170 | for name, status := range statuses { 171 | if status == "unhealthy" { 172 | unhealthyResources = append(unhealthyResources, name) 173 | } 174 | } 175 | 176 | if len(unhealthyResources) > 0 { 177 | formattedContext += " Unhealthy: " 178 | for i, name := range unhealthyResources { 179 | if i > 4 { 180 | formattedContext += fmt.Sprintf("and %d more...", len(unhealthyResources)-5) 181 | break 182 | } 183 | if i > 0 { 184 | formattedContext += ", " 185 | } 186 | formattedContext += name 187 | } 188 | formattedContext += "\n" 189 | } 190 | } 191 | formattedContext += "\n" 192 | } 193 | } 194 | } 195 | 196 | // Format ArgoCD information if available 197 | if rc.ArgoApplication != nil { 198 | formattedContext += "## ArgoCD Application\n" 199 | formattedContext += fmt.Sprintf("Name: %s\n", rc.ArgoApplication.Name) 200 | formattedContext += fmt.Sprintf("Sync Status: %s\n", rc.ArgoSyncStatus) 201 | formattedContext += fmt.Sprintf("Health Status: %s\n", rc.ArgoHealthStatus) 202 | 203 | if rc.ArgoApplication.Spec.Source.RepoURL != "" { 204 | formattedContext += fmt.Sprintf("Source: %s\n", rc.ArgoApplication.Spec.Source.RepoURL) 205 | formattedContext += fmt.Sprintf("Path: %s\n", rc.ArgoApplication.Spec.Source.Path) 206 | formattedContext += fmt.Sprintf("Target Revision: %s\n", rc.ArgoApplication.Spec.Source.TargetRevision) 207 | } 208 | 209 | formattedContext += "\n" 210 | 211 | // Add recent sync history 212 | if len(rc.ArgoSyncHistory) > 0 { 213 | formattedContext += "### Recent Sync History\n" 214 | for i, history := range rc.ArgoSyncHistory { 215 | formattedContext += fmt.Sprintf("%d. [%s] Revision: %s, Status: %s\n", 216 | i+1, 217 | history.DeployedAt.Format(time.RFC3339), 218 | history.Revision, 219 | history.Status) 220 | } 221 | formattedContext += "\n" 222 | } 223 | } 224 | 225 | // Format GitLab information if available 226 | if rc.GitLabProject != nil { 227 | formattedContext += "## GitLab Project\n" 228 | formattedContext += fmt.Sprintf("Name: %s\n", rc.GitLabProject.PathWithNamespace) 229 | formattedContext += fmt.Sprintf("URL: %s\n\n", rc.GitLabProject.WebURL) 230 | 231 | // Add last pipeline information 232 | if rc.LastPipeline != nil { 233 | formattedContext += "### Last Pipeline\n" 234 | 235 | // Handle pipeline CreatedAt timestamp 236 | var pipelineTimestamp string 237 | switch createdAt := rc.LastPipeline.CreatedAt.(type) { 238 | case int64: 239 | pipelineTimestamp = time.Unix(createdAt, 0).Format(time.RFC3339) 240 | case float64: 241 | pipelineTimestamp = time.Unix(int64(createdAt), 0).Format(time.RFC3339) 242 | case string: 243 | // Try to parse the string timestamp 244 | parsed, err := time.Parse(time.RFC3339, createdAt) 245 | if err != nil { 246 | // Try alternative format 247 | parsed, err = time.Parse("2006-01-02T15:04:05.000Z", createdAt) 248 | if err != nil { 249 | // Use raw string if parsing fails 250 | pipelineTimestamp = createdAt 251 | } else { 252 | pipelineTimestamp = parsed.Format(time.RFC3339) 253 | } 254 | } else { 255 | pipelineTimestamp = parsed.Format(time.RFC3339) 256 | } 257 | default: 258 | pipelineTimestamp = "unknown timestamp" 259 | } 260 | 261 | formattedContext += fmt.Sprintf("Status: %s\n", rc.LastPipeline.Status) 262 | formattedContext += fmt.Sprintf("Ref: %s\n", rc.LastPipeline.Ref) 263 | formattedContext += fmt.Sprintf("SHA: %s\n", rc.LastPipeline.SHA) 264 | formattedContext += fmt.Sprintf("Created At: %s\n\n", pipelineTimestamp) 265 | } 266 | 267 | // Add last deployment information 268 | if rc.LastDeployment != nil { 269 | formattedContext += "### Last Deployment\n" 270 | 271 | // Handle deployment CreatedAt timestamp 272 | var deploymentTimestamp string 273 | switch createdAt := rc.LastDeployment.CreatedAt.(type) { 274 | case int64: 275 | deploymentTimestamp = time.Unix(createdAt, 0).Format(time.RFC3339) 276 | case float64: 277 | deploymentTimestamp = time.Unix(int64(createdAt), 0).Format(time.RFC3339) 278 | case string: 279 | // Try to parse the string timestamp 280 | parsed, err := time.Parse(time.RFC3339, createdAt) 281 | if err != nil { 282 | // Try alternative format 283 | parsed, err = time.Parse("2006-01-02T15:04:05.000Z", createdAt) 284 | if err != nil { 285 | // Use raw string if parsing fails 286 | deploymentTimestamp = createdAt 287 | } else { 288 | deploymentTimestamp = parsed.Format(time.RFC3339) 289 | } 290 | } else { 291 | deploymentTimestamp = parsed.Format(time.RFC3339) 292 | } 293 | default: 294 | deploymentTimestamp = "unknown timestamp" 295 | } 296 | 297 | formattedContext += fmt.Sprintf("Status: %s\n", rc.LastDeployment.Status) 298 | formattedContext += fmt.Sprintf("Environment: %s\n", rc.LastDeployment.Environment.Name) 299 | formattedContext += fmt.Sprintf("Created At: %s\n\n", deploymentTimestamp) 300 | } 301 | 302 | // Add recent commits 303 | if len(rc.RecentCommits) > 0 { 304 | formattedContext += "### Recent Commits\n" 305 | for i, commit := range rc.RecentCommits { 306 | // Handle commit CreatedAt timestamp 307 | var commitTimestamp string 308 | switch createdAt := commit.CreatedAt.(type) { 309 | case int64: 310 | commitTimestamp = time.Unix(createdAt, 0).Format(time.RFC3339) 311 | case float64: 312 | commitTimestamp = time.Unix(int64(createdAt), 0).Format(time.RFC3339) 313 | case string: 314 | // Try to parse the string timestamp 315 | parsed, err := time.Parse(time.RFC3339, createdAt) 316 | if err != nil { 317 | // Try alternative format 318 | parsed, err = time.Parse("2006-01-02T15:04:05.000Z", createdAt) 319 | if err != nil { 320 | // Use raw string if parsing fails 321 | commitTimestamp = createdAt 322 | } else { 323 | commitTimestamp = parsed.Format(time.RFC3339) 324 | } 325 | } else { 326 | commitTimestamp = parsed.Format(time.RFC3339) 327 | } 328 | default: 329 | commitTimestamp = "unknown timestamp" 330 | } 331 | 332 | formattedContext += fmt.Sprintf("%d. [%s] %s by %s: %s\n", 333 | i+1, 334 | commitTimestamp, 335 | commit.ShortID, 336 | commit.AuthorName, 337 | commit.Title) 338 | } 339 | formattedContext += "\n" 340 | } 341 | } 342 | 343 | // Format Kubernetes events 344 | if len(rc.Events) > 0 { 345 | formattedContext += "## Recent Kubernetes Events\n" 346 | for i, event := range rc.Events { 347 | formattedContext += fmt.Sprintf("%d. [%s] %s: %s\n", 348 | i+1, 349 | event.Type, 350 | event.Reason, 351 | event.Message) 352 | } 353 | formattedContext += "\n" 354 | } 355 | 356 | if len(rc.RelatedResources) > 0 { 357 | formattedContext += "## Related Resources\n" 358 | // Group by resource kind 359 | resourcesByKind := make(map[string][]string) 360 | for _, resource := range rc.RelatedResources { 361 | parts := strings.Split(resource, "/") 362 | if len(parts) == 2 { 363 | kind := parts[0] 364 | name := parts[1] 365 | resourcesByKind[kind] = append(resourcesByKind[kind], name) 366 | } else { 367 | // If format is unexpected, just add as is 368 | formattedContext += fmt.Sprintf("- %s\n", resource) 369 | } 370 | } 371 | 372 | // Format resources by kind 373 | for kind, names := range resourcesByKind { 374 | formattedContext += fmt.Sprintf("- %s (%d):\n", kind, len(names)) 375 | // Show up to 10 resources per kind 376 | maxToShow := 10 377 | if len(names) > maxToShow { 378 | for i := 0; i < maxToShow; i++ { 379 | formattedContext += fmt.Sprintf(" - %s\n", names[i]) 380 | } 381 | formattedContext += fmt.Sprintf(" - ... and %d more\n", len(names)-maxToShow) 382 | } else { 383 | for _, name := range names { 384 | formattedContext += fmt.Sprintf(" - %s\n", name) 385 | } 386 | } 387 | } 388 | formattedContext += "\n" 389 | } 390 | 391 | // Add errors if any 392 | if len(rc.Errors) > 0 { 393 | formattedContext += "## Errors in Data Collection\n" 394 | for _, err := range rc.Errors { 395 | formattedContext += fmt.Sprintf("- %s\n", err) 396 | } 397 | formattedContext += "\n" 398 | } 399 | 400 | // Ensure context doesn't exceed max size 401 | if len(formattedContext) > cm.maxContextSize { 402 | cm.logger.Debug("Context exceeds maximum size, truncating", 403 | "originalSize", len(formattedContext), 404 | "maxSize", cm.maxContextSize) 405 | formattedContext = utils.TruncateContextSmartly(formattedContext, cm.maxContextSize) 406 | } 407 | 408 | cm.logger.Debug("Formatted resource context", 409 | "kind", rc.Kind, 410 | "name", rc.Name, 411 | "contextSize", len(formattedContext)) 412 | return formattedContext, nil 413 | } 414 | 415 | // CombineContexts combines multiple resource contexts into a single context 416 | func (cm *ContextManager) CombineContexts(ctx context.Context, resourceContexts []models.ResourceContext) (string, error) { 417 | cm.logger.Debug("Combining resource contexts", "count", len(resourceContexts)) 418 | 419 | var combinedContext string 420 | 421 | combinedContext += fmt.Sprintf("# Kubernetes GitOps Context (%d resources)\n\n", len(resourceContexts)) 422 | 423 | // Add context for each resource 424 | for i, rc := range resourceContexts { 425 | resourceContext, err := cm.FormatResourceContext(rc) 426 | if err != nil { 427 | return "", fmt.Errorf("failed to format resource context #%d: %w", i+1, err) 428 | } 429 | 430 | combinedContext += fmt.Sprintf("--- RESOURCE %d/%d ---\n", i+1, len(resourceContexts)) 431 | combinedContext += resourceContext 432 | combinedContext += "------------------------\n\n" 433 | } 434 | 435 | // Ensure combined context doesn't exceed max size 436 | if len(combinedContext) > cm.maxContextSize { 437 | cm.logger.Debug("Combined context exceeds maximum size, truncating", 438 | "originalSize", len(combinedContext), 439 | "maxSize", cm.maxContextSize) 440 | combinedContext = utils.TruncateContextSmartly(combinedContext, cm.maxContextSize) 441 | } 442 | 443 | cm.logger.Debug("Combined resource contexts", 444 | "resourceCount", len(resourceContexts), 445 | "contextSize", len(combinedContext)) 446 | return combinedContext, nil 447 | } ``` -------------------------------------------------------------------------------- /kubernetes-claude-mcp/internal/api/routes.go: -------------------------------------------------------------------------------- ```go 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/Blankcut/kubernetes-mcp-server/kubernetes-claude-mcp/internal/models" 10 | "github.com/gorilla/mux" 11 | ) 12 | 13 | // setupRoutes configures the API routes 14 | func (s *Server) setupRoutes() { 15 | 16 | // Apply CORS middleware to all routes 17 | s.router.Use(s.corsMiddleware) 18 | 19 | // API version prefix 20 | apiV1 := s.router.PathPrefix("/api/v1").Subrouter() 21 | 22 | // Health check endpoint (no auth required) 23 | apiV1.HandleFunc("/health", s.handleHealth).Methods("GET") 24 | 25 | // Add authentication middleware to all other routes 26 | apiSecure := apiV1.NewRoute().Subrouter() 27 | apiSecure.Use(s.authMiddleware) 28 | 29 | // MCP endpoints 30 | apiSecure.HandleFunc("/mcp", s.handleMCPRequest).Methods("POST") 31 | apiSecure.HandleFunc("/mcp/resource", s.handleResourceQuery).Methods("POST") 32 | apiSecure.HandleFunc("/mcp/commit", s.handleCommitQuery).Methods("POST") 33 | apiSecure.HandleFunc("/mcp/troubleshoot", s.handleTroubleshoot).Methods("POST") 34 | 35 | // Kubernetes resource endpoints 36 | apiSecure.HandleFunc("/namespaces", s.handleListNamespaces).Methods("GET") 37 | apiSecure.HandleFunc("/resources/{resource}", s.handleListResources).Methods("GET") 38 | apiSecure.HandleFunc("/resources/{resource}/{name}", s.handleGetResource).Methods("GET") 39 | apiSecure.HandleFunc("/events", s.handleGetEvents).Methods("GET") 40 | 41 | // ArgoCD endpoints 42 | apiSecure.HandleFunc("/argocd/applications", s.handleListArgoApplications).Methods("GET") 43 | apiSecure.HandleFunc("/argocd/applications/{name}", s.handleGetArgoApplication).Methods("GET") 44 | 45 | // GitLab endpoints 46 | apiSecure.HandleFunc("/gitlab/projects", s.handleListGitLabProjects).Methods("GET") 47 | apiSecure.HandleFunc("/gitlab/projects/{projectId}/pipelines", s.handleListGitLabPipelines).Methods("GET") 48 | 49 | // Merge Request endpoints 50 | apiSecure.HandleFunc("/mcp/mergeRequest", s.handleMergeRequestQuery).Methods("POST") 51 | } 52 | 53 | // handleMergeRequestQuery handles MCP requests for analyzing merge requests 54 | func (s *Server) handleMergeRequestQuery(w http.ResponseWriter, r *http.Request) { 55 | var request models.MCPRequest 56 | 57 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil { 58 | s.respondWithError(w, http.StatusBadRequest, "Invalid request format", err) 59 | return 60 | } 61 | 62 | // Force action to be queryMergeRequest 63 | request.Action = "queryMergeRequest" 64 | 65 | // Validate merge request parameters 66 | if request.ProjectID == "" || request.MergeRequestIID <= 0 { 67 | s.respondWithError(w, http.StatusBadRequest, "Project ID and merge request IID are required", nil) 68 | return 69 | } 70 | 71 | s.logger.Info("Received merge request query", 72 | "projectId", request.ProjectID, 73 | "mergeRequestIID", request.MergeRequestIID) 74 | 75 | // Process the request 76 | response, err := s.mcpHandler.ProcessRequest(r.Context(), &request) 77 | if err != nil { 78 | s.respondWithError(w, http.StatusInternalServerError, "Failed to process request", err) 79 | return 80 | } 81 | 82 | s.respondWithJSON(w, http.StatusOK, response) 83 | } 84 | 85 | // handleHealth handles health check requests 86 | func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { 87 | type healthResponse struct { 88 | Status string `json:"status"` 89 | Services map[string]string `json:"services"` 90 | } 91 | 92 | // Check each service 93 | services := map[string]string{ 94 | "kubernetes": "unknown", 95 | "argocd": "unknown", 96 | "gitlab": "unknown", 97 | "claude": "unknown", 98 | } 99 | 100 | ctx := r.Context() 101 | 102 | // Check Kubernetes connectivity 103 | if err := s.k8sClient.CheckConnectivity(ctx); err != nil { 104 | services["kubernetes"] = "unavailable" 105 | s.logger.Warn("Kubernetes health check failed", "error", err) 106 | } else { 107 | services["kubernetes"] = "available" 108 | } 109 | 110 | // Check ArgoCD connectivity 111 | if err := s.argoClient.CheckConnectivity(ctx); err != nil { 112 | services["argocd"] = "unavailable" 113 | s.logger.Warn("ArgoCD health check failed", "error", err) 114 | } else { 115 | services["argocd"] = "available" 116 | } 117 | 118 | // Check GitLab connectivity 119 | if err := s.gitlabClient.CheckConnectivity(ctx); err != nil { 120 | services["gitlab"] = "unavailable" 121 | s.logger.Warn("GitLab health check failed", "error", err) 122 | } else { 123 | services["gitlab"] = "available" 124 | } 125 | 126 | // For Claude, we just assume it's available since we don't want to make an API call 127 | // in a health check endpoint 128 | services["claude"] = "assumed available" 129 | 130 | // Determine overall status 131 | status := "ok" 132 | if services["kubernetes"] != "available" { 133 | status = "degraded" 134 | } 135 | 136 | response := healthResponse{ 137 | Status: status, 138 | Services: services, 139 | } 140 | 141 | w.Header().Set("Content-Type", "application/json") 142 | w.WriteHeader(http.StatusOK) 143 | json.NewEncoder(w).Encode(response) 144 | } 145 | 146 | // handleMCPRequest handles generic MCP requests 147 | func (s *Server) handleMCPRequest(w http.ResponseWriter, r *http.Request) { 148 | var request models.MCPRequest 149 | 150 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil { 151 | s.respondWithError(w, http.StatusBadRequest, "Invalid request format", err) 152 | return 153 | } 154 | 155 | s.logger.Info("Received MCP request", "action", request.Action) 156 | 157 | // Process the request 158 | response, err := s.mcpHandler.ProcessRequest(r.Context(), &request) 159 | if err != nil { 160 | s.respondWithError(w, http.StatusInternalServerError, "Failed to process request", err) 161 | return 162 | } 163 | 164 | s.respondWithJSON(w, http.StatusOK, response) 165 | } 166 | 167 | // handleResourceQuery handles MCP requests for querying resources 168 | func (s *Server) handleResourceQuery(w http.ResponseWriter, r *http.Request) { 169 | var request models.MCPRequest 170 | 171 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil { 172 | s.respondWithError(w, http.StatusBadRequest, "Invalid request format", err) 173 | return 174 | } 175 | 176 | // Force action to be queryResource 177 | request.Action = "queryResource" 178 | 179 | // Validate resource parameters 180 | if request.Resource == "" || request.Name == "" { 181 | s.respondWithError(w, http.StatusBadRequest, "Resource and name are required", nil) 182 | return 183 | } 184 | 185 | s.logger.Info("Received resource query", 186 | "resource", request.Resource, 187 | "name", request.Name, 188 | "namespace", request.Namespace) 189 | 190 | // Special handling for namespace resources to provide comprehensive data 191 | if strings.ToLower(request.Resource) == "namespace" { 192 | // Get namespace topology 193 | topology, err := s.k8sClient.GetNamespaceTopology(r.Context(), request.Name) 194 | if err != nil { 195 | s.respondWithError(w, http.StatusInternalServerError, "Failed to get namespace topology", err) 196 | return 197 | } 198 | 199 | // Get all resources in the namespace 200 | resources, err := s.k8sClient.GetAllNamespaceResources(r.Context(), request.Name) 201 | if err != nil { 202 | s.respondWithError(w, http.StatusInternalServerError, "Failed to get namespace resources", err) 203 | return 204 | } 205 | 206 | // Get namespace analysis 207 | analysis, err := s.mcpHandler.AnalyzeNamespace(r.Context(), request.Name) 208 | if err != nil { 209 | s.respondWithError(w, http.StatusInternalServerError, "Failed to analyze namespace", err) 210 | return 211 | } 212 | 213 | // Create an enhanced request with the gathered data 214 | enhancedRequest := request 215 | enhancedRequest.Context = fmt.Sprintf("# Namespace Analysis: %s\n\n", request.Name) 216 | enhancedRequest.Context += fmt.Sprintf("## Resource Counts\n") 217 | for kind, count := range resources.Stats { 218 | enhancedRequest.Context += fmt.Sprintf("- %s: %d\n", kind, count) 219 | } 220 | enhancedRequest.Context += "\n## Resource Relationships\n" 221 | for _, rel := range topology.Relationships { 222 | enhancedRequest.Context += fmt.Sprintf("- %s/%s → %s/%s (%s)\n", 223 | rel.SourceKind, rel.SourceName, rel.TargetKind, rel.TargetName, rel.RelationType) 224 | } 225 | enhancedRequest.Context += "\n## Health Status\n" 226 | for kind, statuses := range topology.Health { 227 | healthy := 0 228 | unhealthy := 0 229 | progressing := 0 230 | unknown := 0 231 | 232 | for _, status := range statuses { 233 | switch status { 234 | case "healthy": 235 | healthy++ 236 | case "unhealthy": 237 | unhealthy++ 238 | case "progressing": 239 | progressing++ 240 | default: 241 | unknown++ 242 | } 243 | } 244 | 245 | enhancedRequest.Context += fmt.Sprintf("- %s: %d healthy, %d unhealthy, %d progressing, %d unknown\n", 246 | kind, healthy, unhealthy, progressing, unknown) 247 | } 248 | 249 | // Get events for the namespace 250 | events, err := s.k8sClient.GetNamespaceEvents(r.Context(), request.Name) 251 | if err == nil && len(events) > 0 { 252 | enhancedRequest.Context += "\n## Recent Events\n" 253 | for i, event := range events { 254 | if i >= 10 { 255 | break // Limit to 10 events 256 | } 257 | enhancedRequest.Context += fmt.Sprintf("- [%s] %s: %s\n", 258 | event.Type, event.Reason, event.Message) 259 | } 260 | } 261 | 262 | // Process the enhanced request 263 | response, err := s.mcpHandler.ProcessRequest(r.Context(), &enhancedRequest) 264 | if err != nil { 265 | s.respondWithError(w, http.StatusInternalServerError, "Failed to process request", err) 266 | return 267 | } 268 | 269 | // Add analysis insights to the response 270 | if analysis != nil { 271 | response.NamespaceAnalysis = analysis 272 | } 273 | 274 | s.respondWithJSON(w, http.StatusOK, response) 275 | return 276 | } 277 | 278 | // Process regular resource query 279 | response, err := s.mcpHandler.ProcessRequest(r.Context(), &request) 280 | if err != nil { 281 | s.respondWithError(w, http.StatusInternalServerError, "Failed to process request", err) 282 | return 283 | } 284 | 285 | s.respondWithJSON(w, http.StatusOK, response) 286 | } 287 | 288 | // handleCommitQuery handles MCP requests for analyzing commits 289 | func (s *Server) handleCommitQuery(w http.ResponseWriter, r *http.Request) { 290 | var request models.MCPRequest 291 | 292 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil { 293 | s.respondWithError(w, http.StatusBadRequest, "Invalid request format", err) 294 | return 295 | } 296 | 297 | // Force action to be queryCommit 298 | request.Action = "queryCommit" 299 | 300 | // Validate commit parameters 301 | if request.ProjectID == "" || request.CommitSHA == "" { 302 | s.respondWithError(w, http.StatusBadRequest, "Project ID and commit SHA are required", nil) 303 | return 304 | } 305 | 306 | s.logger.Info("Received commit query", 307 | "projectId", request.ProjectID, 308 | "commitSha", request.CommitSHA) 309 | 310 | // Process the request 311 | response, err := s.mcpHandler.ProcessRequest(r.Context(), &request) 312 | if err != nil { 313 | s.respondWithError(w, http.StatusInternalServerError, "Failed to process request", err) 314 | return 315 | } 316 | 317 | s.respondWithJSON(w, http.StatusOK, response) 318 | } 319 | 320 | // handleTroubleshoot handles troubleshooting requests 321 | func (s *Server) handleTroubleshoot(w http.ResponseWriter, r *http.Request) { 322 | var request struct { 323 | Resource string `json:"resource"` 324 | Name string `json:"name"` 325 | Namespace string `json:"namespace"` 326 | Query string `json:"query,omitempty"` 327 | } 328 | 329 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil { 330 | s.respondWithError(w, http.StatusBadRequest, "Invalid request format", err) 331 | return 332 | } 333 | 334 | // Validate parameters 335 | if request.Resource == "" || request.Name == "" { 336 | s.respondWithError(w, http.StatusBadRequest, "Resource and name are required", nil) 337 | return 338 | } 339 | 340 | s.logger.Info("Received troubleshoot request", 341 | "resource", request.Resource, 342 | "name", request.Name, 343 | "namespace", request.Namespace) 344 | 345 | // Process the troubleshooting request 346 | result, err := s.troubleshootCorrelator.TroubleshootResource( 347 | r.Context(), 348 | request.Namespace, 349 | request.Resource, 350 | request.Name, 351 | ) 352 | if err != nil { 353 | s.respondWithError(w, http.StatusInternalServerError, "Failed to troubleshoot resource", err) 354 | return 355 | } 356 | 357 | // If there's a query, use Claude to analyze the results 358 | if request.Query != "" { 359 | mcpRequest := &models.MCPRequest{ 360 | Resource: request.Resource, 361 | Name: request.Name, 362 | Namespace: request.Namespace, 363 | Query: request.Query, 364 | } 365 | 366 | response, err := s.mcpHandler.ProcessTroubleshootRequest(r.Context(), mcpRequest, result) 367 | if err != nil { 368 | s.respondWithError(w, http.StatusInternalServerError, "Failed to process troubleshoot analysis", err) 369 | return 370 | } 371 | 372 | // Add the troubleshoot result to the response 373 | responseWithResult := struct { 374 | *models.MCPResponse 375 | TroubleshootResult *models.TroubleshootResult `json:"troubleshootResult"` 376 | }{ 377 | MCPResponse: response, 378 | TroubleshootResult: result, 379 | } 380 | 381 | s.respondWithJSON(w, http.StatusOK, responseWithResult) 382 | return 383 | } 384 | 385 | // If no query, just return the troubleshoot result 386 | s.respondWithJSON(w, http.StatusOK, result) 387 | } 388 | 389 | // handleListNamespaces handles requests to list namespaces 390 | func (s *Server) handleListNamespaces(w http.ResponseWriter, r *http.Request) { 391 | namespaces, err := s.k8sClient.GetNamespaces(r.Context()) 392 | if err != nil { 393 | s.respondWithError(w, http.StatusInternalServerError, "Failed to list namespaces", err) 394 | return 395 | } 396 | 397 | s.respondWithJSON(w, http.StatusOK, map[string][]string{"namespaces": namespaces}) 398 | } 399 | 400 | // handleListResources handles requests to list resources of a specific type 401 | func (s *Server) handleListResources(w http.ResponseWriter, r *http.Request) { 402 | vars := mux.Vars(r) 403 | resourceType := vars["resource"] 404 | namespace := r.URL.Query().Get("namespace") 405 | 406 | resources, err := s.k8sClient.ListResources(r.Context(), resourceType, namespace) 407 | if err != nil { 408 | s.respondWithError(w, http.StatusInternalServerError, "Failed to list resources", err) 409 | return 410 | } 411 | 412 | s.respondWithJSON(w, http.StatusOK, map[string]interface{}{"resources": resources}) 413 | } 414 | 415 | // handleGetResource handles requests to get a specific resource 416 | func (s *Server) handleGetResource(w http.ResponseWriter, r *http.Request) { 417 | vars := mux.Vars(r) 418 | resourceType := vars["resource"] 419 | name := vars["name"] 420 | namespace := r.URL.Query().Get("namespace") 421 | 422 | resource, err := s.k8sClient.GetResource(r.Context(), resourceType, namespace, name) 423 | if err != nil { 424 | s.respondWithError(w, http.StatusInternalServerError, "Failed to get resource", err) 425 | return 426 | } 427 | 428 | s.respondWithJSON(w, http.StatusOK, resource) 429 | } 430 | 431 | // handleGetEvents handles requests to get events 432 | func (s *Server) handleGetEvents(w http.ResponseWriter, r *http.Request) { 433 | namespace := r.URL.Query().Get("namespace") 434 | resourceType := r.URL.Query().Get("resource") 435 | name := r.URL.Query().Get("name") 436 | 437 | events, err := s.k8sClient.GetResourceEvents(r.Context(), namespace, resourceType, name) 438 | if err != nil { 439 | s.respondWithError(w, http.StatusInternalServerError, "Failed to get events", err) 440 | return 441 | } 442 | 443 | s.respondWithJSON(w, http.StatusOK, map[string]interface{}{"events": events}) 444 | } 445 | 446 | // handleListArgoApplications handles requests to list ArgoCD applications 447 | func (s *Server) handleListArgoApplications(w http.ResponseWriter, r *http.Request) { 448 | applications, err := s.argoClient.ListApplications(r.Context()) 449 | if err != nil { 450 | s.respondWithError(w, http.StatusInternalServerError, "Failed to list ArgoCD applications", err) 451 | return 452 | } 453 | 454 | s.respondWithJSON(w, http.StatusOK, map[string]interface{}{"applications": applications}) 455 | } 456 | 457 | // handleGetArgoApplication handles requests to get a specific ArgoCD application 458 | func (s *Server) handleGetArgoApplication(w http.ResponseWriter, r *http.Request) { 459 | vars := mux.Vars(r) 460 | name := vars["name"] 461 | 462 | application, err := s.argoClient.GetApplication(r.Context(), name) 463 | if err != nil { 464 | s.respondWithError(w, http.StatusInternalServerError, "Failed to get ArgoCD application", err) 465 | return 466 | } 467 | 468 | s.respondWithJSON(w, http.StatusOK, application) 469 | } 470 | 471 | // handleListGitLabProjects handles requests to list GitLab projects 472 | func (s *Server) handleListGitLabProjects(w http.ResponseWriter, r *http.Request) { 473 | // This would typically include pagination parameters 474 | projects, err := s.gitlabClient.ListProjects(r.Context()) 475 | if err != nil { 476 | s.respondWithError(w, http.StatusInternalServerError, "Failed to list GitLab projects", err) 477 | return 478 | } 479 | 480 | s.respondWithJSON(w, http.StatusOK, map[string]interface{}{"projects": projects}) 481 | } 482 | 483 | // handleListGitLabPipelines handles requests to list GitLab pipelines 484 | func (s *Server) handleListGitLabPipelines(w http.ResponseWriter, r *http.Request) { 485 | vars := mux.Vars(r) 486 | projectId := vars["projectId"] 487 | 488 | pipelines, err := s.gitlabClient.ListPipelines(r.Context(), projectId) 489 | if err != nil { 490 | s.respondWithError(w, http.StatusInternalServerError, "Failed to list GitLab pipelines", err) 491 | return 492 | } 493 | 494 | s.respondWithJSON(w, http.StatusOK, map[string]interface{}{"pipelines": pipelines}) 495 | } 496 | 497 | // Helper methods 498 | 499 | // respondWithError sends an error response to the client 500 | func (s *Server) respondWithError(w http.ResponseWriter, code int, message string, err error) { 501 | errorResponse := map[string]string{ 502 | "error": message, 503 | } 504 | 505 | if err != nil { 506 | errorResponse["details"] = err.Error() 507 | s.logger.Error(message, "error", err, "code", code) 508 | } else { 509 | s.logger.Warn(message, "code", code) 510 | } 511 | 512 | w.Header().Set("Content-Type", "application/json") 513 | w.WriteHeader(code) 514 | json.NewEncoder(w).Encode(errorResponse) 515 | } 516 | 517 | // respondWithJSON sends a JSON response to the client 518 | func (s *Server) respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { 519 | w.Header().Set("Content-Type", "application/json") 520 | w.WriteHeader(code) 521 | json.NewEncoder(w).Encode(payload) 522 | } 523 | ```