kubectl-mcp-server 1.14.0__py3-none-any.whl → 1.16.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. kubectl_mcp_server-1.16.0.dist-info/METADATA +1047 -0
  2. kubectl_mcp_server-1.16.0.dist-info/RECORD +61 -0
  3. kubectl_mcp_tool/__init__.py +1 -1
  4. kubectl_mcp_tool/crd_detector.py +247 -0
  5. kubectl_mcp_tool/k8s_config.py +304 -63
  6. kubectl_mcp_tool/mcp_server.py +27 -0
  7. kubectl_mcp_tool/tools/__init__.py +20 -0
  8. kubectl_mcp_tool/tools/backup.py +881 -0
  9. kubectl_mcp_tool/tools/capi.py +727 -0
  10. kubectl_mcp_tool/tools/certs.py +709 -0
  11. kubectl_mcp_tool/tools/cilium.py +582 -0
  12. kubectl_mcp_tool/tools/cluster.py +395 -121
  13. kubectl_mcp_tool/tools/core.py +157 -60
  14. kubectl_mcp_tool/tools/cost.py +97 -41
  15. kubectl_mcp_tool/tools/deployments.py +173 -56
  16. kubectl_mcp_tool/tools/diagnostics.py +40 -13
  17. kubectl_mcp_tool/tools/gitops.py +552 -0
  18. kubectl_mcp_tool/tools/helm.py +133 -46
  19. kubectl_mcp_tool/tools/keda.py +464 -0
  20. kubectl_mcp_tool/tools/kiali.py +652 -0
  21. kubectl_mcp_tool/tools/kubevirt.py +803 -0
  22. kubectl_mcp_tool/tools/networking.py +106 -32
  23. kubectl_mcp_tool/tools/operations.py +176 -50
  24. kubectl_mcp_tool/tools/pods.py +162 -50
  25. kubectl_mcp_tool/tools/policy.py +554 -0
  26. kubectl_mcp_tool/tools/rollouts.py +790 -0
  27. kubectl_mcp_tool/tools/security.py +89 -36
  28. kubectl_mcp_tool/tools/storage.py +35 -16
  29. tests/test_browser.py +2 -2
  30. tests/test_ecosystem.py +331 -0
  31. tests/test_tools.py +73 -10
  32. kubectl_mcp_server-1.14.0.dist-info/METADATA +0 -780
  33. kubectl_mcp_server-1.14.0.dist-info/RECORD +0 -49
  34. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/WHEEL +0 -0
  35. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/entry_points.txt +0 -0
  36. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/licenses/LICENSE +0 -0
  37. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,12 @@ from typing import Any, Dict, List, Optional
3
3
 
4
4
  from mcp.types import ToolAnnotations
5
5
 
6
+ from ..k8s_config import (
7
+ get_k8s_client,
8
+ get_rbac_client,
9
+ get_networking_client,
10
+ )
11
+
6
12
  logger = logging.getLogger("mcp-server")
7
13
 
8
14
 
@@ -15,12 +21,18 @@ def register_security_tools(server, non_destructive: bool):
15
21
  readOnlyHint=True,
16
22
  ),
17
23
  )
18
- def get_rbac_roles(namespace: Optional[str] = None) -> Dict[str, Any]:
19
- """Get RBAC Roles in a namespace or cluster-wide."""
24
+ def get_rbac_roles(
25
+ namespace: Optional[str] = None,
26
+ context: str = ""
27
+ ) -> Dict[str, Any]:
28
+ """Get RBAC Roles in a namespace or cluster-wide.
29
+
30
+ Args:
31
+ namespace: Namespace to list roles from (all namespaces if not specified)
32
+ context: Kubernetes context to use (uses current context if not specified)
33
+ """
20
34
  try:
21
- from kubernetes import client, config
22
- config.load_kube_config()
23
- rbac = client.RbacAuthorizationV1Api()
35
+ rbac = get_rbac_client(context)
24
36
 
25
37
  if namespace:
