kubectl-mcp-server 1.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. kubectl_mcp_server-1.12.0.dist-info/METADATA +711 -0
  2. kubectl_mcp_server-1.12.0.dist-info/RECORD +45 -0
  3. kubectl_mcp_server-1.12.0.dist-info/WHEEL +5 -0
  4. kubectl_mcp_server-1.12.0.dist-info/entry_points.txt +3 -0
  5. kubectl_mcp_server-1.12.0.dist-info/licenses/LICENSE +21 -0
  6. kubectl_mcp_server-1.12.0.dist-info/top_level.txt +2 -0
  7. kubectl_mcp_tool/__init__.py +21 -0
  8. kubectl_mcp_tool/__main__.py +46 -0
  9. kubectl_mcp_tool/auth/__init__.py +13 -0
  10. kubectl_mcp_tool/auth/config.py +71 -0
  11. kubectl_mcp_tool/auth/scopes.py +148 -0
  12. kubectl_mcp_tool/auth/verifier.py +82 -0
  13. kubectl_mcp_tool/cli/__init__.py +9 -0
  14. kubectl_mcp_tool/cli/__main__.py +10 -0
  15. kubectl_mcp_tool/cli/cli.py +111 -0
  16. kubectl_mcp_tool/diagnostics.py +355 -0
  17. kubectl_mcp_tool/k8s_config.py +289 -0
  18. kubectl_mcp_tool/mcp_server.py +530 -0
  19. kubectl_mcp_tool/prompts/__init__.py +5 -0
  20. kubectl_mcp_tool/prompts/prompts.py +823 -0
  21. kubectl_mcp_tool/resources/__init__.py +5 -0
  22. kubectl_mcp_tool/resources/resources.py +305 -0
  23. kubectl_mcp_tool/tools/__init__.py +28 -0
  24. kubectl_mcp_tool/tools/browser.py +371 -0
  25. kubectl_mcp_tool/tools/cluster.py +315 -0
  26. kubectl_mcp_tool/tools/core.py +421 -0
  27. kubectl_mcp_tool/tools/cost.py +680 -0
  28. kubectl_mcp_tool/tools/deployments.py +381 -0
  29. kubectl_mcp_tool/tools/diagnostics.py +174 -0
  30. kubectl_mcp_tool/tools/helm.py +1561 -0
  31. kubectl_mcp_tool/tools/networking.py +296 -0
  32. kubectl_mcp_tool/tools/operations.py +501 -0
  33. kubectl_mcp_tool/tools/pods.py +582 -0
  34. kubectl_mcp_tool/tools/security.py +333 -0
  35. kubectl_mcp_tool/tools/storage.py +133 -0
  36. kubectl_mcp_tool/utils/__init__.py +17 -0
  37. kubectl_mcp_tool/utils/helpers.py +80 -0
  38. tests/__init__.py +9 -0
  39. tests/conftest.py +379 -0
  40. tests/test_auth.py +256 -0
  41. tests/test_browser.py +349 -0
  42. tests/test_prompts.py +536 -0
  43. tests/test_resources.py +343 -0
  44. tests/test_server.py +384 -0
  45. tests/test_tools.py +659 -0
