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
tests/test_browser.py ADDED
@@ -0,0 +1,349 @@
1
+ """Unit tests for browser automation tools (optional module)."""
2
+
3
+ import pytest
4
+ import json
5
+ import os
6
+ from unittest.mock import patch, MagicMock
7
+
8
+
9
+ class TestBrowserAvailability:
10
+ """Tests for browser module availability detection."""
11
+
12
+ @pytest.mark.unit
13
+ def test_browser_disabled_by_default(self):
14
+ """Browser tools should be disabled by default."""
15
+ with patch.dict(os.environ, {}, clear=True):
16
+ # Need to reload module to pick up env changes
17
+ import importlib
18
+ import kubectl_mcp_tool.tools.browser as browser_module
19
+ importlib.reload(browser_module)
20
+ assert browser_module.BROWSER_ENABLED is False
21
+
22
+ @pytest.mark.unit
23
+ def test_browser_enabled_with_env_var(self):
24
+ """Browser tools should be enabled when MCP_BROWSER_ENABLED=true."""
25
+ with patch.dict(os.environ, {"MCP_BROWSER_ENABLED": "true"}):
26
+ import importlib
27
+ import kubectl_mcp_tool.tools.browser as browser_module
28
+ importlib.reload(browser_module)
29
+ assert browser_module.BROWSER_ENABLED is True
30
+
31
+ @pytest.mark.unit
32
+ def test_is_browser_available_disabled(self):
33
+ """is_browser_available returns False when disabled."""
34
+ with patch.dict(os.environ, {"MCP_BROWSER_ENABLED": "false"}):
35
+ import importlib
36
+ import kubectl_mcp_tool.tools.browser as browser_module
37
+ importlib.reload(browser_module)
38
+ assert browser_module.is_browser_available() is False
39
+
40
+ @pytest.mark.unit
41
+ def test_is_browser_available_enabled_no_binary(self):
42
+ """is_browser_available returns False when enabled but binary missing."""
43
+ with patch.dict(os.environ, {"MCP_BROWSER_ENABLED": "true"}):
44
+ with patch("shutil.which", return_value=None):
45
+ import importlib
46
+ import kubectl_mcp_tool.tools.browser as browser_module
47
+ importlib.reload(browser_module)
48
+ # Force re-check since BROWSER_AVAILABLE is set at import time
49
+ browser_module.BROWSER_AVAILABLE = False
50
+ assert browser_module.is_browser_available() is False
51
+
52
+
53
+ class TestBrowserCommands:
54
+ """Tests for browser command execution."""
55
+
56
+ @pytest.mark.unit
57
+ def test_run_browser_success(self):
58
+ """Test successful browser command execution."""
59
+ from kubectl_mcp_tool.tools.browser import _run_browser
60
+
61
+ mock_result = MagicMock()
62
+ mock_result.returncode = 0
63
+ mock_result.stdout = "Success output"
64
+ mock_result.stderr = ""
65
+
66
+ with patch("subprocess.run", return_value=mock_result):
67
+ result = _run_browser(["open", "https://example.com"])
68
+ assert result["success"] is True
69
+ assert result["output"] == "Success output"
70
+
71
+ @pytest.mark.unit
72
+ def test_run_browser_failure(self):
73
+ """Test failed browser command execution."""
74
+ from kubectl_mcp_tool.tools.browser import _run_browser
75
+
76
+ mock_result = MagicMock()
77
+ mock_result.returncode = 1
78
+ mock_result.stdout = ""
79
+ mock_result.stderr = "Error: something went wrong"
80
+
81
+ with patch("subprocess.run", return_value=mock_result):
82
+ result = _run_browser(["open", "invalid"])
83
+ assert result["success"] is False
84
+ assert "Error" in result["error"]
85
+
86
+ @pytest.mark.unit
87
+ def test_run_browser_json_output(self):
88
+ """Test browser command with JSON output."""
89
+ from kubectl_mcp_tool.tools.browser import _run_browser
90
+
91
+ mock_result = MagicMock()
92
+ mock_result.returncode = 0
93
+ mock_result.stdout = '{"data": "test"}'
94
+ mock_result.stderr = ""
95
+
96
+ with patch("subprocess.run", return_value=mock_result):
97
+ result = _run_browser(["snapshot", "--json"])
98
+ assert result["success"] is True
99
+ assert result["data"] == {"data": "test"}
100
+
101
+ @pytest.mark.unit
102
+ def test_run_browser_timeout(self):
103
+ """Test browser command timeout handling."""
104
+ from kubectl_mcp_tool.tools.browser import _run_browser
105
+ import subprocess
106
+
107
+ with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 60)):
108
+ result = _run_browser(["open", "https://slow.com"], timeout=60)
109
+ assert result["success"] is False
110
+ assert "timed out" in result["error"]
111
+
112
+ @pytest.mark.unit
113
+ def test_run_browser_not_found(self):
114
+ """Test handling when agent-browser is not installed."""
115
+ from kubectl_mcp_tool.tools.browser import _run_browser
116
+
117
+ with patch("subprocess.run", side_effect=FileNotFoundError()):
118
+ result = _run_browser(["open", "https://example.com"])
119
+ assert result["success"] is False
120
+ assert "not found" in result["error"]
121
+
122
+
123
+ class TestBrowserToolFunctions:
124
+ """Tests for individual browser tool functions."""
125
+
126
+ @pytest.fixture
127
+ def mock_browser_run(self):
128
+ """Fixture to mock _run_browser."""
129
+ with patch("kubectl_mcp_tool.tools.browser._run_browser") as mock:
130
+ mock.return_value = {"success": True, "output": "OK"}
131
+ yield mock
132
+
133
+ @pytest.mark.unit
134
+ def test_browser_open(self, mock_browser_run):
135
+ """Test browser_open tool."""
136
+ from kubectl_mcp_tool.tools.browser import register_browser_tools
137
+ from fastmcp import FastMCP
138
+
139
+ server = FastMCP(name="test")
140
+ register_browser_tools(server, non_destructive=False)
141
+
142
+ # Verify tool was registered
143
+ import asyncio
144
+ tools = asyncio.run(server.list_tools())
145
+ tool_names = [t.name for t in tools]
146
+ assert "browser_open" in tool_names
147
+
148
+ @pytest.mark.unit
149
+ def test_browser_snapshot(self, mock_browser_run):
150
+ """Test browser_snapshot tool."""
151
+ from kubectl_mcp_tool.tools.browser import register_browser_tools
152
+ from fastmcp import FastMCP
153
+
154
+ server = FastMCP(name="test")
155
+ register_browser_tools(server, non_destructive=False)
156
+
157
+ tools = asyncio.run(server.list_tools())
158
+ tool_names = [t.name for t in tools]
159
+ assert "browser_snapshot" in tool_names
160
+
161
+ @pytest.mark.unit
162
+ def test_browser_screenshot(self, mock_browser_run):
163
+ """Test browser_screenshot tool."""
164
+ from kubectl_mcp_tool.tools.browser import register_browser_tools
165
+ from fastmcp import FastMCP
166
+
167
+ server = FastMCP(name="test")
168
+ register_browser_tools(server, non_destructive=False)
169
+
170
+ tools = asyncio.run(server.list_tools())
171
+ tool_names = [t.name for t in tools]
172
+ assert "browser_screenshot" in tool_names
173
+
174
+ @pytest.mark.unit
175
+ def test_all_19_browser_tools_registered(self):
176
+ """Verify all 19 browser tools are registered."""
177
+ from kubectl_mcp_tool.tools.browser import register_browser_tools
178
+ from fastmcp import FastMCP
179
+ import asyncio
180
+
181
+ server = FastMCP(name="test")
182
+ register_browser_tools(server, non_destructive=False)
183
+
184
+ tools = asyncio.run(server.list_tools())
185
+ assert len(tools) == 19, f"Expected 19 browser tools, got {len(tools)}"
186
+
187
+ expected_tools = [
188
+ "browser_open",
189
+ "browser_snapshot",
190
+ "browser_click",
191
+ "browser_fill",
192
+ "browser_screenshot",
193
+ "browser_get_text",
194
+ "browser_get_url",
195
+ "browser_wait",
196
+ "browser_close",
197
+ "browser_test_ingress",
198
+ "browser_screenshot_service",
199
+ "browser_screenshot_grafana",
200
+ "browser_screenshot_argocd",
201
+ "browser_health_check",
202
+ "browser_form_submit",
203
+ "browser_session_save",
204
+ "browser_session_load",
205
+ "browser_open_cloud_console",
206
+ "browser_pdf_export",
207
+ ]
208
+
209
+ tool_names = {t.name for t in tools}
210
+ missing = set(expected_tools) - tool_names
211
+ assert not missing, f"Missing browser tools: {missing}"
212
+
213
+
214
+ class TestK8sIntegration:
215
+ """Tests for Kubernetes-specific browser tools."""
216
+
217
+ @pytest.mark.unit
218
+ def test_get_ingress_url_not_found(self):
219
+ """Test _get_ingress_url when no ingress exists."""
220
+ from kubectl_mcp_tool.tools.browser import _get_ingress_url
221
+
222
+ with patch("kubernetes.config.load_kube_config"):
223
+ with patch("kubernetes.client.NetworkingV1Api") as mock_api:
224
+ mock_instance = MagicMock()
225
+ mock_instance.list_namespaced_ingress.return_value.items = []
226
+ mock_api.return_value = mock_instance
227
+
228
+ result = _get_ingress_url("my-service", "default")
229
+ assert result is None
230
+
231
+ @pytest.mark.unit
232
+ def test_get_service_url_loadbalancer(self):
233
+ """Test _get_service_url for LoadBalancer type."""
234
+ from kubectl_mcp_tool.tools.browser import _get_service_url
235
+
236
+ with patch("kubernetes.config.load_kube_config"):
237
+ with patch("kubernetes.client.CoreV1Api") as mock_api:
238
+ mock_svc = MagicMock()
239
+ mock_svc.spec.type = "LoadBalancer"
240
+ mock_svc.spec.ports = [MagicMock(port=80)]
241
+ mock_svc.status.load_balancer.ingress = [MagicMock(hostname="lb.example.com", ip=None)]
242
+
243
+ mock_instance = MagicMock()
244
+ mock_instance.read_namespaced_service.return_value = mock_svc
245
+ mock_api.return_value = mock_instance
246
+
247
+ result = _get_service_url("my-service", "default")
248
+ assert result == "http://lb.example.com:80"
249
+
250
+ @pytest.mark.unit
251
+ def test_get_service_url_nodeport(self):
252
+ """Test _get_service_url for NodePort type."""
253
+ from kubectl_mcp_tool.tools.browser import _get_service_url
254
+
255
+ with patch("kubernetes.config.load_kube_config"):
256
+ with patch("kubernetes.client.CoreV1Api") as mock_api:
257
+ mock_svc = MagicMock()
258
+ mock_svc.spec.type = "NodePort"
259
+ mock_svc.spec.ports = [MagicMock(node_port=30080)]
260
+
261
+ mock_instance = MagicMock()
262
+ mock_instance.read_namespaced_service.return_value = mock_svc
263
+ mock_api.return_value = mock_instance
264
+
265
+ result = _get_service_url("my-service", "default")
266
+ assert result == "http://localhost:30080"
267
+
268
+
269
+ class TestCloudConsole:
270
+ """Tests for cloud console URL generation."""
271
+
272
+ @pytest.mark.unit
273
+ def test_open_cloud_console_eks(self):
274
+ """Test EKS console URL generation."""
275
+ from kubectl_mcp_tool.tools.browser import register_browser_tools
276
+ from fastmcp import FastMCP
277
+ import asyncio
278
+
279
+ server = FastMCP(name="test")
280
+ register_browser_tools(server, non_destructive=False)
281
+
282
+ # The tool should generate correct EKS URL
283
+ # This is a basic registration test - actual URL testing would need integration tests
284
+
285
+ @pytest.mark.unit
286
+ def test_open_cloud_console_invalid_provider(self):
287
+ """Test handling of invalid cloud provider."""
288
+ from kubectl_mcp_tool.tools.browser import _run_browser
289
+
290
+ # Mock the browser command to simulate the tool behavior
291
+ with patch("kubectl_mcp_tool.tools.browser._run_browser") as mock:
292
+ mock.return_value = {"success": False, "error": "Unknown provider"}
293
+ result = mock(["open", "invalid-provider"])
294
+ assert result["success"] is False
295
+
296
+
297
+ class TestServerIntegration:
298
+ """Tests for browser tools integration with MCP server."""
299
+
300
+ @pytest.mark.unit
301
+ def test_browser_tools_not_registered_when_disabled(self):
302
+ """Verify browser tools are not registered when disabled."""
303
+ with patch.dict(os.environ, {"MCP_BROWSER_ENABLED": "false"}):
304
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
305
+ with patch("kubernetes.config.load_kube_config"):
306
+ # Force reload to pick up env change
307
+ import importlib
308
+ import kubectl_mcp_tool.tools.browser as browser_module
309
+ importlib.reload(browser_module)
310
+
311
+ from kubectl_mcp_tool.mcp_server import MCPServer
312
+ server = MCPServer(name="test")
313
+
314
+ import asyncio
315
+ tools = asyncio.run(server.server.list_tools())
316
+ tool_names = [t.name for t in tools]
317
+
318
+ # Should not have browser tools
319
+ assert "browser_open" not in tool_names
320
+ assert "browser_screenshot" not in tool_names
321
+
322
+ @pytest.mark.unit
323
+ def test_browser_tools_registered_when_enabled(self):
324
+ """Verify browser tools are registered when enabled and available."""
325
+ with patch.dict(os.environ, {"MCP_BROWSER_ENABLED": "true"}):
326
+ with patch("shutil.which", return_value="/usr/bin/agent-browser"):
327
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
328
+ with patch("kubernetes.config.load_kube_config"):
329
+ # Force reload to pick up env change
330
+ import importlib
331
+ import kubectl_mcp_tool.tools.browser as browser_module
332
+ importlib.reload(browser_module)
333
+ browser_module.BROWSER_AVAILABLE = True
334
+ browser_module.BROWSER_ENABLED = True
335
+
336
+ from kubectl_mcp_tool.mcp_server import MCPServer
337
+ server = MCPServer(name="test")
338
+
339
+ import asyncio
340
+ tools = asyncio.run(server.server.list_tools())
341
+ tool_names = [t.name for t in tools]
342
+
343
+ # Should have browser tools (121 + 19 = 140)
344
+ assert "browser_open" in tool_names
345
+ assert "browser_screenshot" in tool_names
346
+ assert len(tools) == 140, f"Expected 140 tools (121 + 19), got {len(tools)}"
347
+
348
+
349
+ import asyncio