26
38
  roles = rbac.list_namespaced_role(namespace)
@@ -29,6 +41,7 @@ def register_security_tools(server, non_destructive: bool):
29
41
 
30
42
  return {
31
43
  "success": True,
44
+ "context": context or "current",
32
45
  "roles": [
33
46
  {
34
47
  "name": role.metadata.name,
@@ -55,16 +68,19 @@ def register_security_tools(server, non_destructive: bool):
55
68
  readOnlyHint=True,
56
69
  ),
57
70
  )
58
- def get_cluster_roles() -> Dict[str, Any]:
59
- """Get ClusterRoles in the cluster."""
71
+ def get_cluster_roles(context: str = "") -> Dict[str, Any]:
72
+ """Get ClusterRoles in the cluster.
73
+
74
+ Args:
75
+ context: Kubernetes context to use (uses current context if not specified)
76
+ """
60
77
  try:
61
- from kubernetes import client, config
62
- config.load_kube_config()
63
- rbac = client.RbacAuthorizationV1Api()
78
+ rbac = get_rbac_client(context)
64
79
  roles = rbac.list_cluster_role()
65
80
 
66
81
  return {
67
82
  "success": True,
83
+ "context": context or "current",
68
84
  "clusterRoles": [
69
85
  {
70
86
  "name": role.metadata.name,
@@ -90,12 +106,18 @@ def register_security_tools(server, non_destructive: bool):
90
106
  readOnlyHint=True,
91
107
  ),
92
108
  )
93
- def analyze_pod_security(namespace: Optional[str] = None) -> Dict[str, Any]:
94
- """Analyze pod security configurations."""
109
+ def analyze_pod_security(
110
+ namespace: Optional[str] = None,
111
+ context: str = ""
112
+ ) -> Dict[str, Any]:
113
+ """Analyze pod security configurations.
114
+
115
+ Args:
116
+ namespace: Namespace to analyze pods in (all namespaces if not specified)
117
+ context: Kubernetes context to use (uses current context if not specified)
118
+ """
95
119
  try:
96
- from kubernetes import client, config
97
- config.load_kube_config()
98
- v1 = client.CoreV1Api()
120
+ v1 = get_k8s_client(context)
99
121
 
100
122
  if namespace:
101
123
  pods = v1.list_namespaced_pod(namespace)
@@ -133,6 +155,7 @@ def register_security_tools(server, non_destructive: bool):
133
155
 
134
156
  return {
135
157
  "success": True,
158
+ "context": context or "current",
136
159
  "totalPods": len(pods.items),
137
160
  "podsWithIssues": len(issues),
138
161
  "issues": issues[:50]
@@ -147,13 +170,19 @@ def register_security_tools(server, non_destructive: bool):
147
170
  readOnlyHint=True,
148
171
  ),
149
172
  )
150
- def analyze_network_policies(namespace: Optional[str] = None) -> Dict[str, Any]:
151
- """Analyze network policies in the cluster."""
173
+ def analyze_network_policies(
174
+ namespace: Optional[str] = None,
175
+ context: str = ""
176
+ ) -> Dict[str, Any]:
177
+ """Analyze network policies in the cluster.
178
+
179
+ Args:
180
+ namespace: Namespace to analyze policies in (all namespaces if not specified)
181
+ context: Kubernetes context to use (uses current context if not specified)
182
+ """
152
183
  try:
153
- from kubernetes import client, config
154
- config.load_kube_config()
155
- networking = client.NetworkingV1Api()
156
- v1 = client.CoreV1Api()
184
+ networking = get_networking_client(context)
185
+ v1 = get_k8s_client(context)
157
186
 
158
187
  if namespace:
159
188
  policies = networking.list_namespaced_network_policy(namespace)
@@ -174,6 +203,7 @@ def register_security_tools(server, non_destructive: bool):
174
203
 
175
204
  return {
176
205
  "success": True,
206
+ "context": context or "current",
177
207
  "totalPolicies": len(policies.items),
178
208
  "protectedNamespaces": list(protected_namespaces),
179
209
  "unprotectedNamespaces": unprotected,
@@ -197,12 +227,20 @@ def register_security_tools(server, non_destructive: bool):
197
227
  readOnlyHint=True,
198
228
  ),
199
229
  )
