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