kubectl-mcp-server 1.14.0__py3-none-any.whl → 1.16.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 (37) hide show
  1. kubectl_mcp_server-1.16.0.dist-info/METADATA +1047 -0
  2. kubectl_mcp_server-1.16.0.dist-info/RECORD +61 -0
  3. kubectl_mcp_tool/__init__.py +1 -1
  4. kubectl_mcp_tool/crd_detector.py +247 -0
  5. kubectl_mcp_tool/k8s_config.py +304 -63
  6. kubectl_mcp_tool/mcp_server.py +27 -0
  7. kubectl_mcp_tool/tools/__init__.py +20 -0
  8. kubectl_mcp_tool/tools/backup.py +881 -0
  9. kubectl_mcp_tool/tools/capi.py +727 -0
  10. kubectl_mcp_tool/tools/certs.py +709 -0
  11. kubectl_mcp_tool/tools/cilium.py +582 -0
  12. kubectl_mcp_tool/tools/cluster.py +395 -121
  13. kubectl_mcp_tool/tools/core.py +157 -60
  14. kubectl_mcp_tool/tools/cost.py +97 -41
  15. kubectl_mcp_tool/tools/deployments.py +173 -56
  16. kubectl_mcp_tool/tools/diagnostics.py +40 -13
  17. kubectl_mcp_tool/tools/gitops.py +552 -0
  18. kubectl_mcp_tool/tools/helm.py +133 -46
  19. kubectl_mcp_tool/tools/keda.py +464 -0
  20. kubectl_mcp_tool/tools/kiali.py +652 -0
  21. kubectl_mcp_tool/tools/kubevirt.py +803 -0
  22. kubectl_mcp_tool/tools/networking.py +106 -32
  23. kubectl_mcp_tool/tools/operations.py +176 -50
  24. kubectl_mcp_tool/tools/pods.py +162 -50
  25. kubectl_mcp_tool/tools/policy.py +554 -0
  26. kubectl_mcp_tool/tools/rollouts.py +790 -0
  27. kubectl_mcp_tool/tools/security.py +89 -36
  28. kubectl_mcp_tool/tools/storage.py +35 -16
  29. tests/test_browser.py +2 -2
  30. tests/test_ecosystem.py +331 -0
  31. tests/test_tools.py +73 -10
  32. kubectl_mcp_server-1.14.0.dist-info/METADATA +0 -780
  33. kubectl_mcp_server-1.14.0.dist-info/RECORD +0 -49
  34. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/WHEEL +0 -0
  35. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/entry_points.txt +0 -0
  36. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/licenses/LICENSE +0 -0
  37. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/top_level.txt +0 -0
@@ -1,33 +1,63 @@
1
+ """
2
+ Cluster and context management tools for kubectl-mcp-server.
3
+
4
+ All tools support multi-cluster operations via the optional 'context' parameter.
5
+ """
6
+
7
+ import json
1
8
  import logging
9
+ import os
2
10
  import subprocess
3
- from typing import Any, Dict, Optional
11
+ from typing import Any, Dict, List, Optional
4
12
 
5
13
  from mcp.types import ToolAnnotations
6
14
 
15
+ from kubectl_mcp_tool.k8s_config import (
16
+ get_k8s_client,
17
+ get_version_client,
18
+ get_admissionregistration_client,
19
+ list_contexts,
20
+ get_active_context,
21
+ )
22
+
7
23
  logger = logging.getLogger("mcp-server")
8
24
 
9
25
 
26
+ def _get_kubectl_context_args(context: str = "") -> List[str]:
27
+ """Get kubectl context arguments."""
28
+ if context:
29
+ return ["--context", context]
30
+ return []
31
+
32
+
10
33
  def register_cluster_tools(server, non_destructive: bool):
11
34
  """Register cluster and context management tools."""
12
35
 
36
+ # ========== Config Toolset ==========
37
+
13
38
  @server.tool(
14
39
  annotations=ToolAnnotations(
15
- title="Switch Context",
16
- destructiveHint=True,
40
+ title="List Contexts",
41
+ readOnlyHint=True,
17
42
  ),
18
43
  )