200
- def audit_rbac_permissions(namespace: Optional[str] = None, subject: Optional[str] = None) -> Dict[str, Any]:
201
- """Audit RBAC permissions for subjects."""
230
+ def audit_rbac_permissions(
231
+ namespace: Optional[str] = None,
232
+ subject: Optional[str] = None,
233
+ context: str = ""
234
+ ) -> Dict[str, Any]:
235
+ """Audit RBAC permissions for subjects.
236
+
237
+ Args:
238
+ namespace: Namespace to audit (cluster-wide if not specified)
239
+ subject: Filter by subject name
240
+ context: Kubernetes context to use (uses current context if not specified)
241
+ """
202
242
  try:
203
- from kubernetes import client, config
204
- config.load_kube_config()
205
- rbac = client.RbacAuthorizationV1Api()
243
+ rbac = get_rbac_client(context)
206
244
 
207
245
  cluster_bindings = rbac.list_cluster_role_binding()
208
246
  if namespace:
@@ -238,6 +276,7 @@ def register_security_tools(server, non_destructive: bool):
238
276
 
239
277
  return {
240
278
  "success": True,
279
+ "context": context or "current",
241
280
  "permissions": permissions[:100]
242
281
  }
243
282
  except Exception as e:
@@ -250,12 +289,18 @@ def register_security_tools(server, non_destructive: bool):
250
289
  readOnlyHint=True,
251
290
  ),
252
291
  )
253
- def check_secrets_security(namespace: Optional[str] = None) -> Dict[str, Any]:
254
- """Check security posture of secrets."""
292
+ def check_secrets_security(
293
+ namespace: Optional[str] = None,
294
+ context: str = ""
295
+ ) -> Dict[str, Any]:
296
+ """Check security posture of secrets.
297
+
298
+ Args:
299
+ namespace: Namespace to check secrets in (all namespaces if not specified)
300
+ context: Kubernetes context to use (uses current context if not specified)
301
+ """
255
302
  try:
256
- from kubernetes import client, config
257
- config.load_kube_config()
258
- v1 = client.CoreV1Api()
303
+ v1 = get_k8s_client(context)
259
304
 
260
305
  if namespace:
261
306
  secrets = v1.list_namespaced_secret(namespace)
@@ -282,6 +327,7 @@ def register_security_tools(server, non_destructive: bool):
282
327
 
283
328
  return {
284
329
  "success": True,
330
+ "context": context or "current",
285
331
  "totalSecrets": len(secrets.items),
286
332
  "secretsWithIssues": len(findings),
287
333
  "findings": findings[:50]
@@ -296,12 +342,18 @@ def register_security_tools(server, non_destructive: bool):
296
342
  readOnlyHint=True,
297
343
  ),
298
344
  )
299
- def get_pod_security_info(namespace: Optional[str] = None) -> Dict[str, Any]:
300
- """Get Pod Security Standards information for namespaces."""
345
+ def get_pod_security_info(
346
+ namespace: Optional[str] = None,
347
+ context: str = ""
348
+ ) -> Dict[str, Any]:
349
+ """Get Pod Security Standards information for namespaces.
350
+
351
+ Args:
352
+ namespace: Namespace to check (all namespaces if not specified)
353
+ context: Kubernetes context to use (uses current context if not specified)
354
+ """
301
355
  try:
302
- from kubernetes import client, config
303
- config.load_kube_config()
304
- v1 = client.CoreV1Api()
356
+ v1 = get_k8s_client(context)
305
357
 
306
358
  if namespace:
307
359
  namespaces = [v1.read_namespace(namespace)]
@@ -325,6 +377,7 @@ def register_security_tools(server, non_destructive: bool):
325
377
 
