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,12 +1,27 @@
1
1
  import logging
2
2
  import subprocess
3
- from typing import Any, Dict, Optional
3
+ from typing import Any, Dict, List, Optional
4
4
 
5
5
  from mcp.types import ToolAnnotations
6
6
 
7
+ from ..k8s_config import (
8
+ get_apps_client,
9
+ get_batch_client,
10
+ get_autoscaling_client,
11
+ get_policy_client,
12
+ _load_config_for_context,
13
+ )
14
+
7
15
  logger = logging.getLogger("mcp-server")
8
16
 
9
17
 
18
+ def _get_kubectl_context_args(context: str) -> List[str]:
19
+ """Get kubectl context arguments if context is specified."""
20
+ if context:
21
+ return ["--context", context]
22
+ return []
23
+
24
+
10
25
  def register_deployment_tools(server, non_destructive: bool):
11
26
  """Register deployment and workload management tools."""
12
27
 
@@ -16,12 +31,18 @@ def register_deployment_tools(server, non_destructive: bool):
16
31
  readOnlyHint=True,
17
32
  ),
18
33
  )
19
- def get_deployments(namespace: Optional[str] = None) -> Dict[str, Any]:
20
- """Get all deployments in the specified namespace."""
34
+ def get_deployments(
35
+ namespace: Optional[str] = None,
36
+ context: str = ""
37
+ ) -> Dict[str, Any]:
38
+ """Get all deployments in the specified namespace.
39
+
40
+ Args:
41
+ namespace: Namespace to list deployments from (all namespaces if not specified)
42
+ context: Kubernetes context to use (uses current context if not specified)
43
+ """
21
44
  try:
22
- from kubernetes import client, config
23
- config.load_kube_config()
24
- apps = client.AppsV1Api()
45
+ apps = get_apps_client(context)
25
46
 
26
47
  if namespace:
27
48
  deployments = apps.list_namespaced_deployment(namespace)
@@ -30,6 +51,7 @@ def register_deployment_tools(server, non_destructive: bool):
30
51
 
31
52
  return {
32
53
  "success": True,
54
+ "context": context or "current",
33
55
  "deployments": [
34
56
  {
35
57
  "name": d.metadata.name,
@@ -51,14 +73,28 @@ def register_deployment_tools(server, non_destructive: bool):
51
73
  destructiveHint=True,
52
74
  ),
53
75
  )
54
- def create_deployment(name: str, image: str, replicas: int, namespace: Optional[str] = "default") -> Dict[str, Any]:
55
- """Create a new deployment."""
76
+ def create_deployment(
77
+ name: str,
78
+ image: str,
79
+ replicas: int,
80
+ namespace: Optional[str] = "default",
81
+ context: str = ""
82
+ ) -> Dict[str, Any]:
83
+ """Create a new deployment.
84
+
85
+ Args:
86
+ name: Name of the deployment
87
+ image: Container image to deploy
88
+ replicas: Number of replicas
89
+ namespace: Namespace to create deployment in (default: "default")
90
+ context: Kubernetes context to use (uses current context if not specified)
91
+ """
56
92
  if non_destructive:
57
93
  return {"success": False, "error": "Blocked: non-destructive mode"}
58
94
  try:
59
- from kubernetes import client, config
60
- config.load_kube_config()
61
- apps = client.AppsV1Api()
95
+ from kubernetes import client
96
+
97
+ apps = get_apps_client(context)
62
98
 
63
99
  deployment = client.V1Deployment(
64
100
  metadata=client.V1ObjectMeta(name=name),
@@ -82,7 +118,11 @@ def register_deployment_tools(server, non_destructive: bool):
82
118
  )
83
119
 
84
120
  apps.create_namespaced_deployment(namespace, deployment)
85
- return {"success": True, "message": f"Deployment {name} created"}
121
+ return {
122
+ "success": True,
123
+ "context": context or "current",
124
+ "message": f"Deployment {name} created"
125
+ }
86
126
  except Exception as e:
87
127
  logger.error(f"Error creating deployment: {e}")
88
128
  return {"success": False, "error": str(e)}
@@ -93,17 +133,33 @@ def register_deployment_tools(server, non_destructive: bool):
93
133
  destructiveHint=True,
94
134
  ),
