kubectl-mcp-server 1.22.0__tar.gz → 1.23.1__tar.gz

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 (88) hide show
  1. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/PKG-INFO +1 -1
  2. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/PKG-INFO +1 -1
  3. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/SOURCES.txt +1 -0
  4. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/__init__.py +1 -1
  5. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/k8s_config.py +27 -4
  6. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/mcp_server.py +8 -24
  7. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/providers.py +30 -2
  8. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/safety.py +1 -53
  9. kubectl_mcp_server-1.23.1/kubectl_mcp_tool/tools/_cli_utils.py +75 -0
  10. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/cluster.py +2 -8
  11. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/core.py +2 -8
  12. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/cost.py +5 -12
  13. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/deployments.py +2 -8
  14. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/diagnostics.py +2 -9
  15. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/helm.py +44 -44
  16. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/kind.py +4 -8
  17. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/networking.py +18 -14
  18. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/operations.py +39 -13
  19. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/pods.py +9 -12
  20. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/vind.py +2 -7
  21. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/setup.py +1 -1
  22. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_browser.py +2 -3
  23. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_ecosystem.py +2 -4
  24. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_mcp_integration.py +4 -26
  25. kubectl_mcp_server-1.23.1/tests/test_safety.py +98 -0
  26. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_server.py +4 -5
  27. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_tools.py +48 -40
  28. kubectl_mcp_server-1.22.0/tests/test_safety.py +0 -218
  29. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/LICENSE +0 -0
  30. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/README.md +0 -0
  31. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/dependency_links.txt +0 -0
  32. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/entry_points.txt +0 -0
  33. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/requires.txt +0 -0
  34. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/top_level.txt +0 -0
  35. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/__main__.py +0 -0
  36. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/auth/__init__.py +0 -0
  37. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/auth/config.py +0 -0
  38. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/auth/scopes.py +0 -0
  39. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/auth/verifier.py +0 -0
  40. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/cli/__init__.py +0 -0
  41. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/cli/__main__.py +0 -0
  42. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/cli/cli.py +0 -0
  43. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/cli/errors.py +0 -0
  44. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/cli/output.py +0 -0
  45. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/config/__init__.py +0 -0
  46. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/config/loader.py +0 -0
  47. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/config/schema.py +0 -0
  48. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/crd_detector.py +0 -0
  49. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/diagnostics.py +0 -0
  50. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/observability/__init__.py +0 -0
  51. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/observability/metrics.py +0 -0
  52. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/observability/stats.py +0 -0
  53. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/observability/tracing.py +0 -0
  54. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/prompts/__init__.py +0 -0
  55. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/prompts/builtin.py +0 -0
  56. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/prompts/custom.py +0 -0
  57. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/prompts/prompts.py +0 -0
  58. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/resources/__init__.py +0 -0
  59. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/resources/resources.py +0 -0
  60. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/__init__.py +0 -0
  61. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/backup.py +0 -0
  62. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/browser.py +0 -0
  63. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/capi.py +0 -0
  64. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/certs.py +0 -0
  65. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/cilium.py +0 -0
  66. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/gitops.py +0 -0
  67. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/keda.py +0 -0
  68. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/kiali.py +0 -0
  69. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/kubevirt.py +0 -0
  70. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/policy.py +0 -0
  71. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/rollouts.py +0 -0
  72. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/security.py +0 -0
  73. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/storage.py +0 -0
  74. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/ui.py +0 -0
  75. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/utils.py +0 -0
  76. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/utils/__init__.py +0 -0
  77. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/utils/helpers.py +0 -0
  78. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/setup.cfg +0 -0
  79. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/__init__.py +0 -0
  80. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/conftest.py +0 -0
  81. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_auth.py +0 -0
  82. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_cli.py +0 -0
  83. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_config.py +0 -0
  84. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_kind.py +0 -0
  85. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_observability.py +0 -0
  86. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_prompts.py +0 -0
  87. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_resources.py +0 -0
  88. {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_vind.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kubectl-mcp-server
3
- Version: 1.22.0
3
+ Version: 1.23.1
4
4
  Summary: A Model Context Protocol (MCP) server for Kubernetes with 270+ tools, 8 resources, and 8 prompts
5
5
  Home-page: https://github.com/rohitg00/kubectl-mcp-server
6
6
  Author: Rohit Ghumare
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kubectl-mcp-server
3
- Version: 1.22.0
3
+ Version: 1.23.1
4
4
  Summary: A Model Context Protocol (MCP) server for Kubernetes with 270+ tools, 8 resources, and 8 prompts
5
5
  Home-page: https://github.com/rohitg00/kubectl-mcp-server
6
6
  Author: Rohit Ghumare
@@ -38,6 +38,7 @@ kubectl_mcp_tool/prompts/prompts.py
38
38
  kubectl_mcp_tool/resources/__init__.py
39
39
  kubectl_mcp_tool/resources/resources.py
40
40
  kubectl_mcp_tool/tools/__init__.py
41
+ kubectl_mcp_tool/tools/_cli_utils.py
41
42
  kubectl_mcp_tool/tools/backup.py
42
43
  kubectl_mcp_tool/tools/browser.py
43
44
  kubectl_mcp_tool/tools/capi.py
@@ -7,7 +7,7 @@ with Kubernetes clusters through natural language commands.
7
7
  For more information, see: https://github.com/rohitg00/kubectl-mcp-server
8
8
  """
9
9
 
10
- __version__ = "1.22.0"
10
+ __version__ = "1.23.1"
11
11
 
12
12
  from .mcp_server import MCPServer
13
13
  from .diagnostics import run_diagnostics, check_kubectl_installation, check_cluster_connection
@@ -203,6 +203,7 @@ try:
203
203
  KubernetesProvider,
204
204
  ProviderConfig,
205
205
  ProviderType,
206
+ ProviderError,
206
207
  UnknownContextError,
207
208
  get_provider,
208
209
  get_context_names,
@@ -262,9 +263,21 @@ def load_kubernetes_config(context: str = ""):
262
263
 
263
264
 
264
265
  def _patched_load_kube_config(*args, **kwargs):
265
- """Patched version of load_kube_config that tries in-cluster first."""
266
+ """Patched version of load_kube_config that tries in-cluster first.
267
+
268
+ When called with explicit arguments (config_file, context, client_configuration),
269
+ always pass through to the original function. Only try in-cluster as a
270
+ fallback for bare calls during initial module loading.
271
+ """
266
272
  global _config_loaded, _original_load_kube_config
267
273
 
274
+ has_explicit_args = bool(args) or bool(kwargs)
275
+
276
+ if has_explicit_args and _original_load_kube_config:
277
+ _original_load_kube_config(*args, **kwargs)
278
+ _config_loaded = True
279
+ return
280
+
268
281
  if _config_loaded:
269
282
  return
270
283
 
@@ -326,7 +339,7 @@ def _load_config_for_context(context: str = "") -> Any:
326
339
  try:
327
340
  provider = get_provider()
328
341
  return provider.get_api_client(context)
329
- except UnknownContextError:
342
+ except (UnknownContextError, ProviderError):
330
343
  raise
331
344
  except Exception as e:
332
345
  logger.warning(f"Provider failed, falling back to basic config: {e}")
@@ -338,11 +351,20 @@ def _load_config_for_context(context: str = "") -> Any:
338
351
  config.load_incluster_config()
339
352
  return client.ApiClient()
340
353
  except ConfigException:
341
- pass
354
+ logger.debug("In-cluster config not available in fallback path")
342
355
 
343
356
  kubeconfig_path = os.environ.get('KUBECONFIG', '~/.kube/config')
344
357
  kubeconfig_path = os.path.expanduser(kubeconfig_path)
345
358
 
359
+ if not os.path.exists(kubeconfig_path):
360
+ raise RuntimeError(
361
+ f"No Kubernetes configuration found. "
362
+ f"In-cluster config not available and kubeconfig not found at '{kubeconfig_path}'. "
363
+ f"If running in a Kubernetes pod, ensure the ServiceAccount token is mounted "
364
+ f"and KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT env vars are set. "
365
+ f"Set MCP_K8S_PROVIDER=in-cluster for in-cluster deployments."
366
+ )
367
+
346
368
  api_config = client.Configuration()
347
369
 
348
370
  if context:
@@ -543,7 +565,8 @@ _BASE_EXPORTS = [
543
565
  if _HAS_PROVIDER:
544
566
  __all__ = _BASE_EXPORTS + [
545
567
  "KubernetesProvider", "ProviderConfig", "ProviderType",
546
- "UnknownContextError", "get_provider", "validate_context",
568
+ "ProviderError", "UnknownContextError",
569
+ "get_provider", "validate_context",
547
570
  ]
548
571
  else:
549
572
  __all__ = _BASE_EXPORTS
@@ -23,15 +23,10 @@ import logging
23
23
  import asyncio
24
24
  import os
25
25
  import platform
26
- import signal
27
- from pathlib import Path
28
26
  from typing import List, Optional, Any, Dict
29
27
 
30
- # Import k8s_config early to patch kubernetes config for in-cluster support
31
- # This must be done before any tools are imported
32
28
  import kubectl_mcp_tool.k8s_config # noqa: F401
33
29
 
34
- # Import safety mode for operation control
35
30
  from kubectl_mcp_tool.safety import (
36
31
  SafetyMode,
37
32
  set_safety_mode,
@@ -39,7 +34,6 @@ from kubectl_mcp_tool.safety import (
39
34
  get_mode_info,
40
35
  )
41
36
 
42
- # Import observability for metrics and tracing
43
37
  from kubectl_mcp_tool.observability import (
44
38
  get_stats_collector,
45
39
  get_metrics,
@@ -52,7 +46,6 @@ from kubectl_mcp_tool.observability import (
52
46
  record_tool_error_metric,
53
47
  )
54
48
 
55
- # Import config loader
56
49
  from kubectl_mcp_tool.config import (
57
50
  load_config,
58
51
  get_config,
@@ -60,12 +53,13 @@ from kubectl_mcp_tool.config import (
60
53
  setup_sighup_handler,
61
54
  )
62
55
 
63
- # Import custom prompts
64
56
  from kubectl_mcp_tool.prompts import (
65
57
  load_prompts_from_config,
66
58
  get_builtin_prompts,
67
59
  )
68
60
 
61
+ from kubectl_mcp_tool import __version__
62
+
69
63
  from kubectl_mcp_tool.tools import (
70
64
  register_helm_tools,
71
65
  register_pod_tools,
@@ -134,23 +128,13 @@ for handler in logging.root.handlers[:]:
134
128
  if isinstance(handler, logging.StreamHandler) and handler.stream == sys.stdout:
135
129
  logging.root.removeHandler(handler)
136
130
 
137
- # FastMCP 3 from gofastmcp.com (standalone package)
138
- # To revert to official SDK: from mcp.server.fastmcp import FastMCP
139
131
  try:
140
132
  from fastmcp import FastMCP
141
- except ImportError:
142
- logger.error("FastMCP not found. Installing...")
143
- import subprocess
144
- try:
145
- subprocess.check_call(
146
- [sys.executable, "-m", "pip", "install", "fastmcp>=3.0.0b1"],
147
- stdout=subprocess.DEVNULL,
148
- stderr=subprocess.DEVNULL
149
- )
150
- from fastmcp import FastMCP
151
- except Exception as e:
152
- logger.error(f"Failed to install FastMCP: {e}")
153
- raise
133
+ except ImportError as err:
134
+ raise ImportError(
135
+ "FastMCP is required but not installed. "
136
+ "Install with: pip install 'fastmcp>=3.0.0b1'"
137
+ ) from err
154
138
 
155
139
 
156
140
  class MCPServer:
@@ -622,7 +606,7 @@ class MCPServer:
622
606
  },
623
607
  "serverInfo": {
624
608
  "name": self.name,
625
- "version": "1.2.0"
609
+ "version": __version__
626
610
  }
627
611
  }
628
612
  elif method == "tools/list":
@@ -3,7 +3,6 @@ import logging
3
3
  from enum import Enum
4
4
  from typing import Any, Dict, List, Optional, Tuple
5
5
  from dataclasses import dataclass, field
6
- from functools import lru_cache
7
6
 
8
7
  logger = logging.getLogger("mcp-server")
9
8
 
@@ -150,8 +149,37 @@ class KubernetesProvider:
150
149
  raise ProviderError(f"Failed to load context '{self.config.context}': {e}")
151
150
 
152
151
  def _initialize_kubeconfig(self):
153
- """Initialize for multi-cluster kubeconfig provider."""
152
+ """Initialize for multi-cluster kubeconfig provider.
153
+
154
+ Falls back to in-cluster config if kubeconfig file is not found.
155
+ """
154
156
  from kubernetes import config
157
+ from kubernetes.config.config_exception import ConfigException
158
+
159
+ kubeconfig_exists = os.path.exists(self.config.kubeconfig_path)
160
+
161
+ if not kubeconfig_exists:
162
+ try:
163
+ config.load_incluster_config()
164
+ self._in_cluster = True
165
+ self._active_context = "in-cluster"
166
+ self._contexts_cache = []
167
+ logger.info(
168
+ f"Kubeconfig not found at {self.config.kubeconfig_path}, "
169
+ f"using in-cluster config"
170
+ )
171
+ return
172
+ except ConfigException:
173
+ raise ProviderError(
174
+ f"No Kubernetes configuration found. "
175
+ f"Kubeconfig not found at '{self.config.kubeconfig_path}' and "
176
+ f"in-cluster config is not available. "
177
+ f"Ensure a valid kubeconfig exists, or if running inside a "
178
+ f"Kubernetes pod, verify the ServiceAccount token is mounted "
179
+ f"and KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT env vars "
180
+ f"are set. For in-cluster deployments, set "
181
+ f"MCP_K8S_PROVIDER=in-cluster."
182
+ )
155
183
 
156
184
  try:
157
185
  contexts, active = config.list_kube_config_contexts(
@@ -5,8 +5,7 @@ Provides read-only and disable-destructive modes to prevent accidental cluster m
5
5
  """
6
6
 
7
7
  from enum import Enum
8
- from functools import wraps
9
- from typing import Any, Callable, Dict, Set
8
+ from typing import Any, Dict, Set
10
9
  import logging
11
10
 
12
11
  logger = logging.getLogger("mcp-server")
@@ -86,57 +85,6 @@ def set_safety_mode(mode: SafetyMode) -> None:
86
85
  logger.info(f"Safety mode set to: {mode.value}")
87
86
 
88
87
 
89
- def is_operation_allowed(operation_name: str) -> tuple[bool, str]:
90
- """
91
- Check if an operation is allowed under the current safety mode.
92
-
93
- Returns:
94
- Tuple of (allowed: bool, reason: str)
95
- """
96
- mode = get_safety_mode()
97
-
98
- if mode == SafetyMode.NORMAL:
99
- return True, ""
100
-
101
- if mode == SafetyMode.READ_ONLY:
102
- if operation_name in WRITE_OPERATIONS or operation_name in DESTRUCTIVE_OPERATIONS:
103
- return False, f"Operation '{operation_name}' blocked: read-only mode is enabled"
104
-
105
- if mode == SafetyMode.DISABLE_DESTRUCTIVE:
106
- if operation_name in DESTRUCTIVE_OPERATIONS:
107
- return False, f"Operation '{operation_name}' blocked: destructive operations are disabled"
108
-
109
- return True, ""
110
-
111
-
112
- def check_safety_mode(func: Callable) -> Callable:
113
- """
114
- Decorator to check safety mode before executing a tool function.
115
-
116
- Usage:
117
- @check_safety_mode
118
- def delete_pod(...):
119
- ...
120
- """
121
- @wraps(func)
122
- def wrapper(*args, **kwargs) -> Dict[str, Any]:
123
- operation_name = func.__name__
124
- allowed, reason = is_operation_allowed(operation_name)
125
-
126
- if not allowed:
127
- logger.warning(f"Blocked operation: {operation_name} (mode: {get_safety_mode().value})")
128
- return {
129
- "success": False,
130
- "error": reason,
131
- "blocked_by": get_safety_mode().value,
132
- "operation": operation_name
133
- }
134
-
135
- return func(*args, **kwargs)
136
-
137
- return wrapper
138
-
139
-
140
88
  def get_mode_info() -> Dict[str, Any]:
141
89
  """Get information about the current safety mode."""
142
90
  mode = get_safety_mode()
@@ -0,0 +1,75 @@
1
+ import subprocess
2
+ from functools import lru_cache
3
+ from typing import Any, Dict, List, Optional
4
+
5
+
6
+ @lru_cache(maxsize=16)
7
+ def cli_available(binary: str) -> bool:
8
+ """Check if a CLI binary is available (cached)."""
9
+ try:
10
+ result = subprocess.run(
11
+ [binary, "version"],
12
+ capture_output=True,
13
+ timeout=10
14
+ )
15
+ return result.returncode == 0
16
+ except Exception:
17
+ return False
18
+
19
+
20
+ @lru_cache(maxsize=16)
21
+ def get_cli_version(binary: str) -> Optional[str]:
22
+ """Get CLI binary version string (cached)."""
23
+ try:
24
+ result = subprocess.run(
25
+ [binary, "version"],
26
+ capture_output=True,
27
+ text=True,
28
+ timeout=10
29
+ )
30
+ if result.returncode == 0:
31
+ return result.stdout.strip()
32
+ return None
33
+ except Exception:
34
+ return None
35
+
36
+
37
+ def run_cli(
38
+ binary: str,
39
+ args: List[str],
40
+ timeout: int = 300,
41
+ capture_output: bool = True
42
+ ) -> Dict[str, Any]:
43
+ """Run a CLI command and return a standardized result dict.
44
+
45
+ Args:
46
+ binary: CLI binary name (e.g., "kind", "vcluster", "helm")
47
+ args: Command arguments (without the binary prefix)
48
+ timeout: Command timeout in seconds
49
+ capture_output: Whether to capture stdout/stderr
50
+
51
+ Returns:
52
+ Dict with success status and output/error
53
+ """
54
+ cmd = [binary, *args]
55
+
56
+ try:
57
+ result = subprocess.run(
58
+ cmd,
59
+ capture_output=capture_output,
60
+ text=True,
61
+ timeout=timeout
62
+ )
63
+ if result.returncode == 0:
64
+ output = result.stdout.strip() if capture_output else ""
65
+ return {"success": True, "output": output}
66
+ return {
67
+ "success": False,
68
+ "error": result.stderr.strip() if capture_output else f"Command failed with exit code {result.returncode}"
69
+ }
70
+ except subprocess.TimeoutExpired:
71
+ return {"success": False, "error": f"Command timed out after {timeout} seconds"}
72
+ except FileNotFoundError:
73
+ return {"success": False, "error": f"{binary} CLI not available"}
74
+ except Exception as e:
75
+ return {"success": False, "error": str(e)}
@@ -23,18 +23,12 @@ from kubectl_mcp_tool.k8s_config import (
23
23
  disable_kubeconfig_watch,
24
24
  is_stateless_mode,
25
25
  set_stateless_mode,
26
+ _get_kubectl_context_args,
26
27
  )
27
28
 
28
29
  logger = logging.getLogger("mcp-server")
29
30
 
30
31
 
31
- def _get_kubectl_context_args(context: str = "") -> List[str]:
32
- """Get kubectl context arguments."""
33
- if context:
34
- return ["--context", context]
35
- return []
36
-
37
-
38
32
  # DNS-1123 subdomain regex for node name validation
39
33
  _DNS_1123_PATTERN = re.compile(r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
40
34
 
@@ -60,7 +54,7 @@ def _validate_node_name(name: str) -> tuple:
60
54
  return True, None
61
55
 
62
56
 
63
- def register_cluster_tools(server, non_destructive: bool):
57
+ def register_cluster_tools(server: "FastMCP", non_destructive: bool):
64
58
  """Register cluster and context management tools."""
65
59
 
66
60
  @server.tool(
@@ -7,19 +7,13 @@ from mcp.types import ToolAnnotations
7
7
  from ..k8s_config import (
8
8
  get_k8s_client,
9
9
  get_apiextensions_client,
10
+ _get_kubectl_context_args,
10
11
  )
11
12
 
12
13
  logger = logging.getLogger("mcp-server")
13
14
 
14
15
 
15
- def _get_kubectl_context_args(context: str) -> List[str]:
16
- """Get kubectl context arguments if context is specified."""
17
- if context:
18
- return ["--context", context]
19
- return []
20
-
21
-
22
- def register_core_tools(server, non_destructive: bool):
16
+ def register_core_tools(server: "FastMCP", non_destructive: bool):
23
17
  """Register core Kubernetes resource tools."""
24
18
 
25
19
  @server.tool(
@@ -6,18 +6,11 @@ from typing import Any, Dict, List, Optional
6
6
 
7
7
  from mcp.types import ToolAnnotations
8
8
 
9
- from ..k8s_config import get_k8s_client, get_apps_client
9
+ from ..k8s_config import get_k8s_client, get_apps_client, _get_kubectl_context_args
10
10
 
11
11
  logger = logging.getLogger("mcp-server")
12
12
 
13
13
 
14
- def _get_kubectl_context_args(context: str) -> List[str]:
15
- """Get kubectl context arguments if context is specified."""
16
- if context:
17
- return ["--context", context]
18
- return []
19
-
20
-
21
14
  def _parse_cpu(cpu_str: str) -> int:
22
15
  """Parse CPU string to millicores."""
23
16
  try:
@@ -28,7 +21,7 @@ def _parse_cpu(cpu_str: str) -> int:
28
21
  return int(cpu_str[:-1]) // 1000000
29
22
  else:
30
23
  return int(float(cpu_str) * 1000)
31
- except:
24
+ except (ValueError, TypeError):
32
25
  return 0
33
26
 
34
27
 
@@ -50,7 +43,7 @@ def _parse_memory(mem_str: str) -> int:
50
43
  return int(mem_str[:-1]) * 1000000000
51
44
  else:
52
45
  return int(mem_str)
53
- except:
46
+ except (ValueError, TypeError):
54
47
  return 0
55
48
 
56
49
 
@@ -61,11 +54,11 @@ def _calculate_available(hard: str, used: str) -> str:
61
54
  used_num = int(re.sub(r'[^\d]', '', str(used)) or 0)
62
55
  suffix = re.sub(r'[\d]', '', str(hard))
63
56
  return f"{max(0, hard_num - used_num)}{suffix}"
64
- except:
57
+ except (ValueError, TypeError):
65
58
  return "N/A"
66
59
 
67
60
 
68
- def register_cost_tools(server, non_destructive: bool):
61
+ def register_cost_tools(server: "FastMCP", non_destructive: bool):
69
62
  """Register cost and resource optimization tools."""
70
63
 
71
64
  @server.tool(
@@ -10,19 +10,13 @@ from ..k8s_config import (
10
10
  get_autoscaling_client,
11
11
  get_policy_client,
12
12
  _load_config_for_context,
13
+ _get_kubectl_context_args,
13
14
  )
14
15
 
15
16
  logger = logging.getLogger("mcp-server")
16
17
 
17
18
 
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
-
25
- def register_deployment_tools(server, non_destructive: bool):
19
+ def register_deployment_tools(server: "FastMCP", non_destructive: bool):
26
20
  """Register deployment and workload management tools."""
27
21
 
28
22
  @server.tool(
@@ -4,19 +4,12 @@ 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
7
+ from ..k8s_config import get_k8s_client, get_apps_client, _get_kubectl_context_args
8
8
 
9
9
  logger = logging.getLogger("mcp-server")
10
10
 
11
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
-
19
- def register_diagnostics_tools(server, non_destructive: bool):
12
+ def register_diagnostics_tools(server: "FastMCP", non_destructive: bool):
20
13
  """Register diagnostic and troubleshooting tools.
21
14
 
22
15
  Note: Pod-specific diagnostic tools (diagnose_pod_crash, detect_pending_pods,
@@ -8,6 +8,8 @@ from typing import Any, Callable, Dict, List, Optional
8
8
  import yaml
9
9
  from mcp.types import ToolAnnotations
10
10
 
11
+ from kubectl_mcp_tool.k8s_config import _get_kubectl_context_args
12
+
11
13
  logger = logging.getLogger("mcp-server")
12
14
 
13
15
 
@@ -18,15 +20,43 @@ def _get_helm_context_args(context: str) -> List[str]:
18
20
  return []
19
21
 
20
22
 
21
- def _get_kubectl_context_args(context: str) -> List[str]:
22
- """Get kubectl context arguments if context is specified."""
23
- if context:
24
- return ["--context", context]
25
- return []
23
+ def _add_helm_repo(repo: str, chart: str) -> tuple:
24
+ """Add a Helm repo and return the updated chart reference.
25
+
26
+ Args:
27
+ repo: Repository in format 'repo_name=repo_url'
28
+ chart: Original chart reference
29
+
30
+ Returns:
31
+ Tuple of (success: bool, chart_or_error: str)
32
+ """
33
+ repo_parts = repo.split("=", 1)
34
+ if len(repo_parts) != 2:
35
+ return False, "Repository format should be 'repo_name=repo_url'"
36
+
37
+ repo_name, repo_url = (p.strip() for p in repo_parts)
38
+ if not repo_name or not repo_url:
39
+ return False, "Repository format should be 'repo_name=repo_url'"
40
+ try:
41
+ subprocess.check_output(
42
+ ["helm", "repo", "add", repo_name, repo_url],
43
+ stderr=subprocess.PIPE, text=True
44
+ )
45
+ subprocess.check_output(
46
+ ["helm", "repo", "update"],
47
+ stderr=subprocess.PIPE, text=True
48
+ )
49
+ except subprocess.CalledProcessError as e:
50
+ error_msg = e.stderr if hasattr(e, 'stderr') else str(e)
51
+ return False, f"Failed to add Helm repo: {error_msg}"
52
+
53
+ if '/' not in chart:
54
+ chart = f"{repo_name}/{chart}"
55
+ return True, chart
26
56
 
27
57
 
28
58
  def register_helm_tools(
29
- server,
59
+ server: "FastMCP",
30
60
  non_destructive: bool,
31
61
  check_helm_fn: Callable[[], bool]
32
62
  ):
@@ -69,25 +99,10 @@ def register_helm_tools(
69
99
 
70
100
  try:
71
101
  if repo:
72
- try:
73
- repo_parts = repo.split('=')
74
- if len(repo_parts) != 2:
75
- return {"success": False, "error": "Repository format should be 'repo_name=repo_url'"}
76
-
77
- repo_name, repo_url = repo_parts
78
- repo_add_cmd = ["helm", "repo", "add", repo_name, repo_url]
79
- logger.debug(f"Running command: {' '.join(repo_add_cmd)}")
80
- subprocess.check_output(repo_add_cmd, stderr=subprocess.PIPE, text=True)
81
-
82
- repo_update_cmd = ["helm", "repo", "update"]
83
- logger.debug(f"Running command: {' '.join(repo_update_cmd)}")
84
- subprocess.check_output(repo_update_cmd, stderr=subprocess.PIPE, text=True)
85
-
86
- if '/' not in chart:
87
- chart = f"{repo_name}/{chart}"
88
- except subprocess.CalledProcessError as e:
89
- logger.error(f"Error adding Helm repo: {e.stderr if hasattr(e, 'stderr') else str(e)}")
90
- return {"success": False, "error": f"Failed to add Helm repo: {e.stderr if hasattr(e, 'stderr') else str(e)}"}
102
+ success, result = _add_helm_repo(repo, chart)
103
+ if not success:
104
+ return {"success": False, "error": result}
105
+ chart = result
91
106
 
92
107
  cmd = ["helm"] + _get_helm_context_args(context) + ["install", name, chart, "-n", namespace]
93
108
 
@@ -162,25 +177,10 @@ def register_helm_tools(
162
177
 
163
178
  try:
164
179
  if repo:
165
- try:
166
- repo_parts = repo.split('=')
167
- if len(repo_parts) != 2:
168
- return {"success": False, "error": "Repository format should be 'repo_name=repo_url'"}
169
-
170
- repo_name, repo_url = repo_parts
171
- repo_add_cmd = ["helm", "repo", "add", repo_name, repo_url]
172
- logger.debug(f"Running command: {' '.join(repo_add_cmd)}")
173
- subprocess.check_output(repo_add_cmd, stderr=subprocess.PIPE, text=True)
174
-
175
- repo_update_cmd = ["helm", "repo", "update"]
176
- logger.debug(f"Running command: {' '.join(repo_update_cmd)}")
177
- subprocess.check_output(repo_update_cmd, stderr=subprocess.PIPE, text=True)
178
-
179
- if '/' not in chart:
180
- chart = f"{repo_name}/{chart}"
181
- except subprocess.CalledProcessError as e:
182
- logger.error(f"Error adding Helm repo: {e.stderr if hasattr(e, 'stderr') else str(e)}")
183
- return {"success": False, "error": f"Failed to add Helm repo: {e.stderr if hasattr(e, 'stderr') else str(e)}"}
180
+ success, result = _add_helm_repo(repo, chart)
181
+ if not success:
182
+ return {"success": False, "error": result}
183
+ chart = result
184
184
 
185
185
  cmd = ["helm"] + _get_helm_context_args(context) + ["upgrade", name, chart, "-n", namespace]
186
186