19
- def switch_context(context_name: str) -> Dict[str, Any]:
20
- """Switch to a different kubectl context."""
44
+ def list_contexts_tool() -> Dict[str, Any]:
45
+ """List all available kubectl contexts with detailed info.
46
+
47
+ Returns all contexts from kubeconfig with cluster, user, namespace info.
48
+ """
21
49
  try:
22
- result = subprocess.run(
23
- ["kubectl", "config", "use-context", context_name],
24
- capture_output=True, text=True, timeout=10
25
- )
26
- if result.returncode == 0:
27
- return {"success": True, "message": f"Switched to context: {context_name}"}
28
- return {"success": False, "error": result.stderr}
50
+ contexts = list_contexts()
51
+ active = get_active_context()
52
+
53
+ return {
54
+ "success": True,
55
+ "contexts": contexts,
56
+ "active_context": active,
57
+ "total": len(contexts)
58
+ }
29
59
  except Exception as e:
30
- logger.error(f"Error switching context: {e}")
60
+ logger.error(f"Error listing contexts: {e}")
31
61
  return {"success": False, "error": str(e)}
32
62
 
33
63
  @server.tool(
@@ -39,73 +69,111 @@ def register_cluster_tools(server, non_destructive: bool):
39
69
  def get_current_context() -> Dict[str, Any]:
40
70
  """Get the current kubectl context."""
41
71
  try:
42
- result = subprocess.run(
43
- ["kubectl", "config", "current-context"],
44
- capture_output=True, text=True, timeout=10
45
- )
46
- if result.returncode == 0:
47
- return {"success": True, "context": result.stdout.strip()}
48
- return {"success": False, "error": result.stderr}
72
+ active = get_active_context()
73
+ if active:
74
+ return {"success": True, "context": active}
75
+ return {"success": False, "error": "No active context found"}
49
76
  except Exception as e:
50
77
  logger.error(f"Error getting current context: {e}")
51
78
  return {"success": False, "error": str(e)}
52
79
 
53
80
  @server.tool(
54
81
  annotations=ToolAnnotations(
55
- title="List Contexts",
82
+ title="Get Context Details",
56
83
  readOnlyHint=True,
57
84
  ),
58
85
  )
59
- def list_contexts() -> Dict[str, Any]:
60
- """List all available kubectl contexts."""
86
+ def get_context_details(context_name: str) -> Dict[str, Any]:
87
+ """Get details about a specific context.
88
+
89
+ Args:
90
+ context_name: Name of the context to get details for
91
+ """
61
92
  try:
62
- from kubernetes import config
63
- contexts, active_context = config.list_kube_config_contexts()
64
- return {
65
- "success": True,
66
- "contexts": [
67
- {
68
- "name": ctx.get("name"),
69
- "cluster": ctx.get("context", {}).get("cluster"),
70
- "user": ctx.get("context", {}).get("user"),
71
- "namespace": ctx.get("context", {}).get("namespace", "default"),
72
- "active": ctx.get("name") == (active_context.get("name") if active_context else None)
93
+ contexts = list_contexts()
94
+
95
+ for ctx in contexts:
96
+ if ctx.get("name") == context_name:
97
+ return {
98
+ "success": True,
99
+ "context": ctx
73
100
  }
74
- for ctx in contexts
75
- ],
76
- "active_context": active_context.get("name") if active_context else None
77
- }
101
+
102
+ return {"success": False, "error": f"Context '{context_name}' not found"}
78
103
  except Exception as e:
79
- logger.error(f"Error listing contexts: {e}")
104
+ logger.error(f"Error getting context details: {e}")
80
105
  return {"success": False, "error": str(e)}
81
106
 
82
107
  @server.tool(
83
108
  annotations=ToolAnnotations(
84
- title="Get Context Details",
109
+ title="View Kubeconfig",
85
110
  readOnlyHint=True,
86
111
  ),
87
112
  )
88
- def get_context_details(context_name: str) -> Dict[str, Any]:
89
- """Get details about a specific context."""
113
+ def kubeconfig_view(minify: bool = True) -> Dict[str, Any]:
114
+ """View kubeconfig file contents (sanitized - no secrets).
115
+
116
+ Args:
117
+ minify: If True, show only current context info. If False, show all.
118
+ """
90
119
  try:
91
- from kubernetes import config
92
- contexts, _ = config.list_kube_config_contexts()
120
+ cmd = ["kubectl", "config", "view"]
121
+ if minify:
122
+ cmd.append("--minify")
123
+ cmd.extend(["--raw=false", "-o", "json"]) # raw=false strips sensitive data
124
+
125
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
126
+
127
+ if result.returncode == 0:
128
+ try:
129
+ config_data = json.loads(result.stdout)
130
+ # Additional sanitization
131
+ for user in config_data.get("users", []):
132
+ if "user" in user:
133
+ user_data = user["user"]
134
+ for sensitive in ["client-certificate-data", "client-key-data", "token"]:
135
+ if sensitive in user_data:
136
+ user_data[sensitive] = "[REDACTED]"
93
137
 
94
- for ctx in contexts:
95
- if ctx.get("name") == context_name:
96
138
  return {
97
139
  "success": True,
98
- "context": {
99
- "name": ctx.get("name"),
100
- "cluster": ctx.get("context", {}).get("cluster"),
101
- "user": ctx.get("context", {}).get("user"),
102
- "namespace": ctx.get("context", {}).get("namespace", "default")
103
- }
140
+ "minified": minify,
141
+ "kubeconfig": config_data
104
142
  }
143
+ except json.JSONDecodeError:
144
+ return {"success": True, "kubeconfig": result.stdout}
105
145
 
106
- return {"success": False, "error": f"Context '{context_name}' not found"}
146
+ return {"success": False, "error": result.stderr}
107
147
  except Exception as e:
108
- logger.error(f"Error getting context details: {e}")
148
+ logger.error(f"Error viewing kubeconfig: {e}")
149
+ return {"success": False, "error": str(e)}
150
+
151
+ @server.tool(
152
+ annotations=ToolAnnotations(
153
+ title="Switch Context",
154
+ destructiveHint=True,
155
+ ),
156
+ )
157
+ def switch_context(context_name: str) -> Dict[str, Any]:
158
+ """Switch to a different kubectl context (changes default context).
159
+
160
+ Args:
161
+ context_name: Name of the context to switch to
162
+
163
+ Note: This changes the default context in kubeconfig. For multi-cluster
164
+ operations without changing default, use the 'context' parameter on
165
+ individual tools instead.
166
+ """
167
+ try:
168
+ result = subprocess.run(
169
+ ["kubectl", "config", "use-context", context_name],
170
+ capture_output=True, text=True, timeout=10
171
+ )
172
+ if result.returncode == 0:
173
+ return {"success": True, "message": f"Switched to context: {context_name}"}
174
+ return {"success": False, "error": result.stderr}
175
+ except Exception as e:
176
+ logger.error(f"Error switching context: {e}")
109
177
  return {"success": False, "error": str(e)}
110
178
 
111
179
  @server.tool(
@@ -114,8 +182,16 @@ def register_cluster_tools(server, non_destructive: bool):
114
182
  destructiveHint=True,
115
183
  ),
116
184
  )
117
- def set_namespace_for_context(namespace: str, context_name: Optional[str] = None) -> Dict[str, Any]:
118
- """Set the default namespace for a context."""
185
+ def set_namespace_for_context(
186
+ namespace: str,
187
+ context_name: Optional[str] = None
188
+ ) -> Dict[str, Any]:
189
+ """Set the default namespace for a context.
190
+
191
+ Args:
192
+ namespace: Namespace to set as default
193
+ context_name: Context to modify (uses current context if not specified)
194
+ """
119
195
  try:
120
196
  cmd = ["kubectl", "config", "set-context"]
121
197
  if context_name:
@@ -132,45 +208,98 @@ def register_cluster_tools(server, non_destructive: bool):
132
208
  logger.error(f"Error setting namespace: {e}")
133
209
  return {"success": False, "error": str(e)}
134
210
 
211
+ # ========== Cluster Info Tools ==========
212
+
135
213
  @server.tool(
136
214
  annotations=ToolAnnotations(
137
- title="List Kubeconfig Contexts",
215
+ title="Get Cluster Info",
138
216
  readOnlyHint=True,
139
217
  ),
140
218
  )
141
- def list_kubeconfig_contexts() -> Dict[str, Any]:
142
- """List all contexts from kubeconfig with detailed info."""
219
+ def get_cluster_info(context: str = "") -> Dict[str, Any]:
220
+ """Get cluster information.
221
+
222
+ Args:
223
+ context: Kubernetes context to use (uses current context if not specified)
224
+ """
143
225
  try:
226
+ ctx_args = _get_kubectl_context_args(context)
144
227
  result = subprocess.run(
145
- ["kubectl", "config", "get-contexts", "-o", "name"],
146
- capture_output=True, text=True, timeout=10
228
+ ["kubectl"] + ctx_args + ["cluster-info"],
229
+ capture_output=True, text=True, timeout=30
147
230
  )
148
231
  if result.returncode == 0:
149
- contexts = [c.strip() for c in result.stdout.strip().split("\n") if c.strip()]
150
- return {"success": True, "contexts": contexts}
232
+ return {
233
+ "success": True,
234
+ "context": context or "current",
235
+ "info": result.stdout
236
+ }
151
237
  return {"success": False, "error": result.stderr}
152
238
  except Exception as e:
153
- logger.error(f"Error listing kubeconfig contexts: {e}")
239
+ logger.error(f"Error getting cluster info: {e}")
154
240
  return {"success": False, "error": str(e)}
155
241
 
156
242
  @server.tool(
157
243
  annotations=ToolAnnotations(
158
- title="Get Cluster Info",
244
+ title="Get Cluster Version Info",
159
245
  readOnlyHint=True,
160
246
  ),
161
247
  )
162
- def get_cluster_info() -> Dict[str, Any]:
163
- """Get cluster information."""
248
+ def get_cluster_version(context: str = "") -> Dict[str, Any]:
249
+ """Get Kubernetes cluster version information.
250
+
251
+ Args:
252
+ context: Kubernetes context to use (uses current context if not specified)
253
+ """
164
254
  try:
255
+ version_api = get_version_client(context)
256
+ version_info = version_api.get_code()
257
+
258
+ return {
259
+ "success": True,
260
+ "context": context or "current",
261
+ "version": {
262
+ "gitVersion": version_info.git_version,
263
+ "major": version_info.major,
264
+ "minor": version_info.minor,
265
+ "platform": version_info.platform,
266
+ "buildDate": version_info.build_date,
267
+ "goVersion": version_info.go_version,
268
+ "compiler": version_info.compiler
269
+ }
270
+ }
271
+ except Exception as e:
272
+ logger.error(f"Error getting cluster version: {e}")
273
+ return {"success": False, "error": str(e)}
274
+
275
+ @server.tool(
276
+ annotations=ToolAnnotations(
277
+ title="Health Check",
278
+ readOnlyHint=True,
279
+ ),
280
+ )
281
+ def health_check(context: str = "") -> Dict[str, Any]:
282
+ """Perform a cluster health check.
283
+
284
+ Args:
285
+ context: Kubernetes context to use (uses current context if not specified)
286
+ """
287
+ try:
288
+ ctx_args = _get_kubectl_context_args(context)
165
289
  result = subprocess.run(
166
- ["kubectl", "cluster-info"],
290
+ ["kubectl"] + ctx_args + ["get", "componentstatuses", "-o", "json"],
167
291
  capture_output=True, text=True, timeout=30
168
292
  )
169
293
  if result.returncode == 0:
170
- return {"success": True, "info": result.stdout}
294
+ data = json.loads(result.stdout)
295
+ return {
296
+ "success": True,
297
+ "context": context or "current",
298
+ "components": data.get("items", [])
299
+ }
171
300
  return {"success": False, "error": result.stderr}
172
301
  except Exception as e:
173
- logger.error(f"Error getting cluster info: {e}")
302
+ logger.error(f"Error performing health check: {e}")
174
303
  return {"success": False, "error": str(e)}
175
304
 
176
305
  @server.tool(
@@ -179,14 +308,27 @@ def register_cluster_tools(server, non_destructive: bool):
179
308
  readOnlyHint=True,
180
309
  ),
181
310
  )