95
135
  )
96
- def scale_deployment(name: str, replicas: int, namespace: Optional[str] = "default") -> Dict[str, Any]:
97
- """Scale a deployment to a specified number of replicas."""
136
+ def scale_deployment(
137
+ name: str,
138
+ replicas: int,
139
+ namespace: Optional[str] = "default",
140
+ context: str = ""
141
+ ) -> Dict[str, Any]:
142
+ """Scale a deployment to a specified number of replicas.
143
+
144
+ Args:
145
+ name: Name of the deployment
146
+ replicas: Target number of replicas
147
+ namespace: Namespace of the deployment (default: "default")
148
+ context: Kubernetes context to use (uses current context if not specified)
149
+ """
98
150
  if non_destructive:
99
151
  return {"success": False, "error": "Blocked: non-destructive mode"}
100
152
  try:
101
- result = subprocess.run(
102
- ["kubectl", "scale", "deployment", name, f"--replicas={replicas}", "-n", namespace],
103
- capture_output=True, text=True, timeout=30
104
- )
153
+ cmd = ["kubectl", "scale", "deployment", name, f"--replicas={replicas}", "-n", namespace]
154
+ cmd.extend(_get_kubectl_context_args(context))
155
+
156
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
105
157
  if result.returncode == 0:
106
- return {"success": True, "message": f"Deployment {name} scaled to {replicas} replicas"}
158
+ return {
159
+ "success": True,
160
+ "context": context or "current",
161
+ "message": f"Deployment {name} scaled to {replicas} replicas"
162
+ }
107
163
  return {"success": False, "error": result.stderr}
108
164
  except Exception as e:
109
165
  logger.error(f"Error scaling deployment: {e}")
@@ -115,17 +171,31 @@ def register_deployment_tools(server, non_destructive: bool):
115
171
  destructiveHint=True,
116
172
  ),
117
173
  )
118
- def restart_deployment(name: str, namespace: str = "default") -> Dict[str, Any]:
119
- """Restart a deployment by triggering a rolling update."""
174
+ def restart_deployment(
175
+ name: str,
176
+ namespace: str = "default",
177
+ context: str = ""
178
+ ) -> Dict[str, Any]:
179
+ """Restart a deployment by triggering a rolling update.
180
+
181
+ Args:
182
+ name: Name of the deployment
183
+ namespace: Namespace of the deployment (default: "default")
184
+ context: Kubernetes context to use (uses current context if not specified)
185
+ """
120
186
  if non_destructive:
121
187
  return {"success": False, "error": "Blocked: non-destructive mode"}
122
188
  try:
123
- result = subprocess.run(
124
- ["kubectl", "rollout", "restart", "deployment", name, "-n", namespace],
125
- capture_output=True, text=True, timeout=30
126
- )
189
+ cmd = ["kubectl", "rollout", "restart", "deployment", name, "-n", namespace]
190
+ cmd.extend(_get_kubectl_context_args(context))
191
+
192
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
127
193
  if result.returncode == 0:
128
- return {"success": True, "message": f"Deployment {name} restarted"}
194
+ return {
195
+ "success": True,
196
+ "context": context or "current",
197
+ "message": f"Deployment {name} restarted"
198
+ }
129
199
  return {"success": False, "error": result.stderr}
130
200
  except Exception as e:
131
201
  logger.error(f"Error restarting deployment: {e}")
@@ -137,12 +207,18 @@ def register_deployment_tools(server, non_destructive: bool):
137
207
  readOnlyHint=True,
138
208
  ),
139
209
  )
140
- def get_replicasets(namespace: Optional[str] = None) -> Dict[str, Any]:
141
- """Get ReplicaSets in a namespace or cluster-wide."""
210
+ def get_replicasets(
211
+ namespace: Optional[str] = None,
212
+ context: str = ""
213
+ ) -> Dict[str, Any]:
214
+ """Get ReplicaSets in a namespace or cluster-wide.
215
+
216
+ Args:
217
+ namespace: Namespace to list ReplicaSets from (all namespaces if not specified)
218
+ context: Kubernetes context to use (uses current context if not specified)
219
+ """
142
220
  try:
143
- from kubernetes import client, config
144
- config.load_kube_config()
145
- apps = client.AppsV1Api()
221
+ apps = get_apps_client(context)
146
222
 
147
223
  if namespace:
148
224
  rs_list = apps.list_namespaced_replica_set(namespace)
@@ -151,6 +227,7 @@ def register_deployment_tools(server, non_destructive: bool):
151
227
 
152
228
  return {
153
229
  "success": True,
230
+ "context": context or "current",
154
231
  "replicaSets": [
155
232
  {
156
233
  "name": rs.metadata.name,
@@ -176,12 +253,18 @@ def register_deployment_tools(server, non_destructive: bool):
176
253
  readOnlyHint=True,
177
254
  ),
178
255
  )
179
- def get_statefulsets(namespace: Optional[str] = None) -> Dict[str, Any]:
180
- """Get StatefulSets in a namespace or cluster-wide."""
256
+ def get_statefulsets(
257
+ namespace: Optional[str] = None,
258
+ context: str = ""
259
+ ) -> Dict[str, Any]:
260
+ """Get StatefulSets in a namespace or cluster-wide.
261
+
262
+ Args:
263
+ namespace: Namespace to list StatefulSets from (all namespaces if not specified)
264
+ context: Kubernetes context to use (uses current context if not specified)
265
+ """
181
266
  try:
182
- from kubernetes import client, config
183
- config.load_kube_config()
184
- apps = client.AppsV1Api()
267
+ apps = get_apps_client(context)
185
268
 
186
269
  if namespace:
187
270
  sts_list = apps.list_namespaced_stateful_set(namespace)
@@ -190,6 +273,7 @@ def register_deployment_tools(server, non_destructive: bool):
190
273
 
191
274
  return {
192
275
  "success": True,
276
+ "context": context or "current",
193
277
  "statefulSets": [
194
278
  {
195
279
  "name": sts.metadata.name,
@@ -213,12 +297,18 @@ def register_deployment_tools(server, non_destructive: bool):
213
297
  readOnlyHint=True,
214
298
  ),
215
299
  )
216
- def get_daemonsets(namespace: Optional[str] = None) -> Dict[str, Any]:
217
- """Get DaemonSets in a namespace or cluster-wide."""
300
+ def get_daemonsets(
301
+ namespace: Optional[str] = None,
302
+ context: str = ""
303
+ ) -> Dict[str, Any]:
304
+ """Get DaemonSets in a namespace or cluster-wide.
305
+
306
+ Args:
307
+ namespace: Namespace to list DaemonSets from (all namespaces if not specified)
308
+ context: Kubernetes context to use (uses current context if not specified)
309
+ """
218
310
  try:
219
- from kubernetes import client, config
220
- config.load_kube_config()
221
- apps = client.AppsV1Api()
311
+ apps = get_apps_client(context)
222
312
 
223
313
  if namespace:
224
314
  ds_list = apps.list_namespaced_daemon_set(namespace)
@@ -227,6 +317,7 @@ def register_deployment_tools(server, non_destructive: bool):
227
317
 
228
318
  return {
229
319
  "success": True,
320
+ "context": context or "current",
230
321
  "daemonSets": [
231
322
  {
232
323
  "name": ds.metadata.name,
@@ -250,12 +341,20 @@ def register_deployment_tools(server, non_destructive: bool):
250
341
  readOnlyHint=True,
251
342
  ),
252
343
  )
253
- def get_jobs(namespace: Optional[str] = None, include_cronjobs: bool = True) -> Dict[str, Any]:
254
- """Get Jobs and optionally CronJobs."""
344
+ def get_jobs(
345
+ namespace: Optional[str] = None,
346
+ include_cronjobs: bool = True,
347
+ context: str = ""
348
+ ) -> Dict[str, Any]:
349
+ """Get Jobs and optionally CronJobs.
350
+
351
+ Args:
352
+ namespace: Namespace to list Jobs from (all namespaces if not specified)
353
+ include_cronjobs: Whether to include CronJobs in the result
354
+ context: Kubernetes context to use (uses current context if not specified)
355
+ """
255
356
  try:
256
- from kubernetes import client, config
257
- config.load_kube_config()
258
- batch = client.BatchV1Api()
357
+ batch = get_batch_client(context)
259
358
 
260
359
  if namespace:
261
360
  jobs = batch.list_namespaced_job(namespace)
@@ -264,6 +363,7 @@ def register_deployment_tools(server, non_destructive: bool):
264
363
 
265
364
  result = {
266
365
  "success": True,
366
+ "context": context or "current",
267
367
  "jobs": [
268
368
  {
269
369
  "name": job.metadata.name,
@@ -308,12 +408,21 @@ def register_deployment_tools(server, non_destructive: bool):
308
408
  readOnlyHint=True,
309
409
  ),
310
410
  )
311
- def get_hpa(namespace: Optional[str] = None) -> Dict[str, Any]:
312
- """Get HorizontalPodAutoscalers in a namespace or cluster-wide."""
411
+ def get_hpa(
412
+ namespace: Optional[str] = None,
413
+ context: str = ""
414
+ ) -> Dict[str, Any]:
415
+ """Get HorizontalPodAutoscalers in a namespace or cluster-wide.
416
+
417
+ Args:
418
+ namespace: Namespace to list HPAs from (all namespaces if not specified)
419
+ context: Kubernetes context to use (uses current context if not specified)
420
+ """
313
421
  try:
314
- from kubernetes import client, config
315
- config.load_kube_config()
316
- autoscaling = client.AutoscalingV2Api()
422
+ from kubernetes import client
423
+
424
+ api_client = _load_config_for_context(context)
425
+ autoscaling = client.AutoscalingV2Api(api_client=api_client)
317
426
 
318
427
  if namespace:
319
428
  hpas = autoscaling.list_namespaced_horizontal_pod_autoscaler(namespace)
@@ -322,6 +431,7 @@ def register_deployment_tools(server, non_destructive: bool):
322
431
 
323
432
  return {
324
433
  "success": True,
434
+ "context": context or "current",
325
435
  "hpas": [
326
436
  {
327
437
  "name": hpa.metadata.name,
@@ -348,12 +458,18 @@ def register_deployment_tools(server, non_destructive: bool):
348
458
  readOnlyHint=True,
349
459
  ),
350
460
  )
351
- def get_pdb(namespace: Optional[str] = None) -> Dict[str, Any]:
352
- """Get PodDisruptionBudgets in a namespace or cluster-wide."""
461
+ def get_pdb(
462
+ namespace: Optional[str] = None,
463
+ context: str = ""
464
+ ) -> Dict[str, Any]:
465
+ """Get PodDisruptionBudgets in a namespace or cluster-wide.
466
+
467
+ Args:
468
+ namespace: Namespace to list PDBs from (all namespaces if not specified)
469
+ context: Kubernetes context to use (uses current context if not specified)
470
+ """
353
471
  try:
354
- from kubernetes import client, config
355
- config.load_kube_config()
356
- policy = client.PolicyV1Api()
472
+ policy = get_policy_client(context)
357
473
 
358
474
  if namespace:
359
475
  pdbs = policy.list_namespaced_pod_disruption_budget(namespace)
@@ -362,6 +478,7 @@ def register_deployment_tools(server, non_destructive: bool):
362
478
 
363
479
  return {
364
480
  "success": True,
481
+ "context": context or "current",
365
482
  "pdbs": [
366
483
  {
367
484
  "name": pdb.metadata.name,
@@ -1,12 +1,21 @@
1
1
  import logging
2
2
  import subprocess
3
- from typing import Any, Dict, Optional
3
+ from typing import Any, Dict, List, Optional
4
4
 
5
5
  from mcp.types import ToolAnnotations
6
6
 
7
+ from ..k8s_config import get_k8s_client, get_apps_client
8
+
7
9
  logger = logging.getLogger("mcp-server")
8
10
 
9
11
 
12
+ def _get_kubectl_context_args(context: str) -> List[str]:
13
+ """Get kubectl context arguments if context is specified."""
14
+ if context:
15
+ return ["--context", context]
16
+ return []
17
+
18
+
10
19
  def register_diagnostics_tools(server, non_destructive: bool):
11
20
  """Register diagnostic and troubleshooting tools.
12
21
 
@@ -21,13 +30,18 @@ def register_diagnostics_tools(server, non_destructive: bool):
21
30
  readOnlyHint=True,
22
31
  ),
23
32
  )
24
- def compare_namespaces(namespace1: str, namespace2: str, resource_type: str = "deployment") -> Dict[str, Any]:
25
- """Compare resources between two namespaces."""
33
+ def compare_namespaces(namespace1: str, namespace2: str, resource_type: str = "deployment", context: str = "") -> Dict[str, Any]:
34
+ """Compare resources between two namespaces.
35
+
36
+ Args:
37
+ namespace1: First namespace to compare
38
+ namespace2: Second namespace to compare
39
+ resource_type: Type of resource to compare (deployment, service, configmap, secret)
40
+ context: Kubernetes context to use (optional, uses current context if not specified)
41
+ """
26
42
  try:
27
- from kubernetes import client, config
28
- config.load_kube_config()
29
- apps = client.AppsV1Api()
30
- v1 = client.CoreV1Api()
43
+ apps = get_apps_client(context)
44
+ v1 = get_k8s_client(context)
31
45
 
32
46
  def get_resources(ns, res_type):
33
47
  if res_type == "deployment":
@@ -63,6 +77,7 @@ def register_diagnostics_tools(server, non_destructive: bool):
63
77
 
64
78
  return {
65
79
  "success": True,
80
+ "context": context or "current",
66
81
  "resourceType": resource_type,
67
82
  "namespaces": [namespace1, namespace2],
68
83
  "summary": {
@@ -85,10 +100,16 @@ def register_diagnostics_tools(server, non_destructive: bool):
85
100
  readOnlyHint=True,
86
101
  ),
87
102
  )
88
- def get_pod_metrics(namespace: Optional[str] = None, pod_name: Optional[str] = None) -> Dict[str, Any]:
89
- """Get pod resource usage metrics (requires metrics-server)."""
103
+ def get_pod_metrics(namespace: Optional[str] = None, pod_name: Optional[str] = None, context: str = "") -> Dict[str, Any]:
104
+ """Get pod resource usage metrics (requires metrics-server).
105
+
106
+ Args:
107
+ namespace: Target namespace (optional, all namespaces if not specified)
108
+ pod_name: Filter by specific pod name (optional)
109
+ context: Kubernetes context to use (optional, uses current context if not specified)
110
+ """
90
111
  try:
91
- cmd = ["kubectl", "top", "pods", "--no-headers"]
112
+ cmd = ["kubectl"] + _get_kubectl_context_args(context) + ["top", "pods", "--no-headers"]
92
113
  if namespace:
93
114
  cmd.extend(["-n", namespace])
94
115
  else:
@@ -124,6 +145,7 @@ def register_diagnostics_tools(server, non_destructive: bool):
124
145
 
125
146
  return {
126
147
  "success": True,
148
+ "context": context or "current",
127
149
  "count": len(metrics),
128
150
  "metrics": metrics
129
151
  }
@@ -139,10 +161,14 @@ def register_diagnostics_tools(server, non_destructive: bool):
139
161
  readOnlyHint=True,
140
162
  ),
141
163
  )
142
- def get_node_metrics() -> Dict[str, Any]:
143
- """Get node resource usage metrics (requires metrics-server)."""
164
+ def get_node_metrics(context: str = "") -> Dict[str, Any]:
165
+ """Get node resource usage metrics (requires metrics-server).
166
+
167
+ Args:
168
+ context: Kubernetes context to use (optional, uses current context if not specified)
169
+ """
144
170
  try:
145
- cmd = ["kubectl", "top", "nodes", "--no-headers"]
171
+ cmd = ["kubectl"] + _get_kubectl_context_args(context) + ["top", "nodes", "--no-headers"]
146
172
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
147
173
 
148
174
  if result.returncode != 0:
@@ -164,6 +190,7 @@ def register_diagnostics_tools(server, non_destructive: bool):
164
190
 
165
191
  return {
166
192
  "success": True,
193
+ "context": context or "current",
167
194
  "count": len(metrics),
168
195
  "metrics": metrics
169
196
  }