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.
- kubectl_mcp_server-1.12.0.dist-info/METADATA +711 -0
- kubectl_mcp_server-1.12.0.dist-info/RECORD +45 -0
- kubectl_mcp_server-1.12.0.dist-info/WHEEL +5 -0
- kubectl_mcp_server-1.12.0.dist-info/entry_points.txt +3 -0
- kubectl_mcp_server-1.12.0.dist-info/licenses/LICENSE +21 -0
- kubectl_mcp_server-1.12.0.dist-info/top_level.txt +2 -0
- kubectl_mcp_tool/__init__.py +21 -0
- kubectl_mcp_tool/__main__.py +46 -0
- kubectl_mcp_tool/auth/__init__.py +13 -0
- kubectl_mcp_tool/auth/config.py +71 -0
- kubectl_mcp_tool/auth/scopes.py +148 -0
- kubectl_mcp_tool/auth/verifier.py +82 -0
- kubectl_mcp_tool/cli/__init__.py +9 -0
- kubectl_mcp_tool/cli/__main__.py +10 -0
- kubectl_mcp_tool/cli/cli.py +111 -0
- kubectl_mcp_tool/diagnostics.py +355 -0
- kubectl_mcp_tool/k8s_config.py +289 -0
- kubectl_mcp_tool/mcp_server.py +530 -0
- kubectl_mcp_tool/prompts/__init__.py +5 -0
- kubectl_mcp_tool/prompts/prompts.py +823 -0
- kubectl_mcp_tool/resources/__init__.py +5 -0
- kubectl_mcp_tool/resources/resources.py +305 -0
- kubectl_mcp_tool/tools/__init__.py +28 -0
- kubectl_mcp_tool/tools/browser.py +371 -0
- kubectl_mcp_tool/tools/cluster.py +315 -0
- kubectl_mcp_tool/tools/core.py +421 -0
- kubectl_mcp_tool/tools/cost.py +680 -0
- kubectl_mcp_tool/tools/deployments.py +381 -0
- kubectl_mcp_tool/tools/diagnostics.py +174 -0
- kubectl_mcp_tool/tools/helm.py +1561 -0
- kubectl_mcp_tool/tools/networking.py +296 -0
- kubectl_mcp_tool/tools/operations.py +501 -0
- kubectl_mcp_tool/tools/pods.py +582 -0
- kubectl_mcp_tool/tools/security.py +333 -0
- kubectl_mcp_tool/tools/storage.py +133 -0
- kubectl_mcp_tool/utils/__init__.py +17 -0
- kubectl_mcp_tool/utils/helpers.py +80 -0
- tests/__init__.py +9 -0
- tests/conftest.py +379 -0
- tests/test_auth.py +256 -0
- tests/test_browser.py +349 -0
- tests/test_prompts.py +536 -0
- tests/test_resources.py +343 -0
- tests/test_server.py +384 -0
- 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
|