182
- def kubectl_explain(resource: str) -> Dict[str, Any]:
183
- """Explain a Kubernetes resource."""
311
+ def kubectl_explain(
312
+ resource: str,
313
+ context: str = ""
314
+ ) -> Dict[str, Any]:
315
+ """Explain a Kubernetes resource.
316
+
317
+ Args:
318
+ resource: Resource type to explain (e.g., pods, deployments.spec)
319
+ context: Kubernetes context to use (uses current context if not specified)
320
+ """
184
321
  try:
322
+ ctx_args = _get_kubectl_context_args(context)
185
323
  result = subprocess.run(
186
- ["kubectl", "explain", resource],
324
+ ["kubectl"] + ctx_args + ["explain", resource],
187
325
  capture_output=True, text=True, timeout=30
188
326
  )
189
- return {"success": result.returncode == 0, "output": result.stdout or result.stderr}
327
+ return {
328
+ "success": result.returncode == 0,
329
+ "context": context or "current",
330
+ "output": result.stdout or result.stderr
331
+ }
190
332
  except Exception as e:
191
333
  logger.error(f"Error explaining resource: {e}")
192
334
  return {"success": False, "error": str(e)}
@@ -197,68 +339,56 @@ def register_cluster_tools(server, non_destructive: bool):
197
339
  readOnlyHint=True,