326
378
  return {
327
379
  "success": True,
380
+ "context": context or "current",
328
381
  "note": "Pod Security Policies are deprecated. Using Pod Security Standards (PSS) labels.",
329
382
  "namespacesWithPSS": result
330
383
  }
@@ -1,8 +1,10 @@
1
1
  import logging
2
- from typing import Any, Dict, Optional
2
+ from typing import Any, Dict, List, Optional
3
3
 
4
4
  from mcp.types import ToolAnnotations
5
5
 
6
+ from ..k8s_config import get_k8s_client, get_storage_client
7
+
6
8
  logger = logging.getLogger("mcp-server")
7
9
 
8
10
 
@@ -15,12 +17,18 @@ def register_storage_tools(server, non_destructive: bool):
15
17
  readOnlyHint=True,
16
18
  ),
17
19
  )
18
- def get_persistent_volumes(name: Optional[str] = None) -> Dict[str, Any]:
19
- """Get Persistent Volumes in the cluster."""
20
+ def get_persistent_volumes(
21
+ name: Optional[str] = None,
22
+ context: str = ""
23
+ ) -> Dict[str, Any]:
24
+ """Get Persistent Volumes in the cluster.
25
+
26
+ Args:
27
+ name: Specific PV name to get (all PVs if not specified)
28
+ context: Kubernetes context to use (uses current context if not specified)
29
+ """
20
30
  try:
21
- from kubernetes import client, config
22
- config.load_kube_config()
23
- v1 = client.CoreV1Api()
31
+ v1 = get_k8s_client(context)
24
32
 
25
33
  if name:
26
34
  pv = v1.read_persistent_volume(name)
@@ -39,6 +47,7 @@ def register_storage_tools(server, non_destructive: bool):
39
47
 
