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
@@ -0,0 +1,464 @@
1
+ """KEDA autoscaling toolset for kubectl-mcp-server.
2
+
3
+ Provides tools for managing KEDA ScaledObjects, ScaledJobs, and TriggerAuthentications.
4
+ """
5
+
6
+ import subprocess
7
+ import json
8
+ from typing import Dict, Any, List
9
+
10
+ try:
11
+ from fastmcp import FastMCP
12
+ from fastmcp.tools import ToolAnnotations
13
+ except ImportError:
14
+ from mcp.server.fastmcp import FastMCP
15
+ from mcp.types import ToolAnnotations
16
+
17
+ from ..k8s_config import _get_kubectl_context_args
18
+ from ..crd_detector import crd_exists
19
+
20
+
21
+ SCALEDOBJECT_CRD = "scaledobjects.keda.sh"
22
+ SCALEDJOB_CRD = "scaledjobs.keda.sh"
23
+ TRIGGERAUTH_CRD = "triggerauthentications.keda.sh"
24
+ CLUSTERTRIGGERAUTH_CRD = "clustertriggerauthentications.keda.sh"
25
+
26
+
27
+ def _run_kubectl(args: List[str], context: str = "") -> Dict[str, Any]:
28
+ """Run kubectl command and return result."""
29
+ cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
30
+ try:
31
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
32
+ if result.returncode == 0:
33
+ return {"success": True, "output": result.stdout}
34
+ return {"success": False, "error": result.stderr}
35
+ except subprocess.TimeoutExpired:
36
+ return {"success": False, "error": "Command timed out"}
37
+ except Exception as e:
38
+ return {"success": False, "error": str(e)}
39
+
40
+
41
+ def _get_resources(kind: str, namespace: str = "", context: str = "", label_selector: str = "") -> List[Dict]:
42
+ """Get Kubernetes resources of a specific kind."""
43
+ args = ["get", kind, "-o", "json"]
44
+ if namespace:
45
+ args.extend(["-n", namespace])
46
+ else:
47
+ args.append("-A")
48
+ if label_selector:
49
+ args.extend(["-l", label_selector])
50
+
51
+ result = _run_kubectl(args, context)
52
+ if result["success"]:
53
+ try:
54
+ data = json.loads(result["output"])
55
+ return data.get("items", [])
56
+ except json.JSONDecodeError:
57
+ return []
58
+ return []
59
+
60
+
61
+ def keda_scaledobjects_list(
62
+ namespace: str = "",
63
+ context: str = "",
64
+ label_selector: str = ""
65
+ ) -> Dict[str, Any]:
66
+ """List KEDA ScaledObjects with their status.
67
+
68
+ Args:
69
+ namespace: Filter by namespace (empty for all namespaces)
70
+ context: Kubernetes context to use (optional)
71
+ label_selector: Label selector to filter resources
72
+
73
+ Returns:
74
+ List of ScaledObjects with their scaling status
75
+ """
76
+ if not crd_exists(SCALEDOBJECT_CRD, context):
77
+ return {
78
+ "success": False,
79
+ "error": "KEDA is not installed (scaledobjects.keda.sh CRD not found)"
80
+ }
81
+
82
+ objects = []
83
+ for item in _get_resources("scaledobjects.keda.sh", namespace, context, label_selector):
84
+ status = item.get("status", {})
85
+ spec = item.get("spec", {})
86
+ conditions = status.get("conditions", [])
87
+
88
+ ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
89
+ active_cond = next((c for c in conditions if c.get("type") == "Active"), {})
90
+
91
+ triggers = spec.get("triggers", [])
92
+ trigger_types = [t.get("type", "unknown") for t in triggers]
93
+
94
+ objects.append({
95
+ "name": item["metadata"]["name"],
96
+ "namespace": item["metadata"]["namespace"],
97
+ "ready": ready_cond.get("status") == "True",
98
+ "active": active_cond.get("status") == "True",
99
+ "status": ready_cond.get("reason", "Unknown"),
100
+ "message": ready_cond.get("message", ""),
101
+ "scale_target_ref": spec.get("scaleTargetRef", {}),
102
+ "min_replicas": spec.get("minReplicaCount", 0),
103
+ "max_replicas": spec.get("maxReplicaCount", 100),
104
+ "current_replicas": status.get("scaleTargetKind", {}).get("replicas"),
105
+ "trigger_types": trigger_types,
106
+ "triggers_count": len(triggers),
107
+ "paused_replicas": spec.get("pausedReplicaCount"),
108
+ "cooldown_period": spec.get("cooldownPeriod", 300),
109
+ "polling_interval": spec.get("pollingInterval", 30),
110
+ })
111
+
112
+ active_count = sum(1 for o in objects if o["active"])
113
+
114
+ return {
115
+ "context": context or "current",
116
+ "total": len(objects),
117
+ "active": active_count,
118
+ "scaledobjects": objects,
119
+ }
120
+
121
+
122
+ def keda_scaledobject_get(
123
+ name: str,
124
+ namespace: str,
125
+ context: str = ""
126
+ ) -> Dict[str, Any]:
127
+ """Get detailed information about a ScaledObject.
128
+
129
+ Args:
130
+ name: Name of the ScaledObject
131
+ namespace: Namespace of the ScaledObject
132
+ context: Kubernetes context to use (optional)
133
+
134
+ Returns:
135
+ Detailed ScaledObject information
136
+ """
137
+ if not crd_exists(SCALEDOBJECT_CRD, context):
138
+ return {"success": False, "error": "KEDA is not installed"}
139
+
140
+ args = ["get", "scaledobjects.keda.sh", name, "-n", namespace, "-o", "json"]
141
+ result = _run_kubectl(args, context)
142
+
143
+ if result["success"]:
144
+ try:
145
+ data = json.loads(result["output"])
146
+ return {
147
+ "success": True,
148
+ "context": context or "current",
149
+ "scaledobject": data,
150
+ }
151
+ except json.JSONDecodeError:
152
+ return {"success": False, "error": "Failed to parse response"}
153
+
154
+ return {"success": False, "error": result.get("error", "Unknown error")}
155
+
156
+
157
+ def keda_scaledjobs_list(
158
+ namespace: str = "",
159
+ context: str = "",
160
+ label_selector: str = ""
161
+ ) -> Dict[str, Any]:
162
+ """List KEDA ScaledJobs with their status.
163
+
164
+ Args:
165
+ namespace: Filter by namespace (empty for all namespaces)
166
+ context: Kubernetes context to use (optional)
167
+ label_selector: Label selector to filter resources
168
+
169
+ Returns:
170
+ List of ScaledJobs with their scaling status
171
+ """
172
+ if not crd_exists(SCALEDJOB_CRD, context):
173
+ return {
174
+ "success": False,
175
+ "error": "KEDA ScaledJobs CRD not found"
176
+ }
177
+
178
+ jobs = []
179
+ for item in _get_resources("scaledjobs.keda.sh", namespace, context, label_selector):
180
+ status = item.get("status", {})
181
+ spec = item.get("spec", {})
182
+ conditions = status.get("conditions", [])
183
+
184
+ ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
185
+
186
+ triggers = spec.get("triggers", [])
187
+ trigger_types = [t.get("type", "unknown") for t in triggers]
188
+
189
+ jobs.append({
190
+ "name": item["metadata"]["name"],
191
+ "namespace": item["metadata"]["namespace"],
192
+ "ready": ready_cond.get("status") == "True",
193
+ "status": ready_cond.get("reason", "Unknown"),
194
+ "message": ready_cond.get("message", ""),
195
+ "job_target_ref": spec.get("jobTargetRef", {}),
196
+ "min_replicas": spec.get("minReplicaCount", 0),
197
+ "max_replicas": spec.get("maxReplicaCount", 100),
198
+ "trigger_types": trigger_types,
199
+ "triggers_count": len(triggers),
200
+ "polling_interval": spec.get("pollingInterval", 30),
201
+ "successful_jobs_history": spec.get("successfulJobsHistoryLimit", 100),
202
+ "failed_jobs_history": spec.get("failedJobsHistoryLimit", 100),
203
+ })
204
+
205
+ return {
206
+ "context": context or "current",
207
+ "total": len(jobs),
208
+ "scaledjobs": jobs,
209
+ }
210
+
211
+
212
+ def keda_triggerauths_list(
213
+ namespace: str = "",
214
+ context: str = "",
215
+ include_cluster: bool = True
216
+ ) -> Dict[str, Any]:
217
+ """List KEDA TriggerAuthentications and ClusterTriggerAuthentications.
218
+
219
+ Args:
220
+ namespace: Filter by namespace (empty for all)
221
+ context: Kubernetes context to use (optional)
222
+ include_cluster: Include ClusterTriggerAuthentications
223
+
224
+ Returns:
225
+ List of trigger authentications
226
+ """
227
+ auths = []
228
+
229
+ if crd_exists(TRIGGERAUTH_CRD, context):
230
+ for item in _get_resources("triggerauthentications.keda.sh", namespace, context):
231
+ spec = item.get("spec", {})
232
+ secret_refs = spec.get("secretTargetRef", [])
233
+ env_refs = spec.get("env", [])
234
+
235
+ auths.append({
236
+ "name": item["metadata"]["name"],
237
+ "namespace": item["metadata"]["namespace"],
238
+ "kind": "TriggerAuthentication",
239
+ "secret_refs_count": len(secret_refs),
240
+ "env_refs_count": len(env_refs),
241
+ "has_pod_identity": "podIdentity" in spec,
242
+ "has_azure_identity": "azureKeyVault" in spec,
243
+ "has_hashicorp_vault": "hashiCorpVault" in spec,
244
+ })
245
+
246
+ if include_cluster and crd_exists(CLUSTERTRIGGERAUTH_CRD, context):
247
+ for item in _get_resources("clustertriggerauthentications.keda.sh", "", context):
248
+ spec = item.get("spec", {})
249
+ secret_refs = spec.get("secretTargetRef", [])
250
+ env_refs = spec.get("env", [])
251
+
252
+ auths.append({
253
+ "name": item["metadata"]["name"],
254
+ "namespace": "",
255
+ "kind": "ClusterTriggerAuthentication",
256
+ "secret_refs_count": len(secret_refs),
257
+ "env_refs_count": len(env_refs),
258
+ "has_pod_identity": "podIdentity" in spec,
259
+ "has_azure_identity": "azureKeyVault" in spec,
260
+ "has_hashicorp_vault": "hashiCorpVault" in spec,
261
+ })
262
+
263
+ return {
264
+ "context": context or "current",
265
+ "total": len(auths),
266
+ "authentications": auths,
267
+ }
268
+
269
+
270
+ def keda_triggerauth_get(
271
+ name: str,
272
+ namespace: str = "",
273
+ kind: str = "TriggerAuthentication",
274
+ context: str = ""
275
+ ) -> Dict[str, Any]:
276
+ """Get detailed information about a TriggerAuthentication.
277
+
278
+ Args:
279
+ name: Name of the TriggerAuthentication
280
+ namespace: Namespace (only for TriggerAuthentication, not ClusterTriggerAuthentication)
281
+ kind: TriggerAuthentication or ClusterTriggerAuthentication
282
+ context: Kubernetes context to use (optional)
283
+
284
+ Returns:
285
+ Detailed TriggerAuthentication information
286
+ """
287
+ if kind.lower() == "clustertriggerauthentication":
288
+ crd = "clustertriggerauthentications.keda.sh"
289
+ args = ["get", crd, name, "-o", "json"]
290
+ else:
291
+ crd = "triggerauthentications.keda.sh"
292
+ args = ["get", crd, name, "-n", namespace, "-o", "json"]
293
+
294
+ if not crd_exists(crd, context):
295
+ return {"success": False, "error": f"{crd} not found"}
296
+
297
+ result = _run_kubectl(args, context)
298
+
299
+ if result["success"]:
300
+ try:
301
+ data = json.loads(result["output"])
302
+ return {
303
+ "success": True,
304
+ "context": context or "current",
305
+ "authentication": data,
306
+ }
307
+ except json.JSONDecodeError:
308
+ return {"success": False, "error": "Failed to parse response"}
309
+
310
+ return {"success": False, "error": result.get("error", "Unknown error")}
311
+
312
+
313
+ def keda_hpa_list(
314
+ namespace: str = "",
315
+ context: str = "",
316
+ label_selector: str = ""
317
+ ) -> Dict[str, Any]:
318
+ """List HPAs managed by KEDA.
319
+
320
+ Args:
321
+ namespace: Filter by namespace (empty for all namespaces)
322
+ context: Kubernetes context to use (optional)
323
+ label_selector: Label selector to filter resources
324
+
325
+ Returns:
326
+ List of KEDA-managed HPAs
327
+ """
328
+ # KEDA creates HPAs with specific labels
329
+ keda_label = "scaledobject.keda.sh/name"
330
+ selector = label_selector if label_selector else ""
331
+
332
+ args = ["get", "hpa", "-o", "json"]
333
+ if namespace:
334
+ args.extend(["-n", namespace])
335
+ else:
336
+ args.append("-A")
337
+ if selector:
338
+ args.extend(["-l", selector])
339
+
340
+ result = _run_kubectl(args, context)
341
+ if not result["success"]:
342
+ return {"success": False, "error": result.get("error", "Failed to list HPAs")}
343
+
344
+ try:
345
+ data = json.loads(result["output"])
346
+ items = data.get("items", [])
347
+ except json.JSONDecodeError:
348
+ return {"success": False, "error": "Failed to parse response"}
349
+
350
+ hpas = []
351
+ for item in items:
352
+ labels = item.get("metadata", {}).get("labels", {})
353
+ # Filter to KEDA-managed HPAs
354
+ if keda_label not in labels and "app.kubernetes.io/managed-by" not in labels:
355
+ continue
356
+ if labels.get("app.kubernetes.io/managed-by") != "keda-operator":
357
+ if keda_label not in labels:
358
+ continue
359
+
360
+ spec = item.get("spec", {})
361
+ status = item.get("status", {})
362
+
363
+ hpas.append({
364
+ "name": item["metadata"]["name"],
365
+ "namespace": item["metadata"]["namespace"],
366
+ "scaledobject": labels.get(keda_label, ""),
367
+ "min_replicas": spec.get("minReplicas", 1),
368
+ "max_replicas": spec.get("maxReplicas", 10),
369
+ "current_replicas": status.get("currentReplicas", 0),
370
+ "desired_replicas": status.get("desiredReplicas", 0),
371
+ "current_metrics": status.get("currentMetrics", []),
372
+ "conditions": status.get("conditions", []),
373
+ })
374
+
375
+ return {
376
+ "context": context or "current",
377
+ "total": len(hpas),
378
+ "hpas": hpas,
379
+ }
380
+
381
+
382
+ def keda_detect(context: str = "") -> Dict[str, Any]:
383
+ """Detect if KEDA is installed and its components.
384
+
385
+ Args:
386
+ context: Kubernetes context to use (optional)
387
+
388
+ Returns:
389
+ Detection results for KEDA
390
+ """
391
+ return {
392
+ "context": context or "current",
393
+ "installed": crd_exists(SCALEDOBJECT_CRD, context),
394
+ "crds": {
395
+ "scaledobjects": crd_exists(SCALEDOBJECT_CRD, context),
396
+ "scaledjobs": crd_exists(SCALEDJOB_CRD, context),
397
+ "triggerauthentications": crd_exists(TRIGGERAUTH_CRD, context),
398
+ "clustertriggerauthentications": crd_exists(CLUSTERTRIGGERAUTH_CRD, context),
399
+ },
400
+ }
401
+
402
+
403
+ def register_keda_tools(mcp: FastMCP, non_destructive: bool = False):
404
+ """Register KEDA tools with the MCP server."""
405
+
406
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
407
+ def keda_scaledobjects_list_tool(
408
+ namespace: str = "",
409
+ context: str = "",
410
+ label_selector: str = ""
411
+ ) -> str:
412
+ """List KEDA ScaledObjects with their scaling status."""
413
+ return json.dumps(keda_scaledobjects_list(namespace, context, label_selector), indent=2)
414
+
415
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
416
+ def keda_scaledobject_get_tool(
417
+ name: str,
418
+ namespace: str,
419
+ context: str = ""
420
+ ) -> str:
421
+ """Get detailed information about a ScaledObject."""
422
+ return json.dumps(keda_scaledobject_get(name, namespace, context), indent=2)
423
+
424
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
425
+ def keda_scaledjobs_list_tool(
426
+ namespace: str = "",
427
+ context: str = "",
428
+ label_selector: str = ""
429
+ ) -> str:
430
+ """List KEDA ScaledJobs with their status."""
431
+ return json.dumps(keda_scaledjobs_list(namespace, context, label_selector), indent=2)
432
+
433
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
434
+ def keda_triggerauths_list_tool(
435
+ namespace: str = "",
436
+ context: str = "",
437
+ include_cluster: bool = True
438
+ ) -> str:
439
+ """List KEDA TriggerAuthentications and ClusterTriggerAuthentications."""
440
+ return json.dumps(keda_triggerauths_list(namespace, context, include_cluster), indent=2)
441
+
442
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
443
+ def keda_triggerauth_get_tool(
444
+ name: str,
445
+ namespace: str = "",
446
+ kind: str = "TriggerAuthentication",
447
+ context: str = ""
448
+ ) -> str:
449
+ """Get detailed information about a TriggerAuthentication."""
450
+ return json.dumps(keda_triggerauth_get(name, namespace, kind, context), indent=2)
451
+
452
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
453
+ def keda_hpa_list_tool(
454
+ namespace: str = "",
455
+ context: str = "",
456
+ label_selector: str = ""
457
+ ) -> str:
458
+ """List HPAs managed by KEDA."""
459
+ return json.dumps(keda_hpa_list(namespace, context, label_selector), indent=2)
460
+
461
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
462
+ def keda_detect_tool(context: str = "") -> str:
463
+ """Detect if KEDA is installed and its components."""
464
+ return json.dumps(keda_detect(context), indent=2)