198
340
  ),
199
341
  )
200
- def get_api_resources() -> Dict[str, Any]:
201
- """Get available API resources."""
342
+ def get_api_resources(context: str = "") -> Dict[str, Any]:
343
+ """Get available API resources.
344
+
345
+ Args:
346
+ context: Kubernetes context to use (uses current context if not specified)
347
+ """
202
348
  try:
349
+ ctx_args = _get_kubectl_context_args(context)
203
350
  result = subprocess.run(
204
- ["kubectl", "api-resources"],
351
+ ["kubectl"] + ctx_args + ["api-resources"],
205
352
  capture_output=True, text=True, timeout=30
206
353
  )
207
- return {"success": result.returncode == 0, "output": result.stdout or result.stderr}
354
+ return {
355
+ "success": result.returncode == 0,
356
+ "context": context or "current",
357
+ "output": result.stdout or result.stderr
358
+ }
208
359
  except Exception as e:
209
360
  logger.error(f"Error getting API resources: {e}")
210
361
  return {"success": False, "error": str(e)}
211
362
 
212
363
  @server.tool(
213
364
  annotations=ToolAnnotations(
214
- title="Health Check",
365
+ title="Get API Versions",
215
366
  readOnlyHint=True,
216
367
  ),
217
368
  )
218
- def health_check() -> Dict[str, Any]:
219
- """Perform a cluster health check."""
369
+ def get_api_versions(context: str = "") -> Dict[str, Any]:
370
+ """Get available API versions.
371
+
372
+ Args:
373
+ context: Kubernetes context to use (uses current context if not specified)
374
+ """
220
375
  try:
376
+ ctx_args = _get_kubectl_context_args(context)
221
377
  result = subprocess.run(
222
- ["kubectl", "get", "componentstatuses", "-o", "json"],
378
+ ["kubectl"] + ctx_args + ["api-versions"],
223
379
  capture_output=True, text=True, timeout=30
224
380
  )
225
381
  if result.returncode == 0:
226
- import json
227
- data = json.loads(result.stdout)
228
- return {"success": True, "components": data.get("items", [])}
229
- return {"success": False, "error": result.stderr}
230
- except Exception as e:
231
- logger.error(f"Error performing health check: {e}")
232
- return {"success": False, "error": str(e)}
233
-
234
- @server.tool(
235
- annotations=ToolAnnotations(
236
- title="Get Cluster Version Info",
237
- readOnlyHint=True,
238
- ),
239
- )
240
- def get_cluster_version() -> Dict[str, Any]:
241
- """Get Kubernetes cluster version information."""
242
- try:
243
- from kubernetes import client, config
244
- config.load_kube_config()
245
- version_api = client.VersionApi()
246
- version_info = version_api.get_code()
247
-
248
- return {
249
- "success": True,
250
- "version": {
251
- "gitVersion": version_info.git_version,
252
- "major": version_info.major,
253
- "minor": version_info.minor,
254
- "platform": version_info.platform,
255
- "buildDate": version_info.build_date,
256
- "goVersion": version_info.go_version,
257
- "compiler": version_info.compiler
382
+ versions = [v.strip() for v in result.stdout.strip().split("\n") if v.strip()]
383
+ return {
384
+ "success": True,
385
+ "context": context or "current",
386
+ "versions": versions,
387
+ "total": len(versions)
258
388
  }
259
- }
389
+ return {"success": False, "error": result.stderr}
260
390
  except Exception as e:
261
- logger.error(f"Error getting cluster version: {e}")
391
+ logger.error(f"Error getting API versions: {e}")
262
392
  return {"success": False, "error": str(e)}
263
393
 
264
394
  @server.tool(
@@ -267,18 +397,21 @@ def register_cluster_tools(server, non_destructive: bool):
267
397
  readOnlyHint=True,
268
398
  ),
269
399
  )
