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,501 @@
1
+ import logging
2
+ import subprocess
3
+ import shlex
4
+ import tempfile
5
+ import os
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from mcp.types import ToolAnnotations
9
+
10
+ logger = logging.getLogger("mcp-server")
11
+
12
+
13
+ def register_operations_tools(server, non_destructive: bool):
14
+ """Register kubectl operations tools (apply, describe, patch, etc.)."""
15
+
16
+ def check_destructive():
17
+ """Check if operation is blocked in non-destructive mode."""
18
+ if non_destructive:
19
+ return {"success": False, "error": "Operation blocked: non-destructive mode enabled"}
20
+ return None
21
+
22
+ @server.tool(
23
+ annotations=ToolAnnotations(
24
+ title="Kubectl Apply",
25
+ destructiveHint=True,
26
+ ),
27
+ )
28
+ def kubectl_apply(manifest: str, namespace: Optional[str] = "default") -> Dict[str, Any]:
29
+ """Apply a YAML manifest to the cluster."""
30
+ blocked = check_destructive()
31
+ if blocked:
32
+ return blocked
33
+ try:
34
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
35
+ f.write(manifest)
36
+ temp_path = f.name
37
+
38
+ cmd = ["kubectl", "apply", "-f", temp_path, "-n", namespace]
39
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
40
+
41
+ os.unlink(temp_path)
42
+
43
+ if result.returncode == 0:
44
+ return {"success": True, "output": result.stdout.strip()}
45
+ else:
46
+ return {"success": False, "error": result.stderr.strip()}
47
+ except Exception as e:
48
+ logger.error(f"Error applying manifest: {e}")
49
+ return {"success": False, "error": str(e)}
50
+
51
+ @server.tool(
52
+ annotations=ToolAnnotations(
53
+ title="Kubectl Describe",
54
+ readOnlyHint=True,
55
+ ),
56
+ )
57
+ def kubectl_describe(resource_type: str, name: str, namespace: Optional[str] = "default") -> Dict[str, Any]:
58
+ """Describe a Kubernetes resource in detail."""
59
+ try:
60
+ cmd = ["kubectl", "describe", resource_type, name, "-n", namespace]
61
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
62
+
63
+ if result.returncode == 0:
64
+ return {"success": True, "description": result.stdout}
65
+ else:
66
+ return {"success": False, "error": result.stderr.strip()}
67
+ except Exception as e:
68
+ logger.error(f"Error describing resource: {e}")
69
+ return {"success": False, "error": str(e)}
70
+
71
+ @server.tool(
72
+ annotations=ToolAnnotations(
73
+ title="Kubectl Generic",
74
+ readOnlyHint=True,
75
+ ),
76
+ )
77
+ def kubectl_generic(command: str) -> Dict[str, Any]:
78
+ """Execute any kubectl command. Use with caution."""
79
+ try:
80
+ # Security: validate command starts with allowed operations
81
+ allowed_prefixes = [
82
+ "get", "describe", "logs", "top", "explain", "api-resources",
83
+ "config", "version", "cluster-info", "auth"
84
+ ]
85
+ cmd_parts = shlex.split(command)
86
+ if not cmd_parts:
87
+ return {"success": False, "error": "Empty command"}
88
+
89
+ # Remove 'kubectl' prefix if present
90
+ if cmd_parts[0] == "kubectl":
91
+ cmd_parts = cmd_parts[1:]
92
+
93
+ if not cmd_parts or cmd_parts[0] not in allowed_prefixes:
94
+ return {
95
+ "success": False,
96
+ "error": f"Command not allowed. Allowed: {', '.join(allowed_prefixes)}"
97
+ }
98
+
99
+ full_cmd = ["kubectl"] + cmd_parts
100
+ result = subprocess.run(full_cmd, capture_output=True, text=True, timeout=60)
101
+
102
+ return {
103
+ "success": result.returncode == 0,
104
+ "output": result.stdout,
105
+ "error": result.stderr if result.returncode != 0 else None
106
+ }
107
+ except Exception as e:
108
+ logger.error(f"Error running kubectl command: {e}")
109
+ return {"success": False, "error": str(e)}
110
+
111
+ @server.tool(
112
+ annotations=ToolAnnotations(
113
+ title="Kubectl Patch",
114
+ destructiveHint=True,
115
+ ),
116
+ )
117
+ def kubectl_patch(resource_type: str, name: str, patch: str, patch_type: str = "strategic", namespace: Optional[str] = "default") -> Dict[str, Any]:
118
+ """Patch a Kubernetes resource."""
119
+ blocked = check_destructive()
120
+ if blocked:
121
+ return blocked
122
+ try:
123
+ type_flag = {
124
+ "strategic": "strategic",
125
+ "merge": "merge",
126
+ "json": "json"
127
+ }.get(patch_type, "strategic")
128
+
129
+ cmd = [
130
+ "kubectl", "patch", resource_type, name,
131
+ "-n", namespace,
132
+ "--type", type_flag,
133
+ "-p", patch
134
+ ]
135
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
136
+
137
+ if result.returncode == 0:
138
+ return {"success": True, "output": result.stdout.strip()}
139
+ else:
140
+ return {"success": False, "error": result.stderr.strip()}
141
+ except Exception as e:
142
+ logger.error(f"Error patching resource: {e}")
143
+ return {"success": False, "error": str(e)}
144
+
145
+ @server.tool(
146
+ annotations=ToolAnnotations(
147
+ title="Kubectl Rollout",
148
+ destructiveHint=True,
149
+ ),
150
+ )
151
+ def kubectl_rollout(action: str, resource_type: str, name: str, namespace: Optional[str] = "default") -> Dict[str, Any]:
152
+ """Manage rollouts (restart, status, history, undo, pause, resume)."""
153
+ try:
154
+ allowed_actions = ["status", "history", "restart", "undo", "pause", "resume"]
155
+ if action not in allowed_actions:
156
+ return {"success": False, "error": f"Invalid action. Allowed: {', '.join(allowed_actions)}"}
157
+
158
+ # Destructive actions need check
159
+ if action in ["restart", "undo", "pause", "resume"]:
160
+ blocked = check_destructive()
161
+ if blocked:
162
+ return blocked
163
+
164
+ cmd = ["kubectl", "rollout", action, f"{resource_type}/{name}", "-n", namespace]
165
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
166
+
167
+ if result.returncode == 0:
168
+ return {"success": True, "output": result.stdout.strip()}
169
+ else:
170
+ return {"success": False, "error": result.stderr.strip()}
171
+ except Exception as e:
172
+ logger.error(f"Error managing rollout: {e}")
173
+ return {"success": False, "error": str(e)}
174
+
175
+ @server.tool(
176
+ annotations=ToolAnnotations(
177
+ title="Kubectl Create",
178
+ destructiveHint=True,
179
+ ),
180
+ )
181
+ def kubectl_create(resource_type: str, name: str, namespace: Optional[str] = "default", image: Optional[str] = None) -> Dict[str, Any]:
182
+ """Create a Kubernetes resource."""
183
+ blocked = check_destructive()
184
+ if blocked:
185
+ return blocked
186
+ try:
187
+ cmd = ["kubectl", "create", resource_type, name, "-n", namespace]
188
+ if image and resource_type in ["deployment", "pod"]:
189
+ cmd.extend(["--image", image])
190
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
191
+ if result.returncode == 0:
192
+ return {"success": True, "output": result.stdout.strip()}
193
+ else:
194
+ return {"success": False, "error": result.stderr.strip()}
195
+ except Exception as e:
196
+ logger.error(f"Error creating resource: {e}")
197
+ return {"success": False, "error": str(e)}
198
+
199
+ @server.tool(
200
+ annotations=ToolAnnotations(
201
+ title="Delete Resource",
202
+ destructiveHint=True,
203
+ ),
204
+ )
205
+ def delete_resource(resource_type: str, name: str, namespace: Optional[str] = "default") -> Dict[str, Any]:
206
+ """Delete a Kubernetes resource."""
207
+ blocked = check_destructive()
208
+ if blocked:
209
+ return blocked
210
+ try:
211
+ cmd = ["kubectl", "delete", resource_type, name, "-n", namespace]
212
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
213
+ if result.returncode == 0:
214
+ return {"success": True, "message": f"Deleted {resource_type}/{name}"}
215
+ else:
216
+ return {"success": False, "error": result.stderr.strip()}
217
+ except Exception as e:
218
+ logger.error(f"Error deleting resource: {e}")
219
+ return {"success": False, "error": str(e)}
220
+
221
+ @server.tool(
222
+ annotations=ToolAnnotations(
223
+ title="Kubectl Copy",
224
+ destructiveHint=True,
225
+ ),
226
+ )
227
+ def kubectl_cp(source: str, destination: str, namespace: str = "default", container: Optional[str] = None) -> Dict[str, Any]:
228
+ """Copy files between local filesystem and pods.
229
+
230
+ Use pod:path format for pod paths, e.g.:
231
+ - Local to pod: kubectl_cp("/tmp/file.txt", "mypod:/tmp/file.txt")
232
+ - Pod to local: kubectl_cp("mypod:/tmp/file.txt", "/tmp/file.txt")
233
+ """
234
+ blocked = check_destructive()
235
+ if blocked:
236
+ return blocked
237
+ try:
238
+ cmd = ["kubectl", "cp", source, destination, "-n", namespace]
239
+ if container:
240
+ cmd.extend(["-c", container])
241
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
242
+ if result.returncode == 0:
243
+ return {"success": True, "message": f"Copied {source} to {destination}"}
244
+ else:
245
+ return {"success": False, "error": result.stderr.strip()}
246
+ except Exception as e:
247
+ logger.error(f"Error copying files: {e}")
248
+ return {"success": False, "error": str(e)}
249
+
250
+ @server.tool(
251
+ annotations=ToolAnnotations(
252
+ title="Backup Resource as YAML",
253
+ readOnlyHint=True,
254
+ ),
255
+ )
256
+ def backup_resource(resource_type: str, name: str, namespace: Optional[str] = None) -> Dict[str, Any]:
257
+ """Export a resource as YAML for backup or migration."""
258
+ try:
259
+ cmd = ["kubectl", "get", resource_type, name, "-o", "yaml"]
260
+ if namespace:
261
+ cmd.extend(["-n", namespace])
262
+
263
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
264
+
265
+ if result.returncode != 0:
266
+ return {"success": False, "error": result.stderr.strip()}
267
+
268
+ return {
269
+ "success": True,
270
+ "resource": {
271
+ "type": resource_type,
272
+ "name": name,
273
+ "namespace": namespace
274
+ },
275
+ "yaml": result.stdout,
276
+ "hint": "Save this YAML to a file and use 'kubectl apply -f' to restore"
277
+ }
278
+ except Exception as e:
279
+ logger.error(f"Error backing up resource: {e}")
280
+ return {"success": False, "error": str(e)}
281
+
282
+ @server.tool(
283
+ annotations=ToolAnnotations(
284
+ title="Label Resource",
285
+ destructiveHint=True,
286
+ ),
287
+ )
288
+ def label_resource(
289
+ resource_type: str,
290
+ name: str,
291
+ labels: Dict[str, str],
292
+ namespace: Optional[str] = None,
293
+ overwrite: bool = False
294
+ ) -> Dict[str, Any]:
295
+ """Add or update labels on a resource."""
296
+ blocked = check_destructive()
297
+ if blocked:
298
+ return blocked
299
+ try:
300
+ cmd = ["kubectl", "label", resource_type, name]
301
+ if namespace:
302
+ cmd.extend(["-n", namespace])
303
+
304
+ for key, value in labels.items():
305
+ if value is None:
306
+ cmd.append(f"{key}-") # Remove label
307
+ else:
308
+ cmd.append(f"{key}={value}")
309
+
310
+ if overwrite:
311
+ cmd.append("--overwrite")
312
+
313
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
314
+
315
+ if result.returncode != 0:
316
+ return {"success": False, "error": result.stderr.strip()}
317
+
318
+ return {
319
+ "success": True,
320
+ "message": result.stdout.strip(),
321
+ "resource": {"type": resource_type, "name": name, "namespace": namespace},
322
+ "appliedLabels": labels
323
+ }
324
+ except Exception as e:
325
+ logger.error(f"Error labeling resource: {e}")
326
+ return {"success": False, "error": str(e)}
327
+
328
+ @server.tool(
329
+ annotations=ToolAnnotations(
330
+ title="Annotate Resource",
331
+ destructiveHint=True,
332
+ ),
333
+ )
334
+ def annotate_resource(
335
+ resource_type: str,
336
+ name: str,
337
+ annotations: Dict[str, str],
338
+ namespace: Optional[str] = None,
339
+ overwrite: bool = False
340
+ ) -> Dict[str, Any]:
341
+ """Add or update annotations on a resource."""
342
+ blocked = check_destructive()
343
+ if blocked:
344
+ return blocked
345
+ try:
346
+ cmd = ["kubectl", "annotate", resource_type, name]
347
+ if namespace:
348
+ cmd.extend(["-n", namespace])
349
+
350
+ for key, value in annotations.items():
351
+ if value is None:
352
+ cmd.append(f"{key}-") # Remove annotation
353
+ else:
354
+ cmd.append(f"{key}={value}")
355
+
356
+ if overwrite:
357
+ cmd.append("--overwrite")
358
+
359
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
360
+
361
+ if result.returncode != 0:
362
+ return {"success": False, "error": result.stderr.strip()}
363
+
364
+ return {
365
+ "success": True,
366
+ "message": result.stdout.strip(),
367
+ "resource": {"type": resource_type, "name": name, "namespace": namespace},
368
+ "appliedAnnotations": annotations
369
+ }
370
+ except Exception as e:
371
+ logger.error(f"Error annotating resource: {e}")
372
+ return {"success": False, "error": str(e)}
373
+
374
+ @server.tool(
375
+ annotations=ToolAnnotations(
376
+ title="Taint Node",
377
+ destructiveHint=True,
378
+ ),
379
+ )
380
+ def taint_node(
381
+ node_name: str,
382
+ key: str,
383
+ value: Optional[str] = None,
384
+ effect: str = "NoSchedule",
385
+ remove: bool = False
386
+ ) -> Dict[str, Any]:
387
+ """Add or remove taints on a node."""
388
+ blocked = check_destructive()
389
+ if blocked:
390
+ return blocked
391
+ try:
392
+ if effect not in ["NoSchedule", "PreferNoSchedule", "NoExecute"]:
393
+ return {"success": False, "error": f"Invalid effect: {effect}. Must be NoSchedule, PreferNoSchedule, or NoExecute"}
394
+
395
+ cmd = ["kubectl", "taint", "nodes", node_name]
396
+
397
+ if remove:
398
+ taint_str = f"{key}:{effect}-"
399
+ else:
400
+ if value:
401
+ taint_str = f"{key}={value}:{effect}"
402
+ else:
403
+ taint_str = f"{key}:{effect}"
404
+
405
+ cmd.append(taint_str)
406
+
407
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
408
+
409
+ if result.returncode != 0:
410
+ return {"success": False, "error": result.stderr.strip()}
411
+
412
+ return {
413
+ "success": True,
414
+ "message": result.stdout.strip(),
415
+ "node": node_name,
416
+ "action": "removed" if remove else "added",
417
+ "taint": {"key": key, "value": value, "effect": effect}
418
+ }
419
+ except Exception as e:
420
+ logger.error(f"Error tainting node: {e}")
421
+ return {"success": False, "error": str(e)}
422
+
423
+ @server.tool(
424
+ annotations=ToolAnnotations(
425
+ title="Wait for Condition",
426
+ readOnlyHint=True,
427
+ ),
428
+ )
429
+ def wait_for_condition(
430
+ resource_type: str,
431
+ name: str,
432
+ condition: str,
433
+ namespace: Optional[str] = None,
434
+ timeout: int = 60
435
+ ) -> Dict[str, Any]:
436
+ """Wait for a resource to reach a specific condition."""
437
+ try:
438
+ cmd = ["kubectl", "wait", f"{resource_type}/{name}", f"--for={condition}", f"--timeout={timeout}s"]
439
+ if namespace:
440
+ cmd.extend(["-n", namespace])
441
+
442
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 10)
443
+
444
+ if result.returncode != 0:
445
+ return {
446
+ "success": False,
447
+ "conditionMet": False,
448
+ "error": result.stderr.strip(),
449
+ "resource": {"type": resource_type, "name": name, "namespace": namespace},
450
+ "condition": condition
451
+ }
452
+
453
+ return {
454
+ "success": True,
455
+ "conditionMet": True,
456
+ "message": result.stdout.strip(),
457
+ "resource": {"type": resource_type, "name": name, "namespace": namespace},
458
+ "condition": condition
459
+ }
460
+ except subprocess.TimeoutExpired:
461
+ return {
462
+ "success": False,
463
+ "conditionMet": False,
464
+ "error": f"Timeout waiting for condition '{condition}' after {timeout}s",
465
+ "resource": {"type": resource_type, "name": name, "namespace": namespace}
466
+ }
467
+ except Exception as e:
468
+ logger.error(f"Error waiting for condition: {e}")
469
+ return {"success": False, "error": str(e)}
470
+
471
+ @server.tool(
472
+ annotations=ToolAnnotations(
473
+ title="Node Management",
474
+ destructiveHint=True,
475
+ ),
476
+ )
477
+ def node_management(action: str, node_name: str, force: bool = False) -> Dict[str, Any]:
478
+ """Manage nodes: cordon, uncordon, or drain."""
479
+ blocked = check_destructive()
480
+ if blocked:
481
+ return blocked
482
+ try:
483
+ allowed_actions = ["cordon", "uncordon", "drain"]
484
+ if action not in allowed_actions:
485
+ return {"success": False, "error": f"Invalid action. Allowed: {', '.join(allowed_actions)}"}
486
+
487
+ cmd = ["kubectl", action, node_name]
488
+ if action == "drain":
489
+ cmd.extend(["--ignore-daemonsets", "--delete-emptydir-data"])
490
+ if force:
491
+ cmd.append("--force")
492
+
493
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
494
+
495
+ if result.returncode == 0:
496
+ return {"success": True, "output": result.stdout.strip()}
497
+ else:
498
+ return {"success": False, "error": result.stderr.strip()}
499
+ except Exception as e:
500
+ logger.error(f"Error managing node: {e}")
501
+ return {"success": False, "error": str(e)}