@@ -0,0 +1,343 @@
1
+ """
2
+ Unit tests for MCP Resources in kubectl-mcp-server.
3
+
4
+ This module tests all FastMCP 3 resources including:
5
+ - kubeconfig:// resources
6
+ - namespace:// resources
7
+ - cluster:// resources
8
+ - manifest:// resources
9
+ """
10
+
11
+ import pytest
12
+ import json
13
+ from unittest.mock import patch, MagicMock
14
+ from datetime import datetime
15
+
16
+
17
+ class TestKubeconfigResources:
18
+ """Tests for kubeconfig:// resources."""
19
+
20
+ @pytest.mark.unit
21
+ def test_get_kubeconfig_contexts(self, mock_kube_contexts):
22
+ """Test listing all kubectl contexts."""
23
+ from kubectl_mcp_tool.mcp_server import MCPServer
24
+
25
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
26
+ with patch("kubernetes.config.load_kube_config"):
27
+ server = MCPServer(name="test")
28
+
29
+ # Verify the resource is registered
30
+ resources = server.server._resource_manager._resources if hasattr(server.server, '_resource_manager') else {}
31
+ assert server is not None
32
+
33
+ @pytest.mark.unit
34
+ def test_get_current_context(self, mock_kube_contexts):
35
+ """Test getting the current active context."""
36
+ from kubectl_mcp_tool.mcp_server import MCPServer
37
+
38
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
39
+ with patch("kubernetes.config.load_kube_config"):
40
+ server = MCPServer(name="test")
41
+
42
+ assert server is not None
43
+
44
+ @pytest.mark.unit
45
+ def test_context_returns_json(self, mock_kube_contexts):
46
+ """Test that context resource returns valid JSON."""
47
+ contexts = [
48
+ {"name": "minikube", "context": {"cluster": "minikube", "user": "minikube", "namespace": "default"}}
49
+ ]
50
+ active = contexts[0]
51
+
52
+ with patch("kubernetes.config.list_kube_config_contexts", return_value=(contexts, active)):
53
+ result = {
54
+ "active_context": active.get("name"),
55
+ "contexts": contexts
56
+ }
57
+ json_str = json.dumps(result)
58
+ parsed = json.loads(json_str)
59
+ assert "active_context" in parsed
60
+ assert "contexts" in parsed
61
+
62
+
63
+ class TestNamespaceResources:
64
+ """Tests for namespace:// resources."""
65
+
66
+ @pytest.mark.unit
67
+ def test_get_current_namespace(self, mock_kube_contexts):
68
+ """Test getting the current namespace."""
69
+ from kubectl_mcp_tool.mcp_server import MCPServer
70
+
71
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
72
+ with patch("kubernetes.config.load_kube_config"):
73
+ server = MCPServer(name="test")
74
+
75
+ assert server is not None
76
+
77
+ @pytest.mark.unit
78
+ def test_list_all_namespaces(self, mock_all_kubernetes_apis):
79
+ """Test listing all namespaces."""
80
+ from kubectl_mcp_tool.mcp_server import MCPServer
81
+
82
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
83
+ server = MCPServer(name="test")
84
+
85
+ assert server is not None
86
+
87
+ @pytest.mark.unit
88
+ def test_namespace_includes_metadata(self, mock_all_kubernetes_apis):
89
+ """Test that namespace list includes metadata."""
90
+ mock_ns = MagicMock()
91
+ mock_ns.metadata.name = "test-namespace"
92
+ mock_ns.metadata.labels = {"env": "test"}
93
+ mock_ns.metadata.creation_timestamp = datetime.now()
94
+ mock_ns.status.phase = "Active"
95
+
96
+ result = {
97
+ "name": mock_ns.metadata.name,
98
+ "status": mock_ns.status.phase,
99
+ "labels": mock_ns.metadata.labels
100
+ }
101
+
102
+ assert result["name"] == "test-namespace"
103
+ assert result["status"] == "Active"
104
+ assert result["labels"]["env"] == "test"
105
+
106
+
107
+ class TestClusterResources:
108
+ """Tests for cluster:// resources."""
109
+
110
+ @pytest.mark.unit
111
+ def test_get_cluster_info(self, mock_all_kubernetes_apis):
112
+ """Test getting cluster info."""
113
+ from kubectl_mcp_tool.mcp_server import MCPServer
114
+
115
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
116
+ server = MCPServer(name="test")
117
+
118
+ assert server is not None
119
+
120
+ @pytest.mark.unit
121
+ def test_get_cluster_nodes(self, mock_all_kubernetes_apis):
122
+ """Test getting cluster nodes."""
123
+ from kubectl_mcp_tool.mcp_server import MCPServer
124
+
125
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
126
+ server = MCPServer(name="test")
127
+
128
+ assert server is not None
129
+
130
+ @pytest.mark.unit
131
+ def test_get_cluster_version(self, mock_all_kubernetes_apis, mock_version_api):
132
+ """Test getting cluster version."""
133
+ from kubectl_mcp_tool.mcp_server import MCPServer
134
+
135
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
136
+ server = MCPServer(name="test")
137
+
138
+ version_info = mock_version_api.get_code()
139
+ assert version_info.git_version == "v1.28.0"
140
+ assert version_info.major == "1"
141
+ assert version_info.minor == "28"
142
+
143
+ @pytest.mark.unit
144
+ def test_get_api_resources(self, mock_kubectl_subprocess):
145
+ """Test getting API resources."""
146
+ from kubectl_mcp_tool.mcp_server import MCPServer
147
+
148
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
149
+ with patch("kubernetes.config.load_kube_config"):
150
+ server = MCPServer(name="test")
151
+
152
+ assert server is not None
153
+
154
+ @pytest.mark.unit
155
+ def test_cluster_info_includes_node_count(self, mock_all_kubernetes_apis):
156
+ """Test that cluster info includes node count."""
157
+ mock_nodes = [MagicMock(), MagicMock()]
158
+ for node in mock_nodes:
159
+ node.status.conditions = [MagicMock(type="Ready", status="True")]
160
+
161
+ result = {
162
+ "nodes": {
163
+ "count": len(mock_nodes),
164
+ "ready": sum(1 for n in mock_nodes if any(
165
+ c.type == "Ready" and c.status == "True"
166
+ for c in n.status.conditions
167
+ ))
168
+ }
169
+ }
170
+
171
+ assert result["nodes"]["count"] == 2
172
+ assert result["nodes"]["ready"] == 2
173
+
174
+
175
+ class TestManifestResources:
176
+ """Tests for manifest:// resources."""
177
+
178
+ @pytest.mark.unit
179
+ def test_get_deployment_manifest(self, mock_all_kubernetes_apis):
180
+ """Test getting deployment manifest."""
181
+ from kubectl_mcp_tool.mcp_server import MCPServer
182
+
183
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
184
+ server = MCPServer(name="test")
185
+
186
+ assert server is not None
187
+
188
+ @pytest.mark.unit
189
+ def test_get_service_manifest(self, mock_all_kubernetes_apis):
190
+ """Test getting service manifest."""
191
+ from kubectl_mcp_tool.mcp_server import MCPServer
192
+
193
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
194
+ server = MCPServer(name="test")
195
+
196
+ assert server is not None
197
+
198
+ @pytest.mark.unit
199
+ def test_get_configmap_manifest(self, mock_all_kubernetes_apis):
200
+ """Test getting ConfigMap manifest."""
201
+ from kubectl_mcp_tool.mcp_server import MCPServer
202
+
203
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
204
+ server = MCPServer(name="test")
205
+
206
+ assert server is not None
207
+
208
+ @pytest.mark.unit
209
+ def test_get_pod_manifest(self, mock_all_kubernetes_apis):
210
+ """Test getting pod manifest."""
211
+ from kubectl_mcp_tool.mcp_server import MCPServer
212
+
213
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
214
+ server = MCPServer(name="test")
215
+
216
+ assert server is not None
217
+
218
+ @pytest.mark.unit
219
+ def test_get_secret_manifest_masks_data(self, mock_all_kubernetes_apis):
220
+ """Test that secret manifest masks sensitive data."""
221
+ mock_manifest = {
222
+ "apiVersion": "v1",
223
+ "kind": "Secret",
224
+ "metadata": {"name": "test-secret", "namespace": "default"},
225
+ "data": {"password": "c2VjcmV0", "api-key": "YXBpa2V5"}
226
+ }
227
+
228
+ # Simulate masking
229
+ if "data" in mock_manifest and mock_manifest["data"]:
230
+ mock_manifest["data"] = {k: "[MASKED]" for k in mock_manifest["data"].keys()}
231
+
232
+ assert mock_manifest["data"]["password"] == "[MASKED]"
233
+ assert mock_manifest["data"]["api-key"] == "[MASKED]"
234
+
235
+ @pytest.mark.unit
236
+ def test_get_ingress_manifest(self, mock_all_kubernetes_apis):
237
+ """Test getting ingress manifest."""
238
+ from kubectl_mcp_tool.mcp_server import MCPServer
239
+
240
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
241
+ server = MCPServer(name="test")
242
+
243
+ assert server is not None
244
+
245
+ @pytest.mark.unit
246
+ def test_manifest_returns_yaml(self):
247
+ """Test that manifest resources return valid YAML."""
248
+ import yaml
249
+
250
+ manifest = {
251
+ "apiVersion": "apps/v1",
252
+ "kind": "Deployment",
253
+ "metadata": {"name": "test", "namespace": "default"},
254
+ "spec": {"replicas": 3}
255
+ }
256
+
257
+ yaml_str = yaml.dump(manifest, default_flow_style=False)
258
+ parsed = yaml.safe_load(yaml_str)
259
+
260
+ assert parsed["apiVersion"] == "apps/v1"
261
+ assert parsed["kind"] == "Deployment"
262
+ assert parsed["spec"]["replicas"] == 3
263
+
264
+ @pytest.mark.unit
265
+ def test_manifest_handles_not_found(self):
266
+ """Test that manifest resources handle not found errors."""
267
+ error_result = "# Error: Deployment 'not-found' not found in namespace 'default'"
268
+ assert "Error" in error_result
269
+
270
+
271
+ class TestResourceErrorHandling:
272
+ """Tests for error handling in resources."""
273
+
274
+ @pytest.mark.unit
275
+ def test_handles_kube_config_error(self):
276
+ """Test handling of kubeconfig errors."""
277
+ with patch("kubernetes.config.list_kube_config_contexts") as mock_contexts:
278
+ mock_contexts.side_effect = Exception("Config not found")
279
+ result = json.dumps({"error": "Config not found"})
280
+ parsed = json.loads(result)
281
+ assert "error" in parsed
282
+
283
+ @pytest.mark.unit
284
+ def test_handles_api_connection_error(self):
285
+ """Test handling of API connection errors."""
286
+ with patch("kubernetes.config.load_kube_config"):
287
+ with patch("kubernetes.client.CoreV1Api") as mock_api:
288
+ mock_api.return_value.list_namespace.side_effect = Exception("Connection refused")
289
+ result = json.dumps({"error": "Connection refused"})
290
+ parsed = json.loads(result)
291
+ assert "error" in parsed
292
+
293
+ @pytest.mark.unit
294
+ def test_handles_permission_error(self):
295
+ """Test handling of permission errors."""
296
+ result = json.dumps({"error": "Forbidden: User does not have permission"})
297
+ parsed = json.loads(result)
298
+ assert "error" in parsed
299
+ assert "Forbidden" in parsed["error"]
300
+
301
+
302
+ class TestResourceRegistration:
303
+ """Tests for resource registration."""
304
+
305
+ @pytest.mark.unit
306
+ def test_all_resources_registered(self):
307
+ """Test that all expected resources are registered."""
308
+ from kubectl_mcp_tool.mcp_server import MCPServer
309
+
310
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
311
+ with patch("kubernetes.config.load_kube_config"):
312
+ server = MCPServer(name="test")
313
+
314
+ # Server should initialize with resources
315
+ assert server is not None
316
+ assert hasattr(server, 'server')
317
+
318
+ @pytest.mark.unit
319
+ def test_resource_uris_are_valid(self):
320
+ """Test that resource URIs follow correct format."""
321
+ valid_uris = [
322
+ "kubeconfig://contexts",
323
+ "kubeconfig://current-context",
324
+ "namespace://current",
325
+ "namespace://list",
326
+ "cluster://info",
327
+ "cluster://nodes",
328
+ "cluster://version",
329
+ "cluster://api-resources",
330
+ "manifest://deployments/{namespace}/{name}",
331
+ "manifest://services/{namespace}/{name}",
332
+ "manifest://configmaps/{namespace}/{name}",
333
+ "manifest://pods/{namespace}/{name}",
334
+ "manifest://secrets/{namespace}/{name}",
335
+ "manifest://ingresses/{namespace}/{name}",
336
+ ]
337
+
338
+ for uri in valid_uris:
339
+ # URI should have a scheme and path
340
+ assert "://" in uri
341
+ scheme, path = uri.split("://", 1)
342
+ assert scheme in ["kubeconfig", "namespace", "cluster", "manifest"]
343
+ assert len(path) > 0