40
48
  return {
41
49
  "success": True,
50
+ "context": context or "current",
42
51
  "persistentVolumes": [
43
52
  {
44
53
  "name": pv.metadata.name,
@@ -66,12 +75,18 @@ def register_storage_tools(server, non_destructive: bool):
66
75
  readOnlyHint=True,
67
76
  ),
68
77
  )
69
- def get_pvcs(namespace: Optional[str] = None) -> Dict[str, Any]:
70
- """Get Persistent Volume Claims in a namespace or cluster-wide."""
78
+ def get_pvcs(
79
+ namespace: Optional[str] = None,
80
+ context: str = ""
81
+ ) -> Dict[str, Any]:
82
+ """Get Persistent Volume Claims in a namespace or cluster-wide.
83
+
84
+ Args:
85
+ namespace: Namespace to list PVCs from (all namespaces if not specified)
86
+ context: Kubernetes context to use (uses current context if not specified)
87
+ """
71
88
  try:
72
- from kubernetes import client, config
73
- config.load_kube_config()
74
- v1 = client.CoreV1Api()
89
+ v1 = get_k8s_client(context)
75
90
 
76
91
  if namespace:
77
92
  pvcs = v1.list_namespaced_persistent_volume_claim(namespace)
@@ -80,6 +95,7 @@ def register_storage_tools(server, non_destructive: bool):
80
95
 
81
96
  return {
82
97
  "success": True,
98
+ "context": context or "current",
83
99
  "pvcs": [
84
100
  {
85
101
  "name": pvc.metadata.name,
@@ -103,17 +119,20 @@ def register_storage_tools(server, non_destructive: bool):
103
119
  readOnlyHint=True,
104
120
  ),
105
121
  )
106
- def get_storage_classes() -> Dict[str, Any]:
107
- """Get Storage Classes in the cluster."""
122
+ def get_storage_classes(context: str = "") -> Dict[str, Any]:
123
+ """Get Storage Classes in the cluster.
124
+
125
+ Args:
126
+ context: Kubernetes context to use (uses current context if not specified)
127
+ """
108
128
  try:
109
- from kubernetes import client, config
110
- config.load_kube_config()
111
- storage = client.StorageV1Api()
129
+ storage = get_storage_client(context)
112
130
 
113
131
  scs = storage.list_storage_class()
114
132
 
115
133
  return {
116
134
  "success": True,
135
+ "context": context or "current",
117
136
  "storageClasses": [
118
137
  {
119
138
  "name": sc.metadata.name,
tests/test_browser.py CHANGED
@@ -501,11 +501,11 @@ class TestServerIntegration:
501
501
  tools = asyncio.run(server.server.list_tools())
502
502
  tool_names = [t.name for t in tools]
503
503
 
504
- # Should have browser tools (127 + 26 = 153)
504
+ # Should have browser tools (224 + 26 = 250)
505
505
  assert "browser_open" in tool_names
506
506
  assert "browser_screenshot" in tool_names
507
507
  assert "browser_connect_cdp" in tool_names # v0.7 tool
508
- assert len(tools) == 153, f"Expected 153 tools (127 + 26), got {len(tools)}"
508
+ assert len(tools) == 250, f"Expected 250 tools (224 + 26), got {len(tools)}"
509
509
 
510
510
 
511
511
  import asyncio
@@ -0,0 +1,331 @@
1
+ """
2
+ Unit tests for ecosystem tools (GitOps, Cert-Manager, Policy, Backup).
3
+
4
+ This module tests the CRD detector and all ecosystem toolsets.
5
+ """
6
+
7
+ import pytest
8
+ from unittest.mock import patch, MagicMock
9
+ import subprocess
10
+
11
+
12
+ class TestCRDDetector:
13
+ """Tests for the CRD auto-discovery framework."""
14
+
15
+ @pytest.mark.unit
16
+ def test_crd_detector_imports(self):
17
+ """Test that CRD detector module can be imported."""
18
+ from kubectl_mcp_tool.crd_detector import (
19
+ CRD_GROUPS,
20
+ detect_crds,
21
+ crd_exists,
22
+ get_enabled_toolsets,
23
+ get_crd_status_summary,
24
+ FeatureNotInstalledError,
25
+ require_crd,
26
+ require_any_crd,
27
+ )
28
+ assert CRD_GROUPS is not None
29
+ assert callable(detect_crds)
30
+ assert callable(crd_exists)
31
+ assert callable(get_enabled_toolsets)
32
+ assert callable(get_crd_status_summary)
33
+ assert issubclass(FeatureNotInstalledError, Exception)
34
+ assert callable(require_crd)
35
+ assert callable(require_any_crd)
36
+
37
+ @pytest.mark.unit
38
+ def test_crd_groups_structure(self):
39
+ """Test that CRD_GROUPS has expected structure."""
40
+ from kubectl_mcp_tool.crd_detector import CRD_GROUPS
41
+
42
+ expected_groups = ["flux", "argocd", "certmanager", "kyverno", "gatekeeper", "velero"]
43
+ for group in expected_groups:
44
+ assert group in CRD_GROUPS, f"Expected group '{group}' in CRD_GROUPS"
45
+ assert isinstance(CRD_GROUPS[group], list), f"CRD_GROUPS['{group}'] should be a list"
46
+ assert len(CRD_GROUPS[group]) > 0, f"CRD_GROUPS['{group}'] should not be empty"
47
+
48
+ @pytest.mark.unit
49
+ def test_detect_crds_with_mocked_kubectl(self):
50
+ """Test CRD detection with mocked kubectl."""
51
+ from kubectl_mcp_tool.crd_detector import detect_crds, _crd_cache
52
+
53
+ # Clear cache before test
54
+ _crd_cache.clear()
55
+
56
+ mock_output = """NAME CREATED AT
57
+ applications.argoproj.io 2024-01-01T00:00:00Z
58
+ certificates.cert-manager.io 2024-01-01T00:00:00Z
59
+ """
60
+ with patch("subprocess.run") as mock_run:
61
+ mock_run.return_value = MagicMock(
62
+ returncode=0,
63
+ stdout=mock_output
64
+ )
65
+ result = detect_crds(force_refresh=True)
66
+
67
+ assert isinstance(result, dict)
68
+ assert "argocd" in result
69
+ assert "certmanager" in result
70
+
71
+ @pytest.mark.unit
72
+ def test_detect_crds_handles_kubectl_failure(self):
73
+ """Test CRD detection handles kubectl failure gracefully."""
74
+ from kubectl_mcp_tool.crd_detector import detect_crds, _crd_cache
75
+
76
+ _crd_cache.clear()
77
+
78
+ with patch("subprocess.run") as mock_run:
79
+ mock_run.side_effect = subprocess.SubprocessError("Command failed")
80
+ result = detect_crds(force_refresh=True)
81
+
82
+ assert isinstance(result, dict)
83
+ # All groups should be False when kubectl fails
84
+ for value in result.values():
85
+ assert value is False
86
+
87
+ @pytest.mark.unit
88
+ def test_feature_not_installed_error(self):
89
+ """Test FeatureNotInstalledError exception."""
90
+ from kubectl_mcp_tool.crd_detector import FeatureNotInstalledError
91
+
92
+ error = FeatureNotInstalledError("velero", ["backups.velero.io"])
93
+ assert "velero" in str(error)
94
+ assert "backups.velero.io" in str(error)
95
+ assert error.toolset == "velero"
96
+ assert "backups.velero.io" in error.required_crds
97
+
98
+ @pytest.mark.unit
99
+ def test_get_crd_status_summary(self):
100
+ """Test CRD status summary generation."""
101
+ from kubectl_mcp_tool.crd_detector import get_crd_status_summary, _crd_cache
102
+
103
+ _crd_cache.clear()
104
+
105
+ with patch("subprocess.run") as mock_run:
106
+ mock_run.return_value = MagicMock(
107
+ returncode=0,
108
+ stdout="applications.argoproj.io 2024-01-01T00:00:00Z\n"
109
+ )
110
+ summary = get_crd_status_summary()
111
+
112
+ # Summary returns a dict with crd_groups and enabled_toolsets
113
+ assert isinstance(summary, dict)
114
+ assert "crd_groups" in summary
115
+ assert "enabled_toolsets" in summary
116
+
117
+
118
+ class TestGitOpsTools:
119
+ """Tests for GitOps toolset (Flux and ArgoCD)."""
120
+
121
+ @pytest.mark.unit
122
+ def test_gitops_tools_import(self):
123
+ """Test that GitOps tools can be imported."""
124
+ from kubectl_mcp_tool.tools.gitops import register_gitops_tools
125
+ assert callable(register_gitops_tools)
126
+
127
+ @pytest.mark.unit
128
+ def test_gitops_tools_register(self, mock_all_kubernetes_apis):
129
+ """Test that GitOps tools register correctly."""
130
+ from kubectl_mcp_tool.mcp_server import MCPServer
131
+ import asyncio
132
+
133
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
134
+ server = MCPServer(name="test")
135
+
136
+ async def get_tools():
137
+ return await server.server.list_tools()
138
+
139
+ tools = asyncio.run(get_tools())
140
+ tool_names = {t.name for t in tools}
141
+
142
+ gitops_tools = [
143
+ "gitops_apps_list_tool", "gitops_app_get_tool", "gitops_app_sync_tool",
144
+ "gitops_app_status_tool", "gitops_sources_list_tool", "gitops_source_get_tool",
145
+ "gitops_detect_engine_tool"
146
+ ]
147
+ for tool in gitops_tools:
148
+ assert tool in tool_names, f"GitOps tool '{tool}' not registered"
149
+
150
+ @pytest.mark.unit
151
+ def test_gitops_non_destructive_mode(self, mock_all_kubernetes_apis):
152
+ """Test that GitOps sync is blocked in non-destructive mode."""
153
+ from kubectl_mcp_tool.mcp_server import MCPServer
154
+
155
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
156
+ server = MCPServer(name="test", non_destructive=True)
157
+
158
+ # Server should initialize with non_destructive=True
159
+ assert server.non_destructive is True
160
+
161
+
162
+ class TestCertManagerTools:
163
+ """Tests for Cert-Manager toolset."""
164
+
165
+ @pytest.mark.unit
166
+ def test_certs_tools_import(self):
167
+ """Test that Cert-Manager tools can be imported."""
168
+ from kubectl_mcp_tool.tools.certs import register_certs_tools
169
+ assert callable(register_certs_tools)
170
+
171
+ @pytest.mark.unit
172
+ def test_certs_tools_register(self, mock_all_kubernetes_apis):
173
+ """Test that Cert-Manager tools register correctly."""
174
+ from kubectl_mcp_tool.mcp_server import MCPServer
175
+ import asyncio
176
+
177
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
178
+ server = MCPServer(name="test")
179
+
180
+ async def get_tools():
181
+ return await server.server.list_tools()
182
+
183
+ tools = asyncio.run(get_tools())
184
+ tool_names = {t.name for t in tools}
185
+
186
+ certs_tools = [
187
+ "certs_list_tool", "certs_get_tool", "certs_issuers_list_tool", "certs_issuer_get_tool",
188
+ "certs_renew_tool", "certs_status_explain_tool", "certs_challenges_list_tool",
189
+ "certs_requests_list_tool", "certs_detect_tool"
190
+ ]
191
+ for tool in certs_tools:
192
+ assert tool in tool_names, f"Cert-Manager tool '{tool}' not registered"
193
+
194
+
195
+ class TestPolicyTools:
196
+ """Tests for Policy toolset (Kyverno and Gatekeeper)."""
197
+
198
+ @pytest.mark.unit
199
+ def test_policy_tools_import(self):
200
+ """Test that Policy tools can be imported."""
201
+ from kubectl_mcp_tool.tools.policy import register_policy_tools
202
+ assert callable(register_policy_tools)
203
+
204
+ @pytest.mark.unit
205
+ def test_policy_tools_register(self, mock_all_kubernetes_apis):
206
+ """Test that Policy tools register correctly."""
207
+ from kubectl_mcp_tool.mcp_server import MCPServer
208
+ import asyncio
209
+
210
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
211
+ server = MCPServer(name="test")
212
+
213
+ async def get_tools():
214
+ return await server.server.list_tools()
215
+
216
+ tools = asyncio.run(get_tools())
217
+ tool_names = {t.name for t in tools}
218
+
219
+ policy_tools = [
220
+ "policy_list_tool", "policy_get_tool", "policy_violations_list_tool",
221
+ "policy_explain_denial_tool", "policy_audit_tool", "policy_detect_tool"
222
+ ]
223
+ for tool in policy_tools:
224
+ assert tool in tool_names, f"Policy tool '{tool}' not registered"
225
+
226
+
227
+ class TestBackupTools:
228
+ """Tests for Backup toolset (Velero)."""
229
+
230
+ @pytest.mark.unit
231
+ def test_backup_tools_import(self):
232
+ """Test that Backup tools can be imported."""
233
+ from kubectl_mcp_tool.tools.backup import register_backup_tools
234
+ assert callable(register_backup_tools)
235
+
236
+ @pytest.mark.unit
237
+ def test_backup_tools_register(self, mock_all_kubernetes_apis):
238
+ """Test that Backup tools register correctly."""
239
+ from kubectl_mcp_tool.mcp_server import MCPServer
240
+ import asyncio
241
+
242
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
243
+ server = MCPServer(name="test")
244
+
245
+ async def get_tools():
246
+ return await server.server.list_tools()
247
+
248
+ tools = asyncio.run(get_tools())
249
+ tool_names = {t.name for t in tools}
250
+
251
+ backup_tools = [
252
+ "backup_list_tool", "backup_get_tool", "backup_create_tool", "backup_delete_tool",
253
+ "restore_list_tool", "restore_create_tool", "restore_get_tool",
254
+ "backup_locations_list_tool", "backup_schedules_list_tool",
255
+ "backup_schedule_create_tool", "backup_detect_tool"
256
+ ]
257
+ for tool in backup_tools:
258
+ assert tool in tool_names, f"Backup tool '{tool}' not registered"
259
+
260
+ @pytest.mark.unit
261
+ def test_backup_non_destructive_mode(self, mock_all_kubernetes_apis):
262
+ """Test that backup operations are blocked in non-destructive mode."""
263
+ from kubectl_mcp_tool.mcp_server import MCPServer
264
+
265
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
266
+ server = MCPServer(name="test", non_destructive=True)
267
+
268
+ # Server should initialize with non_destructive=True
269
+ assert server.non_destructive is True
270
+
271
+
272
+ class TestEcosystemToolsIntegration:
273
+ """Integration tests for ecosystem tools."""
274
+
275
+ @pytest.mark.unit
276
+ def test_all_ecosystem_tools_have_descriptions(self, mock_all_kubernetes_apis):
277
+ """Test that all ecosystem tools have descriptions."""
278
+ from kubectl_mcp_tool.mcp_server import MCPServer
279
+ import asyncio
280
+
281
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
282
+ server = MCPServer(name="test")
283
+
284
+ async def get_tools():
285
+ return await server.server.list_tools()
286
+
287
+ tools = asyncio.run(get_tools())
288
+
289
+ ecosystem_prefixes = ["gitops_", "certs_", "policy_", "backup_", "restore_"]
290
+ ecosystem_tools = [t for t in tools if any(t.name.startswith(p) for p in ecosystem_prefixes)]
291
+
292
+ tools_without_description = [
293
+ t.name for t in ecosystem_tools
294
+ if not t.description or len(t.description.strip()) == 0
295
+ ]
296
+ assert not tools_without_description, f"Ecosystem tools without descriptions: {tools_without_description}"
297
+
298
+ @pytest.mark.unit
299
+ def test_ecosystem_tool_count(self, mock_all_kubernetes_apis):
300
+ """Test that correct number of ecosystem tools are registered."""
301
+ from kubectl_mcp_tool.mcp_server import MCPServer
302
+ import asyncio
303
+
304
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
305
+ server = MCPServer(name="test")
306
+
307
+ async def get_tools():
308
+ return await server.server.list_tools()
309
+
310
+ tools = asyncio.run(get_tools())
311
+
312
+ # Filter ecosystem tools, but exclude backup_resource which is in operations.py
313
+ ecosystem_tool_names = [
314
+ "gitops_apps_list_tool", "gitops_app_get_tool", "gitops_app_sync_tool",
315
+ "gitops_app_status_tool", "gitops_sources_list_tool", "gitops_source_get_tool",
316
+ "gitops_detect_engine_tool",
317
+ "certs_list_tool", "certs_get_tool", "certs_issuers_list_tool", "certs_issuer_get_tool",
318
+ "certs_renew_tool", "certs_status_explain_tool", "certs_challenges_list_tool",
319
+ "certs_requests_list_tool", "certs_detect_tool",
320
+ "policy_list_tool", "policy_get_tool", "policy_violations_list_tool",
321
+ "policy_explain_denial_tool", "policy_audit_tool", "policy_detect_tool",
322
+ "backup_list_tool", "backup_get_tool", "backup_create_tool", "backup_delete_tool",
323
+ "restore_list_tool", "restore_create_tool", "restore_get_tool",
324
+ "backup_locations_list_tool", "backup_schedules_list_tool",
325
+ "backup_schedule_create_tool", "backup_detect_tool"
326
+ ]
327
+ tool_names = {t.name for t in tools}
328
+ ecosystem_tools = [name for name in ecosystem_tool_names if name in tool_names]
329
+
330
+ # 7 GitOps + 9 Certs + 6 Policy + 11 Backup = 33 ecosystem tools
331
+ assert len(ecosystem_tools) == 33, f"Expected 33 ecosystem tools, got {len(ecosystem_tools)}"