kubectl-mcp-server 1.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. kubectl_mcp_server-1.12.0.dist-info/METADATA +711 -0
  2. kubectl_mcp_server-1.12.0.dist-info/RECORD +45 -0
  3. kubectl_mcp_server-1.12.0.dist-info/WHEEL +5 -0
  4. kubectl_mcp_server-1.12.0.dist-info/entry_points.txt +3 -0
  5. kubectl_mcp_server-1.12.0.dist-info/licenses/LICENSE +21 -0
  6. kubectl_mcp_server-1.12.0.dist-info/top_level.txt +2 -0
  7. kubectl_mcp_tool/__init__.py +21 -0
  8. kubectl_mcp_tool/__main__.py +46 -0
  9. kubectl_mcp_tool/auth/__init__.py +13 -0
  10. kubectl_mcp_tool/auth/config.py +71 -0
  11. kubectl_mcp_tool/auth/scopes.py +148 -0
  12. kubectl_mcp_tool/auth/verifier.py +82 -0
  13. kubectl_mcp_tool/cli/__init__.py +9 -0
  14. kubectl_mcp_tool/cli/__main__.py +10 -0
  15. kubectl_mcp_tool/cli/cli.py +111 -0
  16. kubectl_mcp_tool/diagnostics.py +355 -0
  17. kubectl_mcp_tool/k8s_config.py +289 -0
  18. kubectl_mcp_tool/mcp_server.py +530 -0
  19. kubectl_mcp_tool/prompts/__init__.py +5 -0
  20. kubectl_mcp_tool/prompts/prompts.py +823 -0
  21. kubectl_mcp_tool/resources/__init__.py +5 -0
  22. kubectl_mcp_tool/resources/resources.py +305 -0
  23. kubectl_mcp_tool/tools/__init__.py +28 -0
  24. kubectl_mcp_tool/tools/browser.py +371 -0
  25. kubectl_mcp_tool/tools/cluster.py +315 -0
  26. kubectl_mcp_tool/tools/core.py +421 -0
  27. kubectl_mcp_tool/tools/cost.py +680 -0
  28. kubectl_mcp_tool/tools/deployments.py +381 -0
  29. kubectl_mcp_tool/tools/diagnostics.py +174 -0
  30. kubectl_mcp_tool/tools/helm.py +1561 -0
  31. kubectl_mcp_tool/tools/networking.py +296 -0
  32. kubectl_mcp_tool/tools/operations.py +501 -0
  33. kubectl_mcp_tool/tools/pods.py +582 -0
  34. kubectl_mcp_tool/tools/security.py +333 -0
  35. kubectl_mcp_tool/tools/storage.py +133 -0
  36. kubectl_mcp_tool/utils/__init__.py +17 -0
  37. kubectl_mcp_tool/utils/helpers.py +80 -0
  38. tests/__init__.py +9 -0
  39. tests/conftest.py +379 -0
  40. tests/test_auth.py +256 -0
  41. tests/test_browser.py +349 -0
  42. tests/test_prompts.py +536 -0
  43. tests/test_resources.py +343 -0
  44. tests/test_server.py +384 -0
  45. tests/test_tools.py +659 -0
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Kubernetes diagnostics and monitoring module for kubectl-mcp-tool.
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ from typing import Dict, List, Any, Optional
9
+ import subprocess
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class KubernetesDiagnostics:
14
+ """Kubernetes diagnostics and monitoring tools."""
15
+
16
+ @staticmethod
17
+ def run_command(cmd: List[str]) -> str:
18
+ """Run a kubectl command and return the output."""
19
+ try:
20
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
21
+ return result.stdout.strip()
22
+ except subprocess.CalledProcessError as e:
23
+ logger.error(f"Command failed: {' '.join(cmd)}")
24
+ logger.error(f"Error output: {e.stderr}")
25
+ return f"Error: {e.stderr}"
26
+
27
+ def get_logs(self, pod_name: str, namespace: Optional[str] = None,
28
+ container: Optional[str] = None, tail: Optional[int] = None,
29
+ previous: bool = False) -> Dict[str, Any]:
30
+ """Get logs from a pod."""
31
+ cmd = ["kubectl", "logs"]
32
+
33
+ if namespace:
34
+ cmd.extend(["-n", namespace])
35
+
36
+ if container:
37
+ cmd.extend(["-c", container])
38
+
39
+ if tail:
40
+ cmd.extend(["--tail", str(tail)])
41
+
42
+ if previous:
43
+ cmd.append("-p")
44
+
45
+ cmd.append(pod_name)
46
+
47
+ logs = self.run_command(cmd)
48
+ return {
49
+ "pod_name": pod_name,
50
+ "namespace": namespace or "default",
51
+ "container": container,
52
+ "logs": logs
53
+ }
54
+
55
+ def get_events(self, namespace: Optional[str] = None,
56
+ resource_name: Optional[str] = None) -> Dict[str, Any]:
57
+ """Get Kubernetes events."""
58
+ cmd = ["kubectl", "get", "events"]
59
+
60
+ if namespace:
61
+ cmd.extend(["-n", namespace])
62
+
63
+ if resource_name:
64
+ cmd.extend([f"--field-selector=involvedObject.name={resource_name}"])
65
+
66
+ cmd.extend(["--sort-by=.metadata.creationTimestamp"])
67
+
68
+ events = self.run_command(cmd)
69
+ return {
70
+ "namespace": namespace or "default",
71
+ "resource_name": resource_name,
72
+ "events": events
73
+ }
74
+
75
+ def check_pod_health(self, pod_name: str, namespace: str = "default") -> Dict[str, Any]:
76
+ """Check pod health and detect common issues."""
77
+ # Get pod details
78
+ cmd = ["kubectl", "get", "pod", pod_name, "-n", namespace, "-o", "json"]
79
+ pod_json_str = self.run_command(cmd)
80
+
81
+ try:
82
+ pod_json = json.loads(pod_json_str)
83
+ except json.JSONDecodeError:
84
+ return {"error": "Failed to get pod details", "raw_output": pod_json_str}
85
+
86
+ issues = []
87
+
88
+ # Check pod status
89
+ status = pod_json.get("status", {})
90
+ phase = status.get("phase")
91
+ if phase != "Running":
92
+ issues.append(f"Pod is not running (Status: {phase})")
93
+
94
+ # Check container statuses
95
+ for container in status.get("containerStatuses", []):
96
+ if not container.get("ready"):
97
+ waiting = container.get("state", {}).get("waiting", {})
98
+ if waiting:
99
+ issues.append(
100
+ f"Container {container['name']} is waiting: "
101
+ f"{waiting.get('reason')} - {waiting.get('message')}"
102
+ )
103
+
104
+ # Get recent events
105
+ events = self.get_events(namespace, pod_name)
106
+
107
+ # Check resource usage
108
+ usage = self.get_resource_usage("pods", namespace, pod_name)
109
+
110
+ return {
111
+ "pod_name": pod_name,
112
+ "namespace": namespace,
113
+ "status": phase,
114
+ "issues": issues,
115
+ "events": events.get("events"),
116
+ "resource_usage": usage.get("usage")
117
+ }
118
+
119
+ def get_resource_usage(self, resource_type: str = "pods",
120
+ namespace: Optional[str] = None,
121
+ resource_name: Optional[str] = None) -> Dict[str, Any]:
122
+ """Get resource usage statistics."""
123
+ cmd = ["kubectl", "top", resource_type]
124
+
125
+ if namespace:
126
+ cmd.extend(["-n", namespace])
127
+
128
+ if resource_name:
129
+ cmd.append(resource_name)
130
+
131
+ usage = self.run_command(cmd)
132
+ return {
133
+ "resource_type": resource_type,
134
+ "namespace": namespace,
135
+ "resource_name": resource_name,
136
+ "usage": usage
137
+ }
138
+
139
+ def validate_resources(self, namespace: str = "default") -> Dict[str, Any]:
140
+ """Validate Kubernetes resources for common misconfigurations."""
141
+ validations = []
142
+
143
+ # Get all pods
144
+ cmd = ["kubectl", "get", "pods", "-n", namespace, "-o", "json"]
145
+ pods_json_str = self.run_command(cmd)
146
+
147
+ try:
148
+ pods = json.loads(pods_json_str)
149
+ except json.JSONDecodeError:
150
+ return {"error": "Failed to get pods", "raw_output": pods_json_str}
151
+
152
+ for pod in pods.get("items", []):
153
+ pod_name = pod["metadata"]["name"]
154
+
155
+ # Check resource limits
156
+ for container in pod["spec"]["containers"]:
157
+ if not container.get("resources", {}).get("limits"):
158
+ validations.append({
159
+ "level": "warning",
160
+ "type": "no_resource_limits",
161
+ "message": f"Container {container['name']} in pod {pod_name} has no resource limits"
162
+ })
163
+
164
+ # Check probes
165
+ if not container.get("livenessProbe"):
166
+ validations.append({
167
+ "level": "warning",
168
+ "type": "no_liveness_probe",
169
+ "message": f"Container {container['name']} in pod {pod_name} has no liveness probe"
170
+ })
171
+ if not container.get("readinessProbe"):
172
+ validations.append({
173
+ "level": "warning",
174
+ "type": "no_readiness_probe",
175
+ "message": f"Container {container['name']} in pod {pod_name} has no readiness probe"
176
+ })
177
+
178
+ return {
179
+ "namespace": namespace,
180
+ "validations": validations,
181
+ "summary": {
182
+ "total_validations": len(validations),
183
+ "warnings": len([v for v in validations if v["level"] == "warning"]),
184
+ "errors": len([v for v in validations if v["level"] == "error"])
185
+ }
186
+ }
187
+
188
+ def analyze_pod_logs(self, pod_name: str, namespace: str = "default",
189
+ container: Optional[str] = None, tail: int = 1000) -> Dict[str, Any]:
190
+ """Analyze pod logs for common issues and patterns."""
191
+ # Get logs
192
+ logs_data = self.get_logs(pod_name, namespace, container, tail)
193
+ logs = logs_data.get("logs", "")
194
+
195
+ analysis = {
196
+ "error_count": 0,
197
+ "warning_count": 0,
198
+ "errors": [],
199
+ "warnings": [],
200
+ "patterns": {}
201
+ }
202
+
203
+ # Common error patterns to look for
204
+ error_patterns = [
205
+ "error", "exception", "failed", "failure", "fatal",
206
+ "OOMKilled", "CrashLoopBackOff"
207
+ ]
208
+
209
+ warning_patterns = [
210
+ "warning", "warn", "deprecated"
211
+ ]
212
+
213
+ # Analyze logs
214
+ for line in logs.split("\n"):
215
+ line_lower = line.lower()
216
+
217
+ # Check for errors
218
+ for pattern in error_patterns:
219
+ if pattern in line_lower:
220
+ analysis["error_count"] += 1
221
+ analysis["errors"].append(line)
222
+ break
223
+
224
+ # Check for warnings
225
+ for pattern in warning_patterns:
226
+ if pattern in line_lower:
227
+ analysis["warning_count"] += 1
228
+ analysis["warnings"].append(line)
229
+ break
230
+
231
+ return {
232
+ "pod_name": pod_name,
233
+ "namespace": namespace,
234
+ "container": container,
235
+ "analysis": analysis
236
+ }
237
+
238
+
239
+ # Convenience functions for direct import
240
+ _diagnostics = KubernetesDiagnostics()
241
+
242
+
243
+ def check_kubectl_installation() -> Dict[str, Any]:
244
+ """Check if kubectl is installed and accessible."""
245
+ try:
246
+ import shutil
247
+ kubectl_path = shutil.which("kubectl")
248
+ if not kubectl_path:
249
+ return {
250
+ "installed": False,
251
+ "error": "kubectl not found in PATH"
252
+ }
253
+
254
+ import subprocess
255
+ result = subprocess.run(
256
+ ["kubectl", "version", "--client", "-o", "json"],
257
+ capture_output=True,
258
+ text=True,
259
+ timeout=10
260
+ )
261
+
262
+ if result.returncode != 0:
263
+ return {
264
+ "installed": True,
265
+ "path": kubectl_path,
266
+ "error": result.stderr
267
+ }
268
+
269
+ import json
270
+ version_info = json.loads(result.stdout)
271
+ return {
272
+ "installed": True,
273
+ "path": kubectl_path,
274
+ "version": version_info.get("clientVersion", {})
275
+ }
276
+ except Exception as e:
277
+ return {
278
+ "installed": False,
279
+ "error": str(e)
280
+ }
281
+
282
+
283
+ def check_cluster_connection() -> Dict[str, Any]:
284
+ """Check if we can connect to a Kubernetes cluster."""
285
+ try:
286
+ import subprocess
287
+ result = subprocess.run(
288
+ ["kubectl", "cluster-info"],
289
+ capture_output=True,
290
+ text=True,
291
+ timeout=10
292
+ )
293
+
294
+ if result.returncode != 0:
295
+ return {
296
+ "connected": False,
297
+ "error": result.stderr
298
+ }
299
+
300
+ return {
301
+ "connected": True,
302
+ "cluster_info": result.stdout.strip()
303
+ }
304
+ except Exception as e:
305
+ return {
306
+ "connected": False,
307
+ "error": str(e)
308
+ }
309
+
310
+
311
+ def run_diagnostics() -> Dict[str, Any]:
312
+ """Run comprehensive diagnostics."""
313
+ kubectl_check = check_kubectl_installation()
314
+ cluster_check = check_cluster_connection()
315
+
316
+ diagnostics = {
317
+ "kubectl": kubectl_check,
318
+ "cluster": cluster_check,
319
+ "overall_status": "healthy" if (
320
+ kubectl_check.get("installed") and
321
+ cluster_check.get("connected")
322
+ ) else "unhealthy"
323
+ }
324
+
325
+ if diagnostics["overall_status"] == "healthy":
326
+ # Additional checks
327
+ try:
328
+ import subprocess
329
+
330
+ # Check nodes
331
+ nodes_result = subprocess.run(
332
+ ["kubectl", "get", "nodes", "-o", "json"],
333
+ capture_output=True,
334
+ text=True,
335
+ timeout=10
336
+ )
337
+ if nodes_result.returncode == 0:
338
+ import json
339
+ nodes_data = json.loads(nodes_result.stdout)
340
+ nodes_count = len(nodes_data.get("items", []))
341
+ ready_nodes = sum(
342
+ 1 for node in nodes_data.get("items", [])
343
+ if any(
344
+ c.get("type") == "Ready" and c.get("status") == "True"
345
+ for c in node.get("status", {}).get("conditions", [])
346
+ )
347
+ )
348
+ diagnostics["nodes"] = {
349
+ "total": nodes_count,
350
+ "ready": ready_nodes
351
+ }
352
+ except Exception as e:
353
+ diagnostics["nodes_error"] = str(e)
354
+
355
+ return diagnostics
@@ -0,0 +1,289 @@
1
+ """
2
+ Kubernetes configuration loader utility.
3
+
4
+ Handles both in-cluster and out-of-cluster (kubeconfig) configurations.
5
+
6
+ This module patches the kubernetes.config.load_kube_config function to
7
+ automatically try in-cluster config first when running inside a pod.
8
+ """
9
+
10
+ import os
11
+ import logging
12
+
13
+ logger = logging.getLogger("mcp-server")
14
+
15
+ _config_loaded = False
16
+ _original_load_kube_config = None
17
+
18
+
19
+ def load_kubernetes_config():
20
+ """Load Kubernetes configuration.
21
+
22
+ Tries in-cluster config first (when running inside a pod),
23
+ then falls back to kubeconfig file.
24
+
25
+ Returns:
26
+ bool: True if config loaded successfully, False otherwise
27
+ """
28
+ global _config_loaded
29
+
30
+ if _config_loaded:
31
+ return True
32
+
33
+ from kubernetes import config
34
+ from kubernetes.config.config_exception import ConfigException
35
+
36
+ # Try in-cluster config first (for pods running in Kubernetes)
37
+ try:
38
+ config.load_incluster_config()
39
+ logger.info("Loaded in-cluster Kubernetes configuration")
40
+ _config_loaded = True
41
+ return True
42
+ except ConfigException:
43
+ logger.debug("Not running in-cluster, trying kubeconfig...")
44
+
45
+ # Fall back to kubeconfig file
46
+ try:
47
+ kubeconfig_path = os.environ.get('KUBECONFIG', '~/.kube/config')
48
+ kubeconfig_path = os.path.expanduser(kubeconfig_path)
49
+ config.load_kube_config(config_file=kubeconfig_path)
50
+ logger.info(f"Loaded kubeconfig from {kubeconfig_path}")
51
+ _config_loaded = True
52
+ return True
53
+ except ConfigException as e:
54
+ logger.error(f"Failed to load Kubernetes config: {e}")
55
+ return False
56
+
57
+
58
+ def _patched_load_kube_config(*args, **kwargs):
59
+ """Patched version of load_kube_config that tries in-cluster first."""
60
+ global _config_loaded, _original_load_kube_config
61
+
62
+ if _config_loaded:
63
+ return
64
+
65
+ from kubernetes.config.config_exception import ConfigException
66
+
67
+ # Try in-cluster config first
68
+ try:
69
+ from kubernetes import config
70
+ config.load_incluster_config()
71
+ logger.info("Loaded in-cluster Kubernetes configuration")
72
+ _config_loaded = True
73
+ return
74
+ except ConfigException:
75
+ pass
76
+
77
+ # Fall back to original behavior
78
+ if _original_load_kube_config:
79
+ _original_load_kube_config(*args, **kwargs)
80
+ _config_loaded = True
81
+
82
+
83
+ def patch_kubernetes_config():
84
+ """Patch the kubernetes config module to support in-cluster config.
85
+
86
+ This should be called early in the application startup.
87
+ """
88
+ global _original_load_kube_config
89
+
90
+ try:
91
+ from kubernetes import config
92
+
93
+ if _original_load_kube_config is None:
94
+ _original_load_kube_config = config.load_kube_config
95
+ config.load_kube_config = _patched_load_kube_config
96
+ logger.debug("Patched kubernetes.config.load_kube_config for in-cluster support")
97
+ except ImportError:
98
+ logger.debug("kubernetes package not available for patching")
99
+
100
+
101
+ # Auto-patch when this module is imported
102
+ patch_kubernetes_config()
103
+
104
+
105
+ def get_k8s_client():
106
+ """Get a configured Kubernetes API client.
107
+
108
+ Returns:
109
+ kubernetes.client.CoreV1Api: Configured Kubernetes client
110
+
111
+ Raises:
112
+ RuntimeError: If Kubernetes config cannot be loaded
113
+ """
114
+ from kubernetes import client
115
+
116
+ if not load_kubernetes_config():
117
+ raise RuntimeError("Invalid kube-config file. No configuration found.")
118
+
119
+ return client.CoreV1Api()
120
+
121
+
122
+ def get_apps_client():
123
+ """Get a configured Kubernetes Apps API client.
124
+
125
+ Returns:
126
+ kubernetes.client.AppsV1Api: Configured Apps API client
127
+
128
+ Raises:
129
+ RuntimeError: If Kubernetes config cannot be loaded
130
+ """
131
+ from kubernetes import client
132
+
133
+ if not load_kubernetes_config():
134
+ raise RuntimeError("Invalid kube-config file. No configuration found.")
135
+
136
+ return client.AppsV1Api()
137
+
138
+
139
+ def get_rbac_client():
140
+ """Get a configured Kubernetes RBAC API client.
141
+
142
+ Returns:
143
+ kubernetes.client.RbacAuthorizationV1Api: Configured RBAC client
144
+
145
+ Raises:
146
+ RuntimeError: If Kubernetes config cannot be loaded
147
+ """
148
+ from kubernetes import client
149
+
150
+ if not load_kubernetes_config():
151
+ raise RuntimeError("Invalid kube-config file. No configuration found.")
152
+
153
+ return client.RbacAuthorizationV1Api()
154
+
155
+
156
+ def get_networking_client():
157
+ """Get a configured Kubernetes Networking API client.
158
+
159
+ Returns:
160
+ kubernetes.client.NetworkingV1Api: Configured Networking client
161
+
162
+ Raises:
163
+ RuntimeError: If Kubernetes config cannot be loaded
164
+ """
165
+ from kubernetes import client
166
+
167
+ if not load_kubernetes_config():
168
+ raise RuntimeError("Invalid kube-config file. No configuration found.")
169
+
170
+ return client.NetworkingV1Api()
171
+
172
+
173
+ def get_storage_client():
174
+ """Get a configured Kubernetes Storage API client.
175
+
176
+ Returns:
177
+ kubernetes.client.StorageV1Api: Configured Storage client
178
+
179
+ Raises:
180
+ RuntimeError: If Kubernetes config cannot be loaded
181
+ """
182
+ from kubernetes import client
183
+
184
+ if not load_kubernetes_config():
185
+ raise RuntimeError("Invalid kube-config file. No configuration found.")
186
+
187
+ return client.StorageV1Api()
188
+
189
+
190
+ def get_batch_client():
191
+ """Get a configured Kubernetes Batch API client.
192
+
193
+ Returns:
194
+ kubernetes.client.BatchV1Api: Configured Batch client
195
+
196
+ Raises:
197
+ RuntimeError: If Kubernetes config cannot be loaded
198
+ """
199
+ from kubernetes import client
200
+
201
+ if not load_kubernetes_config():
202
+ raise RuntimeError("Invalid kube-config file. No configuration found.")
203
+
204
+ return client.BatchV1Api()
205
+
206
+
207
+ def get_autoscaling_client():
208
+ """Get a configured Kubernetes Autoscaling API client.
209
+
210
+ Returns:
211
+ kubernetes.client.AutoscalingV1Api: Configured Autoscaling client
212
+
213
+ Raises:
214
+ RuntimeError: If Kubernetes config cannot be loaded
215
+ """
216
+ from kubernetes import client
217
+
218
+ if not load_kubernetes_config():
219
+ raise RuntimeError("Invalid kube-config file. No configuration found.")
220
+
221
+ return client.AutoscalingV1Api()
222
+
223
+
224
+ def get_policy_client():
225
+ """Get a configured Kubernetes Policy API client.
226
+
227
+ Returns:
228
+ kubernetes.client.PolicyV1Api: Configured Policy client
229
+
230
+ Raises:
231
+ RuntimeError: If Kubernetes config cannot be loaded
232
+ """
233
+ from kubernetes import client
234
+
235
+ if not load_kubernetes_config():
236
+ raise RuntimeError("Invalid kube-config file. No configuration found.")
237
+
238
+ return client.PolicyV1Api()
239
+
240
+
241
+ def get_custom_objects_client():
242
+ """Get a configured Kubernetes Custom Objects API client.
243
+
244
+ Returns:
245
+ kubernetes.client.CustomObjectsApi: Configured Custom Objects client
246
+
247
+ Raises:
248
+ RuntimeError: If Kubernetes config cannot be loaded
249
+ """
250
+ from kubernetes import client
251
+
252
+ if not load_kubernetes_config():
253
+ raise RuntimeError("Invalid kube-config file. No configuration found.")
254
+
255
+ return client.CustomObjectsApi()
256
+
257
+
258
+ def get_version_client():
259
+ """Get a configured Kubernetes Version API client.
260
+
261
+ Returns:
262
+ kubernetes.client.VersionApi: Configured Version client
263
+
264
+ Raises:
265
+ RuntimeError: If Kubernetes config cannot be loaded
266
+ """
267
+ from kubernetes import client
268
+
269
+ if not load_kubernetes_config():
270
+ raise RuntimeError("Invalid kube-config file. No configuration found.")
271
+
272
+ return client.VersionApi()
273
+
274
+
275
+ def get_admissionregistration_client():
276
+ """Get a configured Kubernetes Admission Registration API client.
277
+
278
+ Returns:
279
+ kubernetes.client.AdmissionregistrationV1Api: Configured Admission client
280
+
281
+ Raises:
282
+ RuntimeError: If Kubernetes config cannot be loaded
283
+ """
284
+ from kubernetes import client
285
+
286
+ if not load_kubernetes_config():
287
+ raise RuntimeError("Invalid kube-config file. No configuration found.")
288
+
289
+ return client.AdmissionregistrationV1Api()