270
- def get_admission_webhooks() -> Dict[str, Any]:
271
- """Get admission webhooks configured in the cluster."""
400
+ def get_admission_webhooks(context: str = "") -> Dict[str, Any]:
401
+ """Get admission webhooks configured in the cluster.
402
+
403
+ Args:
404
+ context: Kubernetes context to use (uses current context if not specified)
405
+ """
272
406
  try:
273
- from kubernetes import client, config
274
- config.load_kube_config()
275
- api = client.AdmissionregistrationV1Api()
407
+ api = get_admissionregistration_client(context)
276
408
 
277
409
  validating = api.list_validating_webhook_configuration()
278
410
  mutating = api.list_mutating_webhook_configuration()
279
411
 
280
412
  return {
281
413
  "success": True,
414
+ "context": context or "current",
282
415
  "validatingWebhooks": [
283
416
  {
284
417
  "name": w.metadata.name,
@@ -313,3 +446,144 @@ def register_cluster_tools(server, non_destructive: bool):
313
446
  except Exception as e:
314
447
  logger.error(f"Error getting admission webhooks: {e}")
315
448
  return {"success": False, "error": str(e)}
449
+
450
+ @server.tool(
451
+ annotations=ToolAnnotations(
452
+ title="Check CRD Exists",
453
+ readOnlyHint=True,
454
+ ),
455
+ )
456
+ def check_crd_exists(
457
+ crd_name: str,
458
+ context: str = ""
459
+ ) -> Dict[str, Any]:
460
+ """Check if a Custom Resource Definition exists in the cluster.
461
+
462
+ Args:
463
+ crd_name: Name of the CRD to check (e.g., certificates.cert-manager.io)
464
+ context: Kubernetes context to use (uses current context if not specified)
465
+ """
466
+ try:
467
+ ctx_args = _get_kubectl_context_args(context)
468
+ result = subprocess.run(
469
+ ["kubectl"] + ctx_args + ["get", "crd", crd_name, "-o", "name"],
470
+ capture_output=True, text=True, timeout=10
471
+ )
472
+
473
+ exists = result.returncode == 0
474
+
475
+ return {
476
+ "success": True,
477
+ "context": context or "current",
478
+ "crd": crd_name,
479
+ "exists": exists
480
+ }
481
+ except Exception as e:
482
+ logger.error(f"Error checking CRD: {e}")
483
+ return {"success": False, "error": str(e)}
484
+
485
+ @server.tool(
486
+ annotations=ToolAnnotations(
487
+ title="List CRDs",
488
+ readOnlyHint=True,
489
+ ),
490
+ )
491
+ def list_crds(context: str = "") -> Dict[str, Any]:
492
+ """List all Custom Resource Definitions in the cluster.
493
+
494
+ Args:
495
+ context: Kubernetes context to use (uses current context if not specified)
496
+ """
497
+ try:
498
+ ctx_args = _get_kubectl_context_args(context)
499
+ result = subprocess.run(
500
+ ["kubectl"] + ctx_args + ["get", "crd", "-o", "json"],
501
+ capture_output=True, text=True, timeout=30
502
+ )
503
+
504
+ if result.returncode == 0:
505
+ data = json.loads(result.stdout)
506
+ crds = []
507
+ for item in data.get("items", []):
508
+ crds.append({
509
+ "name": item.get("metadata", {}).get("name"),
510
+ "group": item.get("spec", {}).get("group"),
511
+ "scope": item.get("spec", {}).get("scope"),
512
+ "versions": [
513
+ v.get("name") for v in item.get("spec", {}).get("versions", [])
514
+ ]
515
+ })
516
+
517
+ return {
518
+ "success": True,
519
+ "context": context or "current",
520
+ "crds": crds,
521
+ "total": len(crds)
522
+ }
523
+ return {"success": False, "error": result.stderr}
524
+ except Exception as e:
525
+ logger.error(f"Error listing CRDs: {e}")
526
+ return {"success": False, "error": str(e)}
527
+
528
+ @server.tool(
529
+ annotations=ToolAnnotations(
530
+ title="Get Nodes Summary",
531
+ readOnlyHint=True,
532
+ ),
533
+ )
534
+ def get_nodes_summary(context: str = "") -> Dict[str, Any]:
535
+ """Get summary of all nodes in the cluster.
536
+
537
+ Args:
538
+ context: Kubernetes context to use (uses current context if not specified)
539
+ """
540
+ try:
541
+ v1 = get_k8s_client(context)
542
+ nodes = v1.list_node()
543
+
544
+ summary = {
545
+ "total": len(nodes.items),
546
+ "ready": 0,
547
+ "notReady": 0,
548
+ "nodes": []
549
+ }
550
+
551
+ for node in nodes.items:
552
+ node_info = {
553
+ "name": node.metadata.name,
554
+ "status": "Unknown",
555
+ "roles": [],
556
+ "kubeletVersion": node.status.node_info.kubelet_version if node.status.node_info else None,
557
+ "os": node.status.node_info.os_image if node.status.node_info else None,
558
+ "capacity": {
559
+ "cpu": node.status.capacity.get("cpu") if node.status.capacity else None,
560
+ "memory": node.status.capacity.get("memory") if node.status.capacity else None,
561
+ "pods": node.status.capacity.get("pods") if node.status.capacity else None
562
+ }
563
+ }
564
+
565
+ # Get node status
566
+ for condition in (node.status.conditions or []):
567
+ if condition.type == "Ready":
568
+ node_info["status"] = "Ready" if condition.status == "True" else "NotReady"
569
+ if condition.status == "True":
570
+ summary["ready"] += 1
571
+ else:
572
+ summary["notReady"] += 1
573
+
574
+ # Get node roles
575
+ for label, value in (node.metadata.labels or {}).items():
576
+ if label.startswith("node-role.kubernetes.io/"):
577
+ role = label.split("/")[1]
578
+ node_info["roles"].append(role)
579
+
580
+ summary["nodes"].append(node_info)
581
+
582
+ return {
583
+ "success": True,
584
+ "context": context or "current",
585
+ "summary": summary
586
+ }
587
+ except Exception as e:
588
+ logger.error(f"Error getting nodes summary: {e}")
589
+ return {"success": False, "error": str(e)}