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,1561 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import subprocess
5
+ import tempfile
6
+ from typing import Any, Callable, Dict, List, Optional
7
+
8
+ import yaml
9
+ from mcp.types import ToolAnnotations
10
+
11
+ logger = logging.getLogger("mcp-server")
12
+
13
+
14
+ def register_helm_tools(
15
+ server,
16
+ non_destructive: bool,
17
+ check_helm_fn: Callable[[], bool]
18
+ ):
19
+ """Register all Helm-related tools with the MCP server.
20
+
21
+ Args:
22
+ server: FastMCP server instance
23
+ non_destructive: If True, block destructive operations
24
+ check_helm_fn: Function to check if Helm is available
25
+ """
26
+
27
+ @server.tool(
28
+ annotations=ToolAnnotations(
29
+ title="Install Helm Chart",
30
+ destructiveHint=True,
31
+ ),
32
+ )
33
+ def install_helm_chart(
34
+ name: str,
35
+ chart: str,
36
+ namespace: str,
37
+ repo: Optional[str] = None,
38
+ values: Optional[dict] = None
39
+ ) -> Dict[str, Any]:
40
+ """Install a Helm chart."""
41
+ if non_destructive:
42
+ return {"success": False, "error": "Blocked: non-destructive mode"}
43
+ if not check_helm_fn():
44
+ return {"success": False, "error": "Helm is not available on this system"}
45
+
46
+ try:
47
+ if repo:
48
+ try:
49
+ repo_parts = repo.split('=')
50
+ if len(repo_parts) != 2:
51
+ return {"success": False, "error": "Repository format should be 'repo_name=repo_url'"}
52
+
53
+ repo_name, repo_url = repo_parts
54
+ repo_add_cmd = ["helm", "repo", "add", repo_name, repo_url]
55
+ logger.debug(f"Running command: {' '.join(repo_add_cmd)}")
56
+ subprocess.check_output(repo_add_cmd, stderr=subprocess.PIPE, text=True)
57
+
58
+ repo_update_cmd = ["helm", "repo", "update"]
59
+ logger.debug(f"Running command: {' '.join(repo_update_cmd)}")
60
+ subprocess.check_output(repo_update_cmd, stderr=subprocess.PIPE, text=True)
61
+
62
+ if '/' not in chart:
63
+ chart = f"{repo_name}/{chart}"
64
+ except subprocess.CalledProcessError as e:
65
+ logger.error(f"Error adding Helm repo: {e.stderr if hasattr(e, 'stderr') else str(e)}")
66
+ return {"success": False, "error": f"Failed to add Helm repo: {e.stderr if hasattr(e, 'stderr') else str(e)}"}
67
+
68
+ cmd = ["helm", "install", name, chart, "-n", namespace]
69
+
70
+ try:
71
+ ns_cmd = ["kubectl", "get", "namespace", namespace]
72
+ subprocess.check_output(ns_cmd, stderr=subprocess.PIPE, text=True)
73
+ except subprocess.CalledProcessError:
74
+ logger.info(f"Namespace {namespace} not found, creating it")
75
+ create_ns_cmd = ["kubectl", "create", "namespace", namespace]
76
+ try:
77
+ subprocess.check_output(create_ns_cmd, stderr=subprocess.PIPE, text=True)
78
+ except subprocess.CalledProcessError as e:
79
+ logger.error(f"Error creating namespace: {e.stderr if hasattr(e, 'stderr') else str(e)}")
80
+ return {"success": False, "error": f"Failed to create namespace: {e.stderr if hasattr(e, 'stderr') else str(e)}"}
81
+
82
+ values_file = None
83
+ try:
84
+ if values:
85
+ with tempfile.NamedTemporaryFile("w", delete=False) as f:
86
+ yaml.dump(values, f)
87
+ values_file = f.name
88
+ cmd += ["-f", values_file]
89
+
90
+ logger.debug(f"Running command: {' '.join(cmd)}")
91
+ result = subprocess.check_output(cmd, stderr=subprocess.PIPE, text=True)
92
+
93
+ return {
94
+ "success": True,
95
+ "message": f"Helm chart {chart} installed as {name} in {namespace}",
96
+ "details": result
97
+ }
98
+ except subprocess.CalledProcessError as e:
99
+ error_msg = e.stderr if hasattr(e, 'stderr') else str(e)
100
+ logger.error(f"Error installing Helm chart: {error_msg}")
101
+ return {"success": False, "error": f"Failed to install Helm chart: {error_msg}"}
102
+ finally:
103
+ if values_file and os.path.exists(values_file):
104
+ os.unlink(values_file)
105
+ except Exception as e:
106
+ logger.error(f"Unexpected error installing Helm chart: {str(e)}")
107
+ return {"success": False, "error": f"Unexpected error: {str(e)}"}
108
+
109
+ @server.tool(
110
+ annotations=ToolAnnotations(
111
+ title="Upgrade Helm Chart",
112
+ destructiveHint=True,
113
+ ),
114
+ )
115
+ def upgrade_helm_chart(
116
+ name: str,
117
+ chart: str,
118
+ namespace: str,
119
+ repo: Optional[str] = None,
120
+ values: Optional[dict] = None
121
+ ) -> Dict[str, Any]:
122
+ """Upgrade a Helm release."""
123
+ if non_destructive:
124
+ return {"success": False, "error": "Blocked: non-destructive mode"}
125
+ if not check_helm_fn():
126
+ return {"success": False, "error": "Helm is not available on this system"}
127
+
128
+ try:
129
+ if repo:
130
+ try:
131
+ repo_parts = repo.split('=')
132
+ if len(repo_parts) != 2:
133
+ return {"success": False, "error": "Repository format should be 'repo_name=repo_url'"}
134
+
135
+ repo_name, repo_url = repo_parts
136
+ repo_add_cmd = ["helm", "repo", "add", repo_name, repo_url]
137
+ logger.debug(f"Running command: {' '.join(repo_add_cmd)}")
138
+ subprocess.check_output(repo_add_cmd, stderr=subprocess.PIPE, text=True)
139
+
140
+ repo_update_cmd = ["helm", "repo", "update"]
141
+ logger.debug(f"Running command: {' '.join(repo_update_cmd)}")
142
+ subprocess.check_output(repo_update_cmd, stderr=subprocess.PIPE, text=True)
143
+
144
+ if '/' not in chart:
145
+ chart = f"{repo_name}/{chart}"
146
+ except subprocess.CalledProcessError as e:
147
+ logger.error(f"Error adding Helm repo: {e.stderr if hasattr(e, 'stderr') else str(e)}")
148
+ return {"success": False, "error": f"Failed to add Helm repo: {e.stderr if hasattr(e, 'stderr') else str(e)}"}
149
+
150
+ cmd = ["helm", "upgrade", name, chart, "-n", namespace]
151
+
152
+ values_file = None
153
+ try:
154
+ if values:
155
+ with tempfile.NamedTemporaryFile("w", delete=False) as f:
156
+ yaml.dump(values, f)
157
+ values_file = f.name
158
+ cmd += ["-f", values_file]
159
+
160
+ logger.debug(f"Running command: {' '.join(cmd)}")
161
+ result = subprocess.check_output(cmd, stderr=subprocess.PIPE, text=True)
162
+
163
+ return {
164
+ "success": True,
165
+ "message": f"Helm release {name} upgraded with chart {chart} in {namespace}",
166
+ "details": result
167
+ }
168
+ except subprocess.CalledProcessError as e:
169
+ error_msg = e.stderr if hasattr(e, 'stderr') else str(e)
170
+ logger.error(f"Error upgrading Helm chart: {error_msg}")
171
+ return {"success": False, "error": f"Failed to upgrade Helm chart: {error_msg}"}
172
+ finally:
173
+ if values_file and os.path.exists(values_file):
174
+ os.unlink(values_file)
175
+ except Exception as e:
176
+ logger.error(f"Unexpected error upgrading Helm chart: {str(e)}")
177
+ return {"success": False, "error": f"Unexpected error: {str(e)}"}
178
+
179
+ @server.tool(
180
+ annotations=ToolAnnotations(
181
+ title="Uninstall Helm Chart",
182
+ destructiveHint=True,
183
+ ),
184
+ )
185
+ def uninstall_helm_chart(name: str, namespace: str) -> Dict[str, Any]:
186
+ """Uninstall a Helm release."""
187
+ if non_destructive:
188
+ return {"success": False, "error": "Blocked: non-destructive mode"}
189
+ if not check_helm_fn():
190
+ return {"success": False, "error": "Helm is not available on this system"}
191
+
192
+ try:
193
+ cmd = ["helm", "uninstall", name, "-n", namespace]
194
+ logger.debug(f"Running command: {' '.join(cmd)}")
195
+
196
+ try:
197
+ result = subprocess.check_output(cmd, stderr=subprocess.PIPE, text=True)
198
+ return {
199
+ "success": True,
200
+ "message": f"Helm release {name} uninstalled from {namespace}",
201
+ "details": result
202
+ }
203
+ except subprocess.CalledProcessError as e:
204
+ error_msg = e.stderr if hasattr(e, 'stderr') else str(e)
205
+ logger.error(f"Error uninstalling Helm chart: {error_msg}")
206
+ return {"success": False, "error": f"Failed to uninstall Helm chart: {error_msg}"}
207
+ except Exception as e:
208
+ logger.error(f"Unexpected error uninstalling Helm chart: {str(e)}")
209
+ return {"success": False, "error": f"Unexpected error: {str(e)}"}
210
+
211
+ @server.tool(
212
+ annotations=ToolAnnotations(
213
+ title="List Helm Releases",
214
+ readOnlyHint=True,
215
+ ),
216
+ )
217
+ def helm_list(
218
+ namespace: Optional[str] = None,
219
+ all_namespaces: bool = False,
220
+ filter: Optional[str] = None,
221
+ deployed: bool = False,
222
+ failed: bool = False,
223
+ pending: bool = False,
224
+ uninstalled: bool = False,
225
+ superseded: bool = False
226
+ ) -> Dict[str, Any]:
227
+ """List Helm releases with optional filtering.
228
+
229
+ Args:
230
+ namespace: Target namespace (default: current namespace)
231
+ all_namespaces: List releases across all namespaces
232
+ filter: Filter releases by name using regex
233
+ deployed: Show deployed releases only
234
+ failed: Show failed releases only
235
+ pending: Show pending releases only
236
+ uninstalled: Show uninstalled releases (if kept with --keep-history)
237
+ superseded: Show superseded releases only
238
+ """
239
+ if not check_helm_fn():
240
+ return {"success": False, "error": "Helm is not available on this system"}
241
+
242
+ try:
243
+ cmd = ["helm", "list", "--output", "json"]
244
+
245
+ if all_namespaces:
246
+ cmd.append("--all-namespaces")
247
+ elif namespace:
248
+ cmd.extend(["-n", namespace])
249
+
250
+ if filter:
251
+ cmd.extend(["--filter", filter])
252
+ if deployed:
253
+ cmd.append("--deployed")
254
+ if failed:
255
+ cmd.append("--failed")
256
+ if pending:
257
+ cmd.append("--pending")
258
+ if uninstalled:
259
+ cmd.append("--uninstalled")
260
+ if superseded:
261
+ cmd.append("--superseded")
262
+
263
+ if not any([deployed, failed, pending, uninstalled, superseded]):
264
+ cmd.append("--all")
265
+
266
+ logger.debug(f"Running command: {' '.join(cmd)}")
267
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
268
+
269
+ if result.returncode == 0:
270
+ releases = json.loads(result.stdout) if result.stdout.strip() else []
271
+ return {
272
+ "success": True,
273
+ "releases": releases,
274
+ "count": len(releases)
275
+ }
276
+ else:
277
+ return {"success": False, "error": result.stderr.strip()}
278
+ except Exception as e:
279
+ logger.error(f"Error listing Helm releases: {e}")
280
+ return {"success": False, "error": str(e)}
281
+
282
+ @server.tool(
283
+ annotations=ToolAnnotations(
284
+ title="Helm Release Status",
285
+ readOnlyHint=True,
286
+ ),
287
+ )
288
+ def helm_status(
289
+ release_name: str,
290
+ namespace: str = "default",
291
+ revision: Optional[int] = None,
292
+ show_desc: bool = False,
293
+ show_resources: bool = False
294
+ ) -> Dict[str, Any]:
295
+ """Get the status of a Helm release.
296
+
297
+ Args:
298
+ release_name: Name of the release
299
+ namespace: Kubernetes namespace
300
+ revision: Show status for a specific revision (default: latest)
301
+ show_desc: Show description of the release
302
+ show_resources: Show resources created by the release
303
+ """
304
+ if not check_helm_fn():
305
+ return {"success": False, "error": "Helm is not available on this system"}
306
+
307
+ try:
308
+ cmd = ["helm", "status", release_name, "-n", namespace, "--output", "json"]
309
+
310
+ if revision:
311
+ cmd.extend(["--revision", str(revision)])
312
+ if show_desc:
313
+ cmd.append("--show-desc")
314
+ if show_resources:
315
+ cmd.append("--show-resources")
316
+
317
+ logger.debug(f"Running command: {' '.join(cmd)}")
318
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
319
+
320
+ if result.returncode == 0:
321
+ status = json.loads(result.stdout) if result.stdout.strip() else {}
322
+ return {"success": True, "status": status}
323
+ else:
324
+ return {"success": False, "error": result.stderr.strip()}
325
+ except Exception as e:
326
+ logger.error(f"Error getting Helm status: {e}")
327
+ return {"success": False, "error": str(e)}
328
+
329
+ @server.tool(
330
+ annotations=ToolAnnotations(
331
+ title="Helm Release History",
332
+ readOnlyHint=True,
333
+ ),
334
+ )
335
+ def helm_history(
336
+ release_name: str,
337
+ namespace: str = "default",
338
+ max_revisions: int = 256
339
+ ) -> Dict[str, Any]:
340
+ """Get the revision history of a Helm release.
341
+
342
+ Args:
343
+ release_name: Name of the release
344
+ namespace: Kubernetes namespace
345
+ max_revisions: Maximum number of revisions to return
346
+ """
347
+ if not check_helm_fn():
348
+ return {"success": False, "error": "Helm is not available on this system"}
349
+
350
+ try:
351
+ cmd = ["helm", "history", release_name, "-n", namespace, "--output", "json", "--max", str(max_revisions)]
352
+
353
+ logger.debug(f"Running command: {' '.join(cmd)}")
354
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
355
+
356
+ if result.returncode == 0:
357
+ history = json.loads(result.stdout) if result.stdout.strip() else []
358
+ return {
359
+ "success": True,
360
+ "history": history,
361
+ "revisions": len(history)
362
+ }
363
+ else:
364
+ return {"success": False, "error": result.stderr.strip()}
365
+ except Exception as e:
366
+ logger.error(f"Error getting Helm history: {e}")
367
+ return {"success": False, "error": str(e)}
368
+
369
+ @server.tool(
370
+ annotations=ToolAnnotations(
371
+ title="Helm Get Values",
372
+ readOnlyHint=True,
373
+ ),
374
+ )
375
+ def helm_get_values(
376
+ release_name: str,
377
+ namespace: str = "default",
378
+ all_values: bool = False,
379
+ revision: Optional[int] = None
380
+ ) -> Dict[str, Any]:
381
+ """Get the values used for a Helm release.
382
+
383
+ Args:
384
+ release_name: Name of the release
385
+ namespace: Kubernetes namespace
386
+ all_values: Include computed (default + user) values
387
+ revision: Get values for a specific revision
388
+ """
389
+ if not check_helm_fn():
390
+ return {"success": False, "error": "Helm is not available on this system"}
391
+
392
+ try:
393
+ cmd = ["helm", "get", "values", release_name, "-n", namespace, "--output", "yaml"]
394
+
395
+ if all_values:
396
+ cmd.append("--all")
397
+ if revision:
398
+ cmd.extend(["--revision", str(revision)])
399
+
400
+ logger.debug(f"Running command: {' '.join(cmd)}")
401
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
402
+
403
+ if result.returncode == 0:
404
+ values = yaml.safe_load(result.stdout) if result.stdout.strip() else {}
405
+ return {"success": True, "values": values, "raw": result.stdout}
406
+ else:
407
+ return {"success": False, "error": result.stderr.strip()}
408
+ except Exception as e:
409
+ logger.error(f"Error getting Helm values: {e}")
410
+ return {"success": False, "error": str(e)}
411
+
412
+ @server.tool(
413
+ annotations=ToolAnnotations(
414
+ title="Helm Get Manifest",
415
+ readOnlyHint=True,
416
+ ),
417
+ )
418
+ def helm_get_manifest(
419
+ release_name: str,
420
+ namespace: str = "default",
421
+ revision: Optional[int] = None
422
+ ) -> Dict[str, Any]:
423
+ """Get the manifest (rendered templates) of a Helm release.
424
+
425
+ Args:
426
+ release_name: Name of the release
427
+ namespace: Kubernetes namespace
428
+ revision: Get manifest for a specific revision
429
+ """
430
+ if not check_helm_fn():
431
+ return {"success": False, "error": "Helm is not available on this system"}
432
+
433
+ try:
434
+ cmd = ["helm", "get", "manifest", release_name, "-n", namespace]
435
+
436
+ if revision:
437
+ cmd.extend(["--revision", str(revision)])
438
+
439
+ logger.debug(f"Running command: {' '.join(cmd)}")
440
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
441
+
442
+ if result.returncode == 0:
443
+ return {"success": True, "manifest": result.stdout}
444
+ else:
445
+ return {"success": False, "error": result.stderr.strip()}
446
+ except Exception as e:
447
+ logger.error(f"Error getting Helm manifest: {e}")
448
+ return {"success": False, "error": str(e)}
449
+
450
+ @server.tool(
451
+ annotations=ToolAnnotations(
452
+ title="Helm Get Notes",
453
+ readOnlyHint=True,
454
+ ),
455
+ )
456
+ def helm_get_notes(
457
+ release_name: str,
458
+ namespace: str = "default",
459
+ revision: Optional[int] = None
460
+ ) -> Dict[str, Any]:
461
+ """Get the notes (post-install message) of a Helm release.
462
+
463
+ Args:
464
+ release_name: Name of the release
465
+ namespace: Kubernetes namespace
466
+ revision: Get notes for a specific revision
467
+ """
468
+ if not check_helm_fn():
469
+ return {"success": False, "error": "Helm is not available on this system"}
470
+
471
+ try:
472
+ cmd = ["helm", "get", "notes", release_name, "-n", namespace]
473
+
474
+ if revision:
475
+ cmd.extend(["--revision", str(revision)])
476
+
477
+ logger.debug(f"Running command: {' '.join(cmd)}")
478
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
479
+
480
+ if result.returncode == 0:
481
+ return {"success": True, "notes": result.stdout}
482
+ else:
483
+ return {"success": False, "error": result.stderr.strip()}
484
+ except Exception as e:
485
+ logger.error(f"Error getting Helm notes: {e}")
486
+ return {"success": False, "error": str(e)}
487
+
488
+ @server.tool(
489
+ annotations=ToolAnnotations(
490
+ title="Helm Get Hooks",
491
+ readOnlyHint=True,
492
+ ),
493
+ )
494
+ def helm_get_hooks(
495
+ release_name: str,
496
+ namespace: str = "default",
497
+ revision: Optional[int] = None
498
+ ) -> Dict[str, Any]:
499
+ """Get the hooks of a Helm release.
500
+
501
+ Args:
502
+ release_name: Name of the release
503
+ namespace: Kubernetes namespace
504
+ revision: Get hooks for a specific revision
505
+ """
506
+ if not check_helm_fn():
507
+ return {"success": False, "error": "Helm is not available on this system"}
508
+
509
+ try:
510
+ cmd = ["helm", "get", "hooks", release_name, "-n", namespace]
511
+
512
+ if revision:
513
+ cmd.extend(["--revision", str(revision)])
514
+
515
+ logger.debug(f"Running command: {' '.join(cmd)}")
516
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
517
+
518
+ if result.returncode == 0:
519
+ return {"success": True, "hooks": result.stdout}
520
+ else:
521
+ return {"success": False, "error": result.stderr.strip()}
522
+ except Exception as e:
523
+ logger.error(f"Error getting Helm hooks: {e}")
524
+ return {"success": False, "error": str(e)}
525
+
526
+ @server.tool(
527
+ annotations=ToolAnnotations(
528
+ title="Helm Get All",
529
+ readOnlyHint=True,
530
+ ),
531
+ )
532
+ def helm_get_all(
533
+ release_name: str,
534
+ namespace: str = "default",
535
+ revision: Optional[int] = None
536
+ ) -> Dict[str, Any]:
537
+ """Get all information about a Helm release (values, manifest, hooks, notes).
538
+
539
+ Args:
540
+ release_name: Name of the release
541
+ namespace: Kubernetes namespace
542
+ revision: Get info for a specific revision
543
+ """
544
+ if not check_helm_fn():
545
+ return {"success": False, "error": "Helm is not available on this system"}
546
+
547
+ try:
548
+ cmd = ["helm", "get", "all", release_name, "-n", namespace]
549
+
550
+ if revision:
551
+ cmd.extend(["--revision", str(revision)])
552
+
553
+ logger.debug(f"Running command: {' '.join(cmd)}")
554
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
555
+
556
+ if result.returncode == 0:
557
+ return {"success": True, "release_info": result.stdout}
558
+ else:
559
+ return {"success": False, "error": result.stderr.strip()}
560
+ except Exception as e:
561
+ logger.error(f"Error getting all Helm info: {e}")
562
+ return {"success": False, "error": str(e)}
563
+
564
+ @server.tool(
565
+ annotations=ToolAnnotations(
566
+ title="Helm Show Chart",
567
+ readOnlyHint=True,
568
+ ),
569
+ )
570
+ def helm_show_chart(
571
+ chart: str,
572
+ repo: Optional[str] = None,
573
+ version: Optional[str] = None
574
+ ) -> Dict[str, Any]:
575
+ """Show the chart definition (Chart.yaml).
576
+
577
+ Args:
578
+ chart: Chart reference (e.g., 'nginx', 'bitnami/nginx', or local path)
579
+ repo: Repository URL (for OCI or HTTP repos)
580
+ version: Specific chart version
581
+ """
582
+ if not check_helm_fn():
583
+ return {"success": False, "error": "Helm is not available on this system"}
584
+
585
+ try:
586
+ cmd = ["helm", "show", "chart", chart]
587
+
588
+ if repo:
589
+ cmd.extend(["--repo", repo])
590
+ if version:
591
+ cmd.extend(["--version", version])
592
+
593
+ logger.debug(f"Running command: {' '.join(cmd)}")
594
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
595
+
596
+ if result.returncode == 0:
597
+ chart_info = yaml.safe_load(result.stdout) if result.stdout.strip() else {}
598
+ return {"success": True, "chart": chart_info, "raw": result.stdout}
599
+ else:
600
+ return {"success": False, "error": result.stderr.strip()}
601
+ except Exception as e:
602
+ logger.error(f"Error showing chart: {e}")
603
+ return {"success": False, "error": str(e)}
604
+
605
+ @server.tool(
606
+ annotations=ToolAnnotations(
607
+ title="Helm Show Values",
608
+ readOnlyHint=True,
609
+ ),
610
+ )
611
+ def helm_show_values(
612
+ chart: str,
613
+ repo: Optional[str] = None,
614
+ version: Optional[str] = None,
615
+ jsonpath: Optional[str] = None
616
+ ) -> Dict[str, Any]:
617
+ """Show the chart's default values.yaml.
618
+
619
+ Args:
620
+ chart: Chart reference (e.g., 'nginx', 'bitnami/nginx', or local path)
621
+ repo: Repository URL
622
+ version: Specific chart version
623
+ jsonpath: JSONPath expression to filter values
624
+ """
625
+ if not check_helm_fn():
626
+ return {"success": False, "error": "Helm is not available on this system"}
627
+
628
+ try:
629
+ cmd = ["helm", "show", "values", chart]
630
+
631
+ if repo:
632
+ cmd.extend(["--repo", repo])
633
+ if version:
634
+ cmd.extend(["--version", version])
635
+ if jsonpath:
636
+ cmd.extend(["--jsonpath", jsonpath])
637
+
638
+ logger.debug(f"Running command: {' '.join(cmd)}")
639
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
640
+
641
+ if result.returncode == 0:
642
+ values = yaml.safe_load(result.stdout) if result.stdout.strip() and not jsonpath else result.stdout
643
+ return {"success": True, "values": values, "raw": result.stdout}
644
+ else:
645
+ return {"success": False, "error": result.stderr.strip()}
646
+ except Exception as e:
647
+ logger.error(f"Error showing chart values: {e}")
648
+ return {"success": False, "error": str(e)}
649
+
650
+ @server.tool(
651
+ annotations=ToolAnnotations(
652
+ title="Helm Show Readme",
653
+ readOnlyHint=True,
654
+ ),
655
+ )
656
+ def helm_show_readme(
657
+ chart: str,
658
+ repo: Optional[str] = None,
659
+ version: Optional[str] = None
660
+ ) -> Dict[str, Any]:
661
+ """Show the chart's README file.
662
+
663
+ Args:
664
+ chart: Chart reference (e.g., 'nginx', 'bitnami/nginx', or local path)
665
+ repo: Repository URL
666
+ version: Specific chart version
667
+ """
668
+ if not check_helm_fn():
669
+ return {"success": False, "error": "Helm is not available on this system"}
670
+
671
+ try:
672
+ cmd = ["helm", "show", "readme", chart]
673
+
674
+ if repo:
675
+ cmd.extend(["--repo", repo])
676
+ if version:
677
+ cmd.extend(["--version", version])
678
+
679
+ logger.debug(f"Running command: {' '.join(cmd)}")
680
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
681
+
682
+ if result.returncode == 0:
683
+ return {"success": True, "readme": result.stdout}
684
+ else:
685
+ return {"success": False, "error": result.stderr.strip()}
686
+ except Exception as e:
687
+ logger.error(f"Error showing chart readme: {e}")
688
+ return {"success": False, "error": str(e)}
689
+
690
+ @server.tool(
691
+ annotations=ToolAnnotations(
692
+ title="Helm Show CRDs",
693
+ readOnlyHint=True,
694
+ ),
695
+ )
696
+ def helm_show_crds(
697
+ chart: str,
698
+ repo: Optional[str] = None,
699
+ version: Optional[str] = None
700
+ ) -> Dict[str, Any]:
701
+ """Show the chart's Custom Resource Definitions (CRDs).
702
+
703
+ Args:
704
+ chart: Chart reference
705
+ repo: Repository URL
706
+ version: Specific chart version
707
+ """
708
+ if not check_helm_fn():
709
+ return {"success": False, "error": "Helm is not available on this system"}
710
+
711
+ try:
712
+ cmd = ["helm", "show", "crds", chart]
713
+
714
+ if repo:
715
+ cmd.extend(["--repo", repo])
716
+ if version:
717
+ cmd.extend(["--version", version])
718
+
719
+ logger.debug(f"Running command: {' '.join(cmd)}")
720
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
721
+
722
+ if result.returncode == 0:
723
+ return {"success": True, "crds": result.stdout}
724
+ else:
725
+ return {"success": False, "error": result.stderr.strip()}
726
+ except Exception as e:
727
+ logger.error(f"Error showing chart CRDs: {e}")
728
+ return {"success": False, "error": str(e)}
729
+
730
+ @server.tool(
731
+ annotations=ToolAnnotations(
732
+ title="Helm Show All",
733
+ readOnlyHint=True,
734
+ ),
735
+ )
736
+ def helm_show_all(
737
+ chart: str,
738
+ repo: Optional[str] = None,
739
+ version: Optional[str] = None
740
+ ) -> Dict[str, Any]:
741
+ """Show all chart information (chart.yaml, values, readme, crds).
742
+
743
+ Args:
744
+ chart: Chart reference
745
+ repo: Repository URL
746
+ version: Specific chart version
747
+ """
748
+ if not check_helm_fn():
749
+ return {"success": False, "error": "Helm is not available on this system"}
750
+
751
+ try:
752
+ cmd = ["helm", "show", "all", chart]
753
+
754
+ if repo:
755
+ cmd.extend(["--repo", repo])
756
+ if version:
757
+ cmd.extend(["--version", version])
758
+
759
+ logger.debug(f"Running command: {' '.join(cmd)}")
760
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
761
+
762
+ if result.returncode == 0:
763
+ return {"success": True, "chart_info": result.stdout}
764
+ else:
765
+ return {"success": False, "error": result.stderr.strip()}
766
+ except Exception as e:
767
+ logger.error(f"Error showing all chart info: {e}")
768
+ return {"success": False, "error": str(e)}
769
+
770
+ @server.tool(
771
+ annotations=ToolAnnotations(
772
+ title="Helm Search Repo",
773
+ readOnlyHint=True,
774
+ ),
775
+ )
776
+ def helm_search_repo(
777
+ keyword: str,
778
+ regexp: bool = False,
779
+ versions: bool = False,
780
+ version: Optional[str] = None,
781
+ max_results: int = 50
782
+ ) -> Dict[str, Any]:
783
+ """Search for charts in configured Helm repositories.
784
+
785
+ Args:
786
+ keyword: Search keyword
787
+ regexp: Use regular expression for searching
788
+ versions: Show all chart versions (not just latest)
789
+ version: Version constraint (e.g., '>1.0.0')
790
+ max_results: Maximum number of results
791
+ """
792
+ if not check_helm_fn():
793
+ return {"success": False, "error": "Helm is not available on this system"}
794
+
795
+ try:
796
+ cmd = ["helm", "search", "repo", keyword, "--output", "json", "--max-col-width", "0"]
797
+
798
+ if regexp:
799
+ cmd.append("--regexp")
800
+ if versions:
801
+ cmd.append("--versions")
802
+ if version:
803
+ cmd.extend(["--version", version])
804
+
805
+ logger.debug(f"Running command: {' '.join(cmd)}")
806
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
807
+
808
+ if result.returncode == 0:
809
+ charts = json.loads(result.stdout) if result.stdout.strip() else []
810
+ return {
811
+ "success": True,
812
+ "charts": charts[:max_results],
813
+ "count": len(charts[:max_results])
814
+ }
815
+ else:
816
+ return {"success": False, "error": result.stderr.strip()}
817
+ except Exception as e:
818
+ logger.error(f"Error searching repos: {e}")
819
+ return {"success": False, "error": str(e)}
820
+
821
+ @server.tool(
822
+ annotations=ToolAnnotations(
823
+ title="Helm Search Hub",
824
+ readOnlyHint=True,
825
+ ),
826
+ )
827
+ def helm_search_hub(
828
+ keyword: str,
829
+ max_results: int = 50,
830
+ list_repo_url: bool = True
831
+ ) -> Dict[str, Any]:
832
+ """Search for charts in Artifact Hub (https://artifacthub.io).
833
+
834
+ Args:
835
+ keyword: Search keyword
836
+ max_results: Maximum number of results
837
+ list_repo_url: Show repository URL for each chart
838
+ """
839
+ if not check_helm_fn():
840
+ return {"success": False, "error": "Helm is not available on this system"}
841
+
842
+ try:
843
+ cmd = ["helm", "search", "hub", keyword, "--output", "json", "--max-col-width", "0"]
844
+
845
+ if list_repo_url:
846
+ cmd.append("--list-repo-url")
847
+
848
+ logger.debug(f"Running command: {' '.join(cmd)}")
849
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
850
+
851
+ if result.returncode == 0:
852
+ charts = json.loads(result.stdout) if result.stdout.strip() else []
853
+ return {
854
+ "success": True,
855
+ "charts": charts[:max_results],
856
+ "count": len(charts[:max_results]),
857
+ "source": "Artifact Hub (artifacthub.io)"
858
+ }
859
+ else:
860
+ return {"success": False, "error": result.stderr.strip()}
861
+ except Exception as e:
862
+ logger.error(f"Error searching Artifact Hub: {e}")
863
+ return {"success": False, "error": str(e)}
864
+
865
+ @server.tool(
866
+ annotations=ToolAnnotations(
867
+ title="Helm Repo List",
868
+ readOnlyHint=True,
869
+ ),
870
+ )
871
+ def helm_repo_list() -> Dict[str, Any]:
872
+ """List all configured Helm repositories."""
873
+ if not check_helm_fn():
874
+ return {"success": False, "error": "Helm is not available on this system"}
875
+
876
+ try:
877
+ cmd = ["helm", "repo", "list", "--output", "json"]
878
+
879
+ logger.debug(f"Running command: {' '.join(cmd)}")
880
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
881
+
882
+ if result.returncode == 0:
883
+ repos = json.loads(result.stdout) if result.stdout.strip() else []
884
+ return {
885
+ "success": True,
886
+ "repositories": repos,
887
+ "count": len(repos)
888
+ }
889
+ else:
890
+ if "no repositories to show" in result.stderr.lower():
891
+ return {"success": True, "repositories": [], "count": 0}
892
+ return {"success": False, "error": result.stderr.strip()}
893
+ except Exception as e:
894
+ logger.error(f"Error listing repos: {e}")
895
+ return {"success": False, "error": str(e)}
896
+
897
+ @server.tool(
898
+ annotations=ToolAnnotations(
899
+ title="Helm Repo Add",
900
+ destructiveHint=False,
901
+ ),
902
+ )
903
+ def helm_repo_add(
904
+ name: str,
905
+ url: str,
906
+ username: Optional[str] = None,
907
+ password: Optional[str] = None,
908
+ force_update: bool = False,
909
+ pass_credentials: bool = False
910
+ ) -> Dict[str, Any]:
911
+ """Add a Helm chart repository.
912
+
913
+ Args:
914
+ name: Repository name (local alias)
915
+ url: Repository URL
916
+ username: Username for basic auth
917
+ password: Password for basic auth
918
+ force_update: Replace existing repo with same name
919
+ pass_credentials: Pass credentials to all domains
920
+ """
921
+ if not check_helm_fn():
922
+ return {"success": False, "error": "Helm is not available on this system"}
923
+
924
+ try:
925
+ cmd = ["helm", "repo", "add", name, url]
926
+
927
+ if username:
928
+ cmd.extend(["--username", username])
929
+ if password:
930
+ cmd.extend(["--password", password])
931
+ if force_update:
932
+ cmd.append("--force-update")
933
+ if pass_credentials:
934
+ cmd.append("--pass-credentials")
935
+
936
+ logger.debug(f"Running command: {' '.join(cmd[:4])}...")
937
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
938
+
939
+ if result.returncode == 0:
940
+ return {
941
+ "success": True,
942
+ "message": f"Repository '{name}' added successfully",
943
+ "details": result.stdout.strip()
944
+ }
945
+ else:
946
+ return {"success": False, "error": result.stderr.strip()}
947
+ except Exception as e:
948
+ logger.error(f"Error adding repo: {e}")
949
+ return {"success": False, "error": str(e)}
950
+
951
+ @server.tool(
952
+ annotations=ToolAnnotations(
953
+ title="Helm Repo Remove",
954
+ destructiveHint=True,
955
+ ),
956
+ )
957
+ def helm_repo_remove(name: str) -> Dict[str, Any]:
958
+ """Remove a Helm chart repository.
959
+
960
+ Args:
961
+ name: Repository name to remove
962
+ """
963
+ if not check_helm_fn():
964
+ return {"success": False, "error": "Helm is not available on this system"}
965
+
966
+ try:
967
+ cmd = ["helm", "repo", "remove", name]
968
+
969
+ logger.debug(f"Running command: {' '.join(cmd)}")
970
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
971
+
972
+ if result.returncode == 0:
973
+ return {
974
+ "success": True,
975
+ "message": f"Repository '{name}' removed successfully",
976
+ "details": result.stdout.strip()
977
+ }
978
+ else:
979
+ return {"success": False, "error": result.stderr.strip()}
980
+ except Exception as e:
981
+ logger.error(f"Error removing repo: {e}")
982
+ return {"success": False, "error": str(e)}
983
+
984
+ @server.tool(
985
+ annotations=ToolAnnotations(
986
+ title="Helm Repo Update",
987
+ readOnlyHint=False,
988
+ ),
989
+ )
990
+ def helm_repo_update(repos: Optional[List[str]] = None) -> Dict[str, Any]:
991
+ """Update Helm repository indexes.
992
+
993
+ Args:
994
+ repos: Specific repositories to update (default: all)
995
+ """
996
+ if not check_helm_fn():
997
+ return {"success": False, "error": "Helm is not available on this system"}
998
+
999
+ try:
1000
+ cmd = ["helm", "repo", "update"]
1001
+
1002
+ if repos:
1003
+ cmd.extend(repos)
1004
+
1005
+ logger.debug(f"Running command: {' '.join(cmd)}")
1006
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
1007
+
1008
+ if result.returncode == 0:
1009
+ return {
1010
+ "success": True,
1011
+ "message": "Repositories updated successfully",
1012
+ "details": result.stdout.strip()
1013
+ }
1014
+ else:
1015
+ return {"success": False, "error": result.stderr.strip()}
1016
+ except Exception as e:
1017
+ logger.error(f"Error updating repos: {e}")
1018
+ return {"success": False, "error": str(e)}
1019
+
1020
+ @server.tool(
1021
+ annotations=ToolAnnotations(
1022
+ title="Helm Rollback",
1023
+ destructiveHint=True,
1024
+ ),
1025
+ )
1026
+ def helm_rollback(
1027
+ release_name: str,
1028
+ revision: int,
1029
+ namespace: str = "default",
1030
+ force: bool = False,
1031
+ recreate_pods: bool = False,
1032
+ cleanup_on_fail: bool = False,
1033
+ wait: bool = False,
1034
+ timeout: str = "5m0s"
1035
+ ) -> Dict[str, Any]:
1036
+ """Rollback a Helm release to a previous revision.
1037
+
1038
+ Args:
1039
+ release_name: Name of the release
1040
+ revision: Revision number to rollback to (use helm_history to see revisions)
1041
+ namespace: Kubernetes namespace
1042
+ force: Force resource updates through delete/recreate
1043
+ recreate_pods: Force pod restarts
1044
+ cleanup_on_fail: Delete newly created resources on failure
1045
+ wait: Wait until all resources are ready
1046
+ timeout: Timeout for waiting
1047
+ """
1048
+ if non_destructive:
1049
+ return {"success": False, "error": "Blocked: non-destructive mode"}
1050
+ if not check_helm_fn():
1051
+ return {"success": False, "error": "Helm is not available on this system"}
1052
+
1053
+ try:
1054
+ cmd = ["helm", "rollback", release_name, str(revision), "-n", namespace]
1055
+
1056
+ if force:
1057
+ cmd.append("--force")
1058
+ if recreate_pods:
1059
+ cmd.append("--recreate-pods")
1060
+ if cleanup_on_fail:
1061
+ cmd.append("--cleanup-on-fail")
1062
+ if wait:
1063
+ cmd.append("--wait")
1064
+ cmd.extend(["--timeout", timeout])
1065
+
1066
+ logger.debug(f"Running command: {' '.join(cmd)}")
1067
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
1068
+
1069
+ if result.returncode == 0:
1070
+ return {
1071
+ "success": True,
1072
+ "message": f"Release '{release_name}' rolled back to revision {revision}",
1073
+ "details": result.stdout.strip()
1074
+ }
1075
+ else:
1076
+ return {"success": False, "error": result.stderr.strip()}
1077
+ except Exception as e:
1078
+ logger.error(f"Error rolling back: {e}")
1079
+ return {"success": False, "error": str(e)}
1080
+
1081
+ @server.tool(
1082
+ annotations=ToolAnnotations(
1083
+ title="Helm Test",
1084
+ readOnlyHint=False,
1085
+ ),
1086
+ )
1087
+ def helm_test(
1088
+ release_name: str,
1089
+ namespace: str = "default",
1090
+ timeout: str = "5m0s",
1091
+ logs: bool = True,
1092
+ filter: Optional[str] = None
1093
+ ) -> Dict[str, Any]:
1094
+ """Run tests for a Helm release.
1095
+
1096
+ Args:
1097
+ release_name: Name of the release
1098
+ namespace: Kubernetes namespace
1099
+ timeout: Timeout for tests
1100
+ logs: Show test pod logs
1101
+ filter: Filter tests by name
1102
+ """
1103
+ if not check_helm_fn():
1104
+ return {"success": False, "error": "Helm is not available on this system"}
1105
+
1106
+ try:
1107
+ cmd = ["helm", "test", release_name, "-n", namespace, "--timeout", timeout]
1108
+
1109
+ if logs:
1110
+ cmd.append("--logs")
1111
+ if filter:
1112
+ cmd.extend(["--filter", filter])
1113
+
1114
+ logger.debug(f"Running command: {' '.join(cmd)}")
1115
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
1116
+
1117
+ if result.returncode == 0:
1118
+ return {
1119
+ "success": True,
1120
+ "message": f"Tests passed for release '{release_name}'",
1121
+ "output": result.stdout.strip()
1122
+ }
1123
+ else:
1124
+ return {
1125
+ "success": False,
1126
+ "error": "Tests failed",
1127
+ "output": result.stdout.strip(),
1128
+ "stderr": result.stderr.strip()
1129
+ }
1130
+ except Exception as e:
1131
+ logger.error(f"Error running tests: {e}")
1132
+ return {"success": False, "error": str(e)}
1133
+
1134
+ @server.tool(
1135
+ annotations=ToolAnnotations(
1136
+ title="Helm Lint",
1137
+ readOnlyHint=True,
1138
+ ),
1139
+ )
1140
+ def helm_lint(
1141
+ chart_path: str,
1142
+ values: Optional[str] = None,
1143
+ strict: bool = False,
1144
+ with_subcharts: bool = False
1145
+ ) -> Dict[str, Any]:
1146
+ """Lint a Helm chart for issues.
1147
+
1148
+ Args:
1149
+ chart_path: Path to the chart directory
1150
+ values: Values file path or --set values
1151
+ strict: Fail on lint warnings
1152
+ with_subcharts: Lint dependent charts
1153
+ """
1154
+ if not check_helm_fn():
1155
+ return {"success": False, "error": "Helm is not available on this system"}
1156
+
1157
+ try:
1158
+ cmd = ["helm", "lint", chart_path]
1159
+
1160
+ if values:
1161
+ if values.endswith(('.yaml', '.yml', '.json')):
1162
+ cmd.extend(["-f", values])
1163
+ else:
1164
+ cmd.extend(["--set", values])
1165
+ if strict:
1166
+ cmd.append("--strict")
1167
+ if with_subcharts:
1168
+ cmd.append("--with-subcharts")
1169
+
1170
+ logger.debug(f"Running command: {' '.join(cmd)}")
1171
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
1172
+
1173
+ if result.returncode == 0:
1174
+ return {
1175
+ "success": True,
1176
+ "message": "Chart linting passed",
1177
+ "output": result.stdout.strip()
1178
+ }
1179
+ else:
1180
+ return {
1181
+ "success": False,
1182
+ "error": "Chart linting failed",
1183
+ "output": result.stdout.strip(),
1184
+ "stderr": result.stderr.strip()
1185
+ }
1186
+ except Exception as e:
1187
+ logger.error(f"Error linting chart: {e}")
1188
+ return {"success": False, "error": str(e)}
1189
+
1190
+ @server.tool(
1191
+ annotations=ToolAnnotations(
1192
+ title="Helm Package",
1193
+ readOnlyHint=False,
1194
+ ),
1195
+ )
1196
+ def helm_package(
1197
+ chart_path: str,
1198
+ destination: Optional[str] = None,
1199
+ version: Optional[str] = None,
1200
+ app_version: Optional[str] = None,
1201
+ dependency_update: bool = False
1202
+ ) -> Dict[str, Any]:
1203
+ """Package a Helm chart into a versioned archive.
1204
+
1205
+ Args:
1206
+ chart_path: Path to the chart directory
1207
+ destination: Output directory for the package
1208
+ version: Override the chart version
1209
+ app_version: Override the app version
1210
+ dependency_update: Update dependencies before packaging
1211
+ """
1212
+ if not check_helm_fn():
1213
+ return {"success": False, "error": "Helm is not available on this system"}
1214
+
1215
+ try:
1216
+ cmd = ["helm", "package", chart_path]
1217
+
1218
+ if destination:
1219
+ cmd.extend(["--destination", destination])
1220
+ if version:
1221
+ cmd.extend(["--version", version])
1222
+ if app_version:
1223
+ cmd.extend(["--app-version", app_version])
1224
+ if dependency_update:
1225
+ cmd.append("--dependency-update")
1226
+
1227
+ logger.debug(f"Running command: {' '.join(cmd)}")
1228
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
1229
+
1230
+ if result.returncode == 0:
1231
+ return {
1232
+ "success": True,
1233
+ "message": "Chart packaged successfully",
1234
+ "output": result.stdout.strip()
1235
+ }
1236
+ else:
1237
+ return {"success": False, "error": result.stderr.strip()}
1238
+ except Exception as e:
1239
+ logger.error(f"Error packaging chart: {e}")
1240
+ return {"success": False, "error": str(e)}
1241
+
1242
+ @server.tool(
1243
+ annotations=ToolAnnotations(
1244
+ title="Helm Dependency Update",
1245
+ readOnlyHint=False,
1246
+ ),
1247
+ )
1248
+ def helm_dependency_update(chart_path: str, skip_refresh: bool = False) -> Dict[str, Any]:
1249
+ """Update chart dependencies (download from Chart.yaml).
1250
+
1251
+ Args:
1252
+ chart_path: Path to the chart directory
1253
+ skip_refresh: Don't refresh repo cache
1254
+ """
1255
+ if not check_helm_fn():
1256
+ return {"success": False, "error": "Helm is not available on this system"}
1257
+
1258
+ try:
1259
+ cmd = ["helm", "dependency", "update", chart_path]
1260
+
1261
+ if skip_refresh:
1262
+ cmd.append("--skip-refresh")
1263
+
1264
+ logger.debug(f"Running command: {' '.join(cmd)}")
1265
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
1266
+
1267
+ if result.returncode == 0:
1268
+ return {
1269
+ "success": True,
1270
+ "message": "Dependencies updated successfully",
1271
+ "output": result.stdout.strip()
1272
+ }
1273
+ else:
1274
+ return {"success": False, "error": result.stderr.strip()}
1275
+ except Exception as e:
1276
+ logger.error(f"Error updating dependencies: {e}")
1277
+ return {"success": False, "error": str(e)}
1278
+
1279
+ @server.tool(
1280
+ annotations=ToolAnnotations(
1281
+ title="Helm Dependency List",
1282
+ readOnlyHint=True,
1283
+ ),
1284
+ )
1285
+ def helm_dependency_list(chart_path: str) -> Dict[str, Any]:
1286
+ """List chart dependencies.
1287
+
1288
+ Args:
1289
+ chart_path: Path to the chart directory
1290
+ """
1291
+ if not check_helm_fn():
1292
+ return {"success": False, "error": "Helm is not available on this system"}
1293
+
1294
+ try:
1295
+ cmd = ["helm", "dependency", "list", chart_path]
1296
+
1297
+ logger.debug(f"Running command: {' '.join(cmd)}")
1298
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
1299
+
1300
+ if result.returncode == 0:
1301
+ return {
1302
+ "success": True,
1303
+ "dependencies": result.stdout.strip()
1304
+ }
1305
+ else:
1306
+ return {"success": False, "error": result.stderr.strip()}
1307
+ except Exception as e:
1308
+ logger.error(f"Error listing dependencies: {e}")
1309
+ return {"success": False, "error": str(e)}
1310
+
1311
+ @server.tool(
1312
+ annotations=ToolAnnotations(
1313
+ title="Helm Dependency Build",
1314
+ readOnlyHint=False,
1315
+ ),
1316
+ )
1317
+ def helm_dependency_build(chart_path: str, skip_refresh: bool = False) -> Dict[str, Any]:
1318
+ """Build chart dependencies from Chart.lock.
1319
+
1320
+ Args:
1321
+ chart_path: Path to the chart directory
1322
+ skip_refresh: Don't refresh repo cache
1323
+ """
1324
+ if not check_helm_fn():
1325
+ return {"success": False, "error": "Helm is not available on this system"}
1326
+
1327
+ try:
1328
+ cmd = ["helm", "dependency", "build", chart_path]
1329
+
1330
+ if skip_refresh:
1331
+ cmd.append("--skip-refresh")
1332
+
1333
+ logger.debug(f"Running command: {' '.join(cmd)}")
1334
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
1335
+
1336
+ if result.returncode == 0:
1337
+ return {
1338
+ "success": True,
1339
+ "message": "Dependencies built successfully",
1340
+ "output": result.stdout.strip()
1341
+ }
1342
+ else:
1343
+ return {"success": False, "error": result.stderr.strip()}
1344
+ except Exception as e:
1345
+ logger.error(f"Error building dependencies: {e}")
1346
+ return {"success": False, "error": str(e)}
1347
+
1348
+ @server.tool(
1349
+ annotations=ToolAnnotations(
1350
+ title="Helm Pull",
1351
+ readOnlyHint=False,
1352
+ ),
1353
+ )
1354
+ def helm_pull(
1355
+ chart: str,
1356
+ repo: Optional[str] = None,
1357
+ version: Optional[str] = None,
1358
+ destination: Optional[str] = None,
1359
+ untar: bool = True
1360
+ ) -> Dict[str, Any]:
1361
+ """Download a chart from a repository.
1362
+
1363
+ Args:
1364
+ chart: Chart reference (e.g., 'bitnami/nginx')
1365
+ repo: Repository URL
1366
+ version: Specific chart version
1367
+ destination: Download directory
1368
+ untar: Extract the chart archive
1369
+ """
1370
+ if not check_helm_fn():
1371
+ return {"success": False, "error": "Helm is not available on this system"}
1372
+
1373
+ try:
1374
+ cmd = ["helm", "pull", chart]
1375
+
1376
+ if repo:
1377
+ cmd.extend(["--repo", repo])
1378
+ if version:
1379
+ cmd.extend(["--version", version])
1380
+ if destination:
1381
+ cmd.extend(["--destination", destination])
1382
+ if untar:
1383
+ cmd.append("--untar")
1384
+
1385
+ logger.debug(f"Running command: {' '.join(cmd)}")
1386
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
1387
+
1388
+ if result.returncode == 0:
1389
+ return {
1390
+ "success": True,
1391
+ "message": f"Chart '{chart}' downloaded successfully",
1392
+ "output": result.stdout.strip() if result.stdout.strip() else "Download complete"
1393
+ }
1394
+ else:
1395
+ return {"success": False, "error": result.stderr.strip()}
1396
+ except Exception as e:
1397
+ logger.error(f"Error pulling chart: {e}")
1398
+ return {"success": False, "error": str(e)}
1399
+
1400
+ @server.tool(
1401
+ annotations=ToolAnnotations(
1402
+ title="Helm Create",
1403
+ readOnlyHint=False,
1404
+ ),
1405
+ )
1406
+ def helm_create(name: str, starter: Optional[str] = None) -> Dict[str, Any]:
1407
+ """Create a new Helm chart with the given name.
1408
+
1409
+ Args:
1410
+ name: Name of the chart to create
1411
+ starter: Name of the starter chart to use
1412
+ """
1413
+ if not check_helm_fn():
1414
+ return {"success": False, "error": "Helm is not available on this system"}
1415
+
1416
+ try:
1417
+ cmd = ["helm", "create", name]
1418
+
1419
+ if starter:
1420
+ cmd.extend(["--starter", starter])
1421
+
1422
+ logger.debug(f"Running command: {' '.join(cmd)}")
1423
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
1424
+
1425
+ if result.returncode == 0:
1426
+ return {
1427
+ "success": True,
1428
+ "message": f"Chart '{name}' created successfully",
1429
+ "output": result.stdout.strip()
1430
+ }
1431
+ else:
1432
+ return {"success": False, "error": result.stderr.strip()}
1433
+ except Exception as e:
1434
+ logger.error(f"Error creating chart: {e}")
1435
+ return {"success": False, "error": str(e)}
1436
+
1437
+ @server.tool(
1438
+ annotations=ToolAnnotations(
1439
+ title="Helm Version",
1440
+ readOnlyHint=True,
1441
+ ),
1442
+ )
1443
+ def helm_version() -> Dict[str, Any]:
1444
+ """Get the Helm client version information."""
1445
+ if not check_helm_fn():
1446
+ return {"success": False, "error": "Helm is not available on this system"}
1447
+
1448
+ try:
1449
+ cmd = ["helm", "version", "--short"]
1450
+
1451
+ logger.debug(f"Running command: {' '.join(cmd)}")
1452
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
1453
+
1454
+ if result.returncode == 0:
1455
+ return {
1456
+ "success": True,
1457
+ "version": result.stdout.strip()
1458
+ }
1459
+ else:
1460
+ return {"success": False, "error": result.stderr.strip()}
1461
+ except Exception as e:
1462
+ logger.error(f"Error getting Helm version: {e}")
1463
+ return {"success": False, "error": str(e)}
1464
+
1465
+ @server.tool(
1466
+ annotations=ToolAnnotations(
1467
+ title="Helm Environment",
1468
+ readOnlyHint=True,
1469
+ ),
1470
+ )
1471
+ def helm_env() -> Dict[str, Any]:
1472
+ """Get Helm environment information (paths, settings)."""
1473
+ if not check_helm_fn():
1474
+ return {"success": False, "error": "Helm is not available on this system"}
1475
+
1476
+ try:
1477
+ cmd = ["helm", "env"]
1478
+
1479
+ logger.debug(f"Running command: {' '.join(cmd)}")
1480
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
1481
+
1482
+ if result.returncode == 0:
1483
+ env_dict = {}
1484
+ for line in result.stdout.strip().split('\n'):
1485
+ if '=' in line:
1486
+ key, value = line.split('=', 1)
1487
+ env_dict[key] = value.strip('"')
1488
+ return {
1489
+ "success": True,
1490
+ "environment": env_dict,
1491
+ "raw": result.stdout.strip()
1492
+ }
1493
+ else:
1494
+ return {"success": False, "error": result.stderr.strip()}
1495
+ except Exception as e:
1496
+ logger.error(f"Error getting Helm environment: {e}")
1497
+ return {"success": False, "error": str(e)}
1498
+
1499
+ @server.tool(
1500
+ annotations=ToolAnnotations(
1501
+ title="Helm Template",
1502
+ readOnlyHint=True,
1503
+ ),
1504
+ )
1505
+ def helm_template(
1506
+ chart: str,
1507
+ name: str,
1508
+ namespace: str = "default",
1509
+ repo: Optional[str] = None,
1510
+ values: Optional[str] = None
1511
+ ) -> Dict[str, Any]:
1512
+ """Render Helm chart templates locally without installing."""
1513
+ try:
1514
+ cmd = ["helm", "template", name, chart, "-n", namespace]
1515
+ if repo:
1516
+ cmd.extend(["--repo", repo])
1517
+ if values:
1518
+ cmd.extend(["--set", values])
1519
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
1520
+ if result.returncode == 0:
1521
+ return {"success": True, "manifest": result.stdout}
1522
+ else:
1523
+ return {"success": False, "error": result.stderr.strip()}
1524
+ except Exception as e:
1525
+ logger.error(f"Error templating chart: {e}")
1526
+ return {"success": False, "error": str(e)}
1527
+
1528
+ @server.tool(
1529
+ annotations=ToolAnnotations(
1530
+ title="Helm Template Apply",
1531
+ destructiveHint=True,
1532
+ ),
1533
+ )
1534
+ def helm_template_apply(
1535
+ chart: str,
1536
+ name: str,
1537
+ namespace: str = "default",
1538
+ repo: Optional[str] = None,
1539
+ values: Optional[str] = None
1540
+ ) -> Dict[str, Any]:
1541
+ """Render and apply Helm chart (bypasses Tiller/auth issues)."""
1542
+ if non_destructive:
1543
+ return {"success": False, "error": "Operation blocked: non-destructive mode enabled"}
1544
+ try:
1545
+ cmd = ["helm", "template", name, chart, "-n", namespace]
1546
+ if repo:
1547
+ cmd.extend(["--repo", repo])
1548
+ if values:
1549
+ cmd.extend(["--set", values])
1550
+ template_result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
1551
+ if template_result.returncode != 0:
1552
+ return {"success": False, "error": template_result.stderr.strip()}
1553
+ apply_cmd = ["kubectl", "apply", "-f", "-", "-n", namespace]
1554
+ apply_result = subprocess.run(apply_cmd, input=template_result.stdout, capture_output=True, text=True, timeout=60)
1555
+ if apply_result.returncode == 0:
1556
+ return {"success": True, "output": apply_result.stdout.strip()}
1557
+ else:
1558
+ return {"success": False, "error": apply_result.stderr.strip()}
1559
+ except Exception as e:
1560
+ logger.error(f"Error applying template: {e}")
1561
+ return {"success": False, "error": str(e)}