#
tokens: 48927/50000 24/82 files (page 2/5)
lines: on (toggle) GitHub
raw markdown copy reset
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 |           &copy; 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 |         &copy; {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 | }
```
Page 2/5FirstPrevNextLast