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.
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/PKG-INFO +1 -1
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/PKG-INFO +1 -1
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/SOURCES.txt +1 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/__init__.py +1 -1
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/k8s_config.py +27 -4
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/mcp_server.py +8 -24
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/providers.py +30 -2
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/safety.py +1 -53
- kubectl_mcp_server-1.23.1/kubectl_mcp_tool/tools/_cli_utils.py +75 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/cluster.py +2 -8
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/core.py +2 -8
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/cost.py +5 -12
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/deployments.py +2 -8
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/diagnostics.py +2 -9
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/helm.py +44 -44
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/kind.py +4 -8
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/networking.py +18 -14
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/operations.py +39 -13
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/pods.py +9 -12
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/vind.py +2 -7
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/setup.py +1 -1
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_browser.py +2 -3
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_ecosystem.py +2 -4
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_mcp_integration.py +4 -26
- kubectl_mcp_server-1.23.1/tests/test_safety.py +98 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_server.py +4 -5
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_tools.py +48 -40
- kubectl_mcp_server-1.22.0/tests/test_safety.py +0 -218
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/LICENSE +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/README.md +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/dependency_links.txt +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/entry_points.txt +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/requires.txt +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/top_level.txt +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/__main__.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/auth/__init__.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/auth/config.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/auth/scopes.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/auth/verifier.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/cli/__init__.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/cli/__main__.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/cli/cli.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/cli/errors.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/cli/output.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/config/__init__.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/config/loader.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/config/schema.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/crd_detector.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/diagnostics.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/observability/__init__.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/observability/metrics.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/observability/stats.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/observability/tracing.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/prompts/__init__.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/prompts/builtin.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/prompts/custom.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/prompts/prompts.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/resources/__init__.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/resources/resources.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/__init__.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/backup.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/browser.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/capi.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/certs.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/cilium.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/gitops.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/keda.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/kiali.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/kubevirt.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/policy.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/rollouts.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/security.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/storage.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/ui.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/utils.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/utils/__init__.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/utils/helpers.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/setup.cfg +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/__init__.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/conftest.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_auth.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_cli.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_config.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_kind.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_observability.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_prompts.py +0 -0
- {kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/tests/test_resources.py +0 -0
- {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.
|
|
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
|
{kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kubectl-mcp-server
|
|
3
|
-
Version: 1.
|
|
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
|
{kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_server.egg-info/SOURCES.txt
RENAMED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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":
|
|
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
|
|
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
|
|
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(
|
{kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/deployments.py
RENAMED
|
@@ -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
|
|
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(
|
{kubectl_mcp_server-1.22.0 → kubectl_mcp_server-1.23.1}/kubectl_mcp_tool/tools/diagnostics.py
RENAMED
|
@@ -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
|
|
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
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|