kubectl-mcp-server 1.16.0__py3-none-any.whl → 1.18.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.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/METADATA +48 -1
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/RECORD +30 -15
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/cli/cli.py +83 -9
- kubectl_mcp_tool/cli/output.py +14 -0
- kubectl_mcp_tool/config/__init__.py +46 -0
- kubectl_mcp_tool/config/loader.py +386 -0
- kubectl_mcp_tool/config/schema.py +184 -0
- kubectl_mcp_tool/k8s_config.py +127 -1
- kubectl_mcp_tool/mcp_server.py +219 -8
- kubectl_mcp_tool/observability/__init__.py +59 -0
- kubectl_mcp_tool/observability/metrics.py +223 -0
- kubectl_mcp_tool/observability/stats.py +255 -0
- kubectl_mcp_tool/observability/tracing.py +335 -0
- kubectl_mcp_tool/prompts/__init__.py +43 -0
- kubectl_mcp_tool/prompts/builtin.py +695 -0
- kubectl_mcp_tool/prompts/custom.py +298 -0
- kubectl_mcp_tool/prompts/prompts.py +180 -4
- kubectl_mcp_tool/providers.py +347 -0
- kubectl_mcp_tool/safety.py +155 -0
- kubectl_mcp_tool/tools/cluster.py +384 -0
- tests/test_config.py +386 -0
- tests/test_mcp_integration.py +251 -0
- tests/test_observability.py +521 -0
- tests/test_prompts.py +716 -0
- tests/test_safety.py +218 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/top_level.txt +0 -0
tests/test_config.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""Tests for configuration management module."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestServerConfig:
|
|
12
|
+
"""Test ServerConfig dataclass."""
|
|
13
|
+
|
|
14
|
+
def test_default_values(self):
|
|
15
|
+
"""Test default configuration values."""
|
|
16
|
+
from kubectl_mcp_tool.config.schema import ServerConfig
|
|
17
|
+
|
|
18
|
+
config = ServerConfig()
|
|
19
|
+
assert config.transport == "streamable-http"
|
|
20
|
+
assert config.host == "127.0.0.1"
|
|
21
|
+
assert config.port == 8000
|
|
22
|
+
assert config.debug is False
|
|
23
|
+
assert config.log_file is None
|
|
24
|
+
|
|
25
|
+
def test_custom_values(self):
|
|
26
|
+
"""Test custom configuration values."""
|
|
27
|
+
from kubectl_mcp_tool.config.schema import ServerConfig
|
|
28
|
+
|
|
29
|
+
config = ServerConfig(
|
|
30
|
+
transport="stdio",
|
|
31
|
+
host="0.0.0.0",
|
|
32
|
+
port=9000,
|
|
33
|
+
debug=True,
|
|
34
|
+
log_file="/var/log/mcp.log",
|
|
35
|
+
)
|
|
36
|
+
assert config.transport == "stdio"
|
|
37
|
+
assert config.host == "0.0.0.0"
|
|
38
|
+
assert config.port == 9000
|
|
39
|
+
assert config.debug is True
|
|
40
|
+
assert config.log_file == "/var/log/mcp.log"
|
|
41
|
+
|
|
42
|
+
def test_invalid_transport(self):
|
|
43
|
+
"""Test validation rejects invalid transport."""
|
|
44
|
+
from kubectl_mcp_tool.config.schema import ServerConfig
|
|
45
|
+
|
|
46
|
+
with pytest.raises(ValueError, match="Invalid transport"):
|
|
47
|
+
ServerConfig(transport="invalid")
|
|
48
|
+
|
|
49
|
+
def test_invalid_port(self):
|
|
50
|
+
"""Test validation rejects invalid port."""
|
|
51
|
+
from kubectl_mcp_tool.config.schema import ServerConfig
|
|
52
|
+
|
|
53
|
+
with pytest.raises(ValueError, match="Invalid port"):
|
|
54
|
+
ServerConfig(port=0)
|
|
55
|
+
|
|
56
|
+
with pytest.raises(ValueError, match="Invalid port"):
|
|
57
|
+
ServerConfig(port=70000)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestSafetyConfig:
|
|
61
|
+
"""Test SafetyConfig dataclass."""
|
|
62
|
+
|
|
63
|
+
def test_default_values(self):
|
|
64
|
+
"""Test default safety configuration."""
|
|
65
|
+
from kubectl_mcp_tool.config.schema import SafetyConfig
|
|
66
|
+
|
|
67
|
+
config = SafetyConfig()
|
|
68
|
+
assert config.mode == "normal"
|
|
69
|
+
assert config.confirm_destructive is False
|
|
70
|
+
assert config.max_delete_count == 10
|
|
71
|
+
assert "kube-system" in config.blocked_namespaces
|
|
72
|
+
|
|
73
|
+
def test_valid_modes(self):
|
|
74
|
+
"""Test all valid safety modes."""
|
|
75
|
+
from kubectl_mcp_tool.config.schema import SafetyConfig
|
|
76
|
+
|
|
77
|
+
for mode in ["normal", "read-only", "disable-destructive"]:
|
|
78
|
+
config = SafetyConfig(mode=mode)
|
|
79
|
+
assert config.mode == mode
|
|
80
|
+
|
|
81
|
+
def test_invalid_mode(self):
|
|
82
|
+
"""Test validation rejects invalid mode."""
|
|
83
|
+
from kubectl_mcp_tool.config.schema import SafetyConfig
|
|
84
|
+
|
|
85
|
+
with pytest.raises(ValueError, match="Invalid safety mode"):
|
|
86
|
+
SafetyConfig(mode="invalid")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestBrowserConfig:
|
|
90
|
+
"""Test BrowserConfig dataclass."""
|
|
91
|
+
|
|
92
|
+
def test_default_values(self):
|
|
93
|
+
"""Test default browser configuration."""
|
|
94
|
+
from kubectl_mcp_tool.config.schema import BrowserConfig
|
|
95
|
+
|
|
96
|
+
config = BrowserConfig()
|
|
97
|
+
assert config.enabled is False
|
|
98
|
+
assert config.provider == "local"
|
|
99
|
+
assert config.headed is False
|
|
100
|
+
assert config.timeout == 60
|
|
101
|
+
|
|
102
|
+
def test_valid_providers(self):
|
|
103
|
+
"""Test all valid browser providers."""
|
|
104
|
+
from kubectl_mcp_tool.config.schema import BrowserConfig
|
|
105
|
+
|
|
106
|
+
for provider in ["local", "browserbase", "browseruse", "cdp"]:
|
|
107
|
+
config = BrowserConfig(provider=provider)
|
|
108
|
+
assert config.provider == provider
|
|
109
|
+
|
|
110
|
+
def test_invalid_provider(self):
|
|
111
|
+
"""Test validation rejects invalid provider."""
|
|
112
|
+
from kubectl_mcp_tool.config.schema import BrowserConfig
|
|
113
|
+
|
|
114
|
+
with pytest.raises(ValueError, match="Invalid browser provider"):
|
|
115
|
+
BrowserConfig(provider="invalid")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestMetricsConfig:
|
|
119
|
+
"""Test MetricsConfig dataclass."""
|
|
120
|
+
|
|
121
|
+
def test_default_values(self):
|
|
122
|
+
"""Test default metrics configuration."""
|
|
123
|
+
from kubectl_mcp_tool.config.schema import MetricsConfig
|
|
124
|
+
|
|
125
|
+
config = MetricsConfig()
|
|
126
|
+
assert config.enabled is False
|
|
127
|
+
assert config.endpoint == "/metrics"
|
|
128
|
+
assert config.sample_rate == 1.0
|
|
129
|
+
|
|
130
|
+
def test_invalid_sample_rate(self):
|
|
131
|
+
"""Test validation rejects invalid sample rate."""
|
|
132
|
+
from kubectl_mcp_tool.config.schema import MetricsConfig
|
|
133
|
+
|
|
134
|
+
with pytest.raises(ValueError, match="Invalid sample_rate"):
|
|
135
|
+
MetricsConfig(sample_rate=1.5)
|
|
136
|
+
|
|
137
|
+
with pytest.raises(ValueError, match="Invalid sample_rate"):
|
|
138
|
+
MetricsConfig(sample_rate=-0.1)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestValidateConfig:
|
|
142
|
+
"""Test validate_config function."""
|
|
143
|
+
|
|
144
|
+
def test_valid_config(self):
|
|
145
|
+
"""Test validation passes for valid config."""
|
|
146
|
+
from kubectl_mcp_tool.config.schema import validate_config
|
|
147
|
+
|
|
148
|
+
config = {
|
|
149
|
+
"server": {"transport": "stdio", "port": 8000},
|
|
150
|
+
"safety": {"mode": "read-only"},
|
|
151
|
+
"browser": {"provider": "browserbase"},
|
|
152
|
+
"metrics": {"sample_rate": 0.5},
|
|
153
|
+
}
|
|
154
|
+
errors = validate_config(config)
|
|
155
|
+
assert errors == []
|
|
156
|
+
|
|
157
|
+
def test_invalid_config(self):
|
|
158
|
+
"""Test validation catches errors."""
|
|
159
|
+
from kubectl_mcp_tool.config.schema import validate_config
|
|
160
|
+
|
|
161
|
+
config = {
|
|
162
|
+
"server": {"transport": "invalid", "port": 0},
|
|
163
|
+
"safety": {"mode": "invalid"},
|
|
164
|
+
"browser": {"provider": "invalid"},
|
|
165
|
+
"metrics": {"sample_rate": 2.0},
|
|
166
|
+
}
|
|
167
|
+
errors = validate_config(config)
|
|
168
|
+
assert len(errors) == 5
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TestConfigPaths:
|
|
172
|
+
"""Test get_config_paths function."""
|
|
173
|
+
|
|
174
|
+
def test_default_paths(self):
|
|
175
|
+
"""Test default config paths."""
|
|
176
|
+
from kubectl_mcp_tool.config.loader import get_config_paths
|
|
177
|
+
|
|
178
|
+
paths = get_config_paths()
|
|
179
|
+
assert "config_dir" in paths
|
|
180
|
+
assert "main_config" in paths
|
|
181
|
+
assert "drop_in_dir" in paths
|
|
182
|
+
assert paths["main_config"].name == "config.toml"
|
|
183
|
+
assert paths["drop_in_dir"].name == "config.d"
|
|
184
|
+
|
|
185
|
+
def test_xdg_config_home(self):
|
|
186
|
+
"""Test XDG_CONFIG_HOME is respected."""
|
|
187
|
+
from kubectl_mcp_tool.config.loader import get_config_paths
|
|
188
|
+
|
|
189
|
+
with patch.dict(os.environ, {"XDG_CONFIG_HOME": "/custom/config"}):
|
|
190
|
+
paths = get_config_paths()
|
|
191
|
+
assert str(paths["config_dir"]).startswith("/custom/config")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class TestDeepMerge:
|
|
195
|
+
"""Test _deep_merge function."""
|
|
196
|
+
|
|
197
|
+
def test_simple_merge(self):
|
|
198
|
+
"""Test simple dictionary merge."""
|
|
199
|
+
from kubectl_mcp_tool.config.loader import _deep_merge
|
|
200
|
+
|
|
201
|
+
base = {"a": 1, "b": 2}
|
|
202
|
+
override = {"b": 3, "c": 4}
|
|
203
|
+
result = _deep_merge(base, override)
|
|
204
|
+
assert result == {"a": 1, "b": 3, "c": 4}
|
|
205
|
+
|
|
206
|
+
def test_nested_merge(self):
|
|
207
|
+
"""Test nested dictionary merge."""
|
|
208
|
+
from kubectl_mcp_tool.config.loader import _deep_merge
|
|
209
|
+
|
|
210
|
+
base = {"a": {"x": 1, "y": 2}, "b": 3}
|
|
211
|
+
override = {"a": {"y": 20, "z": 30}}
|
|
212
|
+
result = _deep_merge(base, override)
|
|
213
|
+
assert result == {"a": {"x": 1, "y": 20, "z": 30}, "b": 3}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class TestEnvOverrides:
|
|
217
|
+
"""Test _apply_env_overrides function."""
|
|
218
|
+
|
|
219
|
+
def test_server_port_override(self):
|
|
220
|
+
"""Test MCP_SERVER_PORT environment override."""
|
|
221
|
+
from kubectl_mcp_tool.config.loader import _apply_env_overrides
|
|
222
|
+
|
|
223
|
+
with patch.dict(os.environ, {"MCP_SERVER_PORT": "9000"}):
|
|
224
|
+
result = _apply_env_overrides({})
|
|
225
|
+
assert result["server"]["port"] == 9000
|
|
226
|
+
|
|
227
|
+
def test_safety_mode_override(self):
|
|
228
|
+
"""Test MCP_SAFETY_MODE environment override."""
|
|
229
|
+
from kubectl_mcp_tool.config.loader import _apply_env_overrides
|
|
230
|
+
|
|
231
|
+
with patch.dict(os.environ, {"MCP_SAFETY_MODE": "read-only"}):
|
|
232
|
+
result = _apply_env_overrides({})
|
|
233
|
+
assert result["safety"]["mode"] == "read-only"
|
|
234
|
+
|
|
235
|
+
def test_browser_enabled_override(self):
|
|
236
|
+
"""Test MCP_BROWSER_ENABLED environment override."""
|
|
237
|
+
from kubectl_mcp_tool.config.loader import _apply_env_overrides
|
|
238
|
+
|
|
239
|
+
with patch.dict(os.environ, {"MCP_BROWSER_ENABLED": "true"}):
|
|
240
|
+
result = _apply_env_overrides({})
|
|
241
|
+
assert result["browser"]["enabled"] is True
|
|
242
|
+
|
|
243
|
+
def test_debug_override(self):
|
|
244
|
+
"""Test MCP_DEBUG environment override."""
|
|
245
|
+
from kubectl_mcp_tool.config.loader import _apply_env_overrides
|
|
246
|
+
|
|
247
|
+
with patch.dict(os.environ, {"MCP_DEBUG": "1"}):
|
|
248
|
+
result = _apply_env_overrides({})
|
|
249
|
+
assert result["server"]["debug"] is True
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class TestLoadConfig:
|
|
253
|
+
"""Test load_config function."""
|
|
254
|
+
|
|
255
|
+
def test_load_default_config(self):
|
|
256
|
+
"""Test loading config with defaults."""
|
|
257
|
+
from kubectl_mcp_tool.config.loader import load_config
|
|
258
|
+
|
|
259
|
+
config = load_config(skip_env=True)
|
|
260
|
+
assert config.server.transport == "streamable-http"
|
|
261
|
+
assert config.server.port == 8000
|
|
262
|
+
assert config.safety.mode == "normal"
|
|
263
|
+
|
|
264
|
+
def test_load_from_file(self):
|
|
265
|
+
"""Test loading config from TOML file."""
|
|
266
|
+
pytest.importorskip("tomli", reason="tomli required for TOML parsing")
|
|
267
|
+
from kubectl_mcp_tool.config.loader import load_config
|
|
268
|
+
|
|
269
|
+
with tempfile.NamedTemporaryFile(suffix=".toml", delete=False, mode="w") as f:
|
|
270
|
+
f.write("""
|
|
271
|
+
[server]
|
|
272
|
+
port = 9999
|
|
273
|
+
debug = true
|
|
274
|
+
|
|
275
|
+
[safety]
|
|
276
|
+
mode = "read-only"
|
|
277
|
+
""")
|
|
278
|
+
f.flush()
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
config = load_config(config_file=f.name, skip_env=True)
|
|
282
|
+
assert config.server.port == 9999
|
|
283
|
+
assert config.server.debug is True
|
|
284
|
+
assert config.safety.mode == "read-only"
|
|
285
|
+
finally:
|
|
286
|
+
os.unlink(f.name)
|
|
287
|
+
|
|
288
|
+
def test_env_overrides_applied(self):
|
|
289
|
+
"""Test environment overrides are applied."""
|
|
290
|
+
from kubectl_mcp_tool.config.loader import load_config
|
|
291
|
+
|
|
292
|
+
with patch.dict(os.environ, {"MCP_SERVER_PORT": "7777"}):
|
|
293
|
+
config = load_config()
|
|
294
|
+
assert config.server.port == 7777
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class TestGetConfig:
|
|
298
|
+
"""Test get_config function."""
|
|
299
|
+
|
|
300
|
+
def test_get_config_singleton(self):
|
|
301
|
+
"""Test get_config returns same instance."""
|
|
302
|
+
from kubectl_mcp_tool.config import loader
|
|
303
|
+
|
|
304
|
+
# Reset global config
|
|
305
|
+
loader._config = None
|
|
306
|
+
|
|
307
|
+
config1 = loader.get_config()
|
|
308
|
+
config2 = loader.get_config()
|
|
309
|
+
assert config1 is config2
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class TestReloadConfig:
|
|
313
|
+
"""Test reload_config function."""
|
|
314
|
+
|
|
315
|
+
def test_reload_config(self):
|
|
316
|
+
"""Test configuration reload."""
|
|
317
|
+
from kubectl_mcp_tool.config import loader
|
|
318
|
+
|
|
319
|
+
# Load initial config
|
|
320
|
+
loader._config = None
|
|
321
|
+
config1 = loader.load_config(skip_env=True)
|
|
322
|
+
|
|
323
|
+
# Reload
|
|
324
|
+
config2 = loader.reload_config()
|
|
325
|
+
assert config2 is not None
|
|
326
|
+
assert config2.server.port == config1.server.port
|
|
327
|
+
|
|
328
|
+
def test_reload_callback(self):
|
|
329
|
+
"""Test reload callbacks are called."""
|
|
330
|
+
from kubectl_mcp_tool.config import loader
|
|
331
|
+
|
|
332
|
+
callback_called = []
|
|
333
|
+
|
|
334
|
+
def callback(config):
|
|
335
|
+
callback_called.append(config)
|
|
336
|
+
|
|
337
|
+
loader._config = None
|
|
338
|
+
loader.load_config(skip_env=True)
|
|
339
|
+
loader.register_reload_callback(callback)
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
loader.reload_config()
|
|
343
|
+
assert len(callback_called) == 1
|
|
344
|
+
finally:
|
|
345
|
+
loader.unregister_reload_callback(callback)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class TestSighupHandler:
|
|
349
|
+
"""Test SIGHUP handler setup."""
|
|
350
|
+
|
|
351
|
+
def test_setup_sighup_handler(self):
|
|
352
|
+
"""Test SIGHUP handler can be installed."""
|
|
353
|
+
import sys
|
|
354
|
+
|
|
355
|
+
from kubectl_mcp_tool.config.loader import setup_sighup_handler
|
|
356
|
+
|
|
357
|
+
if sys.platform == "win32":
|
|
358
|
+
result = setup_sighup_handler()
|
|
359
|
+
assert result is False
|
|
360
|
+
else:
|
|
361
|
+
result = setup_sighup_handler()
|
|
362
|
+
assert result is True
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class TestConfigDataclass:
|
|
366
|
+
"""Test Config root dataclass."""
|
|
367
|
+
|
|
368
|
+
def test_config_has_all_sections(self):
|
|
369
|
+
"""Test Config has all expected sections."""
|
|
370
|
+
from kubectl_mcp_tool.config.schema import Config
|
|
371
|
+
|
|
372
|
+
config = Config()
|
|
373
|
+
assert hasattr(config, "server")
|
|
374
|
+
assert hasattr(config, "safety")
|
|
375
|
+
assert hasattr(config, "browser")
|
|
376
|
+
assert hasattr(config, "metrics")
|
|
377
|
+
assert hasattr(config, "logging")
|
|
378
|
+
assert hasattr(config, "kubernetes")
|
|
379
|
+
assert hasattr(config, "custom")
|
|
380
|
+
|
|
381
|
+
def test_config_custom_section(self):
|
|
382
|
+
"""Test Config custom section for unknown keys."""
|
|
383
|
+
from kubectl_mcp_tool.config.schema import Config
|
|
384
|
+
|
|
385
|
+
config = Config(custom={"my_plugin": {"setting": "value"}})
|
|
386
|
+
assert config.custom["my_plugin"]["setting"] == "value"
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Integration tests for MCP server with safety, observability, and config modules."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from unittest.mock import patch, MagicMock
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestMCPServerIntegration:
|
|
10
|
+
"""Test MCP server integration with safety, observability, and config modules."""
|
|
11
|
+
|
|
12
|
+
def test_import_mcp_server(self):
|
|
13
|
+
"""Test that MCP server can be imported without errors."""
|
|
14
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
15
|
+
assert MCPServer is not None
|
|
16
|
+
|
|
17
|
+
def test_mcp_server_init_default(self):
|
|
18
|
+
"""Test MCP server initialization with defaults."""
|
|
19
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
20
|
+
from kubectl_mcp_tool.safety import SafetyMode, get_safety_mode, set_safety_mode
|
|
21
|
+
|
|
22
|
+
# Reset to normal mode
|
|
23
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
24
|
+
|
|
25
|
+
server = MCPServer("test-server")
|
|
26
|
+
assert server.name == "test-server"
|
|
27
|
+
assert get_safety_mode() == SafetyMode.NORMAL
|
|
28
|
+
|
|
29
|
+
def test_mcp_server_init_read_only(self):
|
|
30
|
+
"""Test MCP server initialization with read-only mode."""
|
|
31
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
32
|
+
from kubectl_mcp_tool.safety import SafetyMode, get_safety_mode, set_safety_mode
|
|
33
|
+
|
|
34
|
+
# Reset to normal mode first
|
|
35
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
36
|
+
|
|
37
|
+
server = MCPServer("test-server", read_only=True)
|
|
38
|
+
assert server.name == "test-server"
|
|
39
|
+
assert get_safety_mode() == SafetyMode.READ_ONLY
|
|
40
|
+
assert server.non_destructive is True
|
|
41
|
+
|
|
42
|
+
def test_mcp_server_init_disable_destructive(self):
|
|
43
|
+
"""Test MCP server initialization with disable-destructive mode."""
|
|
44
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
45
|
+
from kubectl_mcp_tool.safety import SafetyMode, get_safety_mode, set_safety_mode
|
|
46
|
+
|
|
47
|
+
# Reset to normal mode first
|
|
48
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
49
|
+
|
|
50
|
+
server = MCPServer("test-server", disable_destructive=True)
|
|
51
|
+
assert server.name == "test-server"
|
|
52
|
+
assert get_safety_mode() == SafetyMode.DISABLE_DESTRUCTIVE
|
|
53
|
+
assert server.non_destructive is True
|
|
54
|
+
|
|
55
|
+
def test_mcp_server_init_with_config_file(self):
|
|
56
|
+
"""Test MCP server initialization with config file."""
|
|
57
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
58
|
+
from kubectl_mcp_tool.safety import SafetyMode, set_safety_mode, get_safety_mode
|
|
59
|
+
|
|
60
|
+
# Reset to normal mode first
|
|
61
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
62
|
+
|
|
63
|
+
# Create a temporary config file with valid transport
|
|
64
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f:
|
|
65
|
+
f.write("""
|
|
66
|
+
[server]
|
|
67
|
+
transport = "stdio"
|
|
68
|
+
port = 9000
|
|
69
|
+
|
|
70
|
+
[safety]
|
|
71
|
+
mode = "read-only"
|
|
72
|
+
""")
|
|
73
|
+
config_file = f.name
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
server = MCPServer("test-server", config_file=config_file)
|
|
77
|
+
assert server.name == "test-server"
|
|
78
|
+
assert server.config is not None
|
|
79
|
+
# Config file sets read-only mode
|
|
80
|
+
assert get_safety_mode() == SafetyMode.READ_ONLY
|
|
81
|
+
finally:
|
|
82
|
+
os.unlink(config_file)
|
|
83
|
+
|
|
84
|
+
def test_mcp_server_has_stats_collector(self):
|
|
85
|
+
"""Test MCP server has stats collector initialized."""
|
|
86
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
87
|
+
from kubectl_mcp_tool.safety import SafetyMode, set_safety_mode
|
|
88
|
+
|
|
89
|
+
# Reset to normal mode first
|
|
90
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
91
|
+
|
|
92
|
+
server = MCPServer("test-server")
|
|
93
|
+
assert server._stats is not None
|
|
94
|
+
# Can get stats
|
|
95
|
+
stats = server._stats.get_stats()
|
|
96
|
+
assert "uptime_seconds" in stats
|
|
97
|
+
assert "tool_calls_total" in stats
|
|
98
|
+
|
|
99
|
+
def test_mcp_server_reload_callback(self):
|
|
100
|
+
"""Test MCP server registers reload callback."""
|
|
101
|
+
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
102
|
+
from kubectl_mcp_tool.config import reload_config
|
|
103
|
+
from kubectl_mcp_tool.safety import SafetyMode, set_safety_mode
|
|
104
|
+
|
|
105
|
+
# Reset to normal mode first
|
|
106
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
107
|
+
|
|
108
|
+
server = MCPServer("test-server")
|
|
109
|
+
|
|
110
|
+
# Reload config should not raise
|
|
111
|
+
# The callback is registered and will be called
|
|
112
|
+
try:
|
|
113
|
+
reload_config()
|
|
114
|
+
except Exception:
|
|
115
|
+
# Config files may not exist, which is fine
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
def test_cli_parameters_read_only(self):
|
|
119
|
+
"""Test CLI parameters for read-only mode."""
|
|
120
|
+
from kubectl_mcp_tool.safety import SafetyMode, set_safety_mode
|
|
121
|
+
|
|
122
|
+
# Reset to normal mode first
|
|
123
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
124
|
+
|
|
125
|
+
# Verify the set_safety_mode works as expected
|
|
126
|
+
set_safety_mode(SafetyMode.READ_ONLY)
|
|
127
|
+
from kubectl_mcp_tool.safety import get_safety_mode
|
|
128
|
+
assert get_safety_mode() == SafetyMode.READ_ONLY
|
|
129
|
+
|
|
130
|
+
def test_cli_parameters_disable_destructive(self):
|
|
131
|
+
"""Test CLI parameters for disable-destructive mode."""
|
|
132
|
+
from kubectl_mcp_tool.safety import SafetyMode, set_safety_mode, get_safety_mode
|
|
133
|
+
|
|
134
|
+
# Reset to normal mode first
|
|
135
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
136
|
+
|
|
137
|
+
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
138
|
+
assert get_safety_mode() == SafetyMode.DISABLE_DESTRUCTIVE
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestMCPServerObservability:
|
|
142
|
+
"""Test observability integration in MCP server."""
|
|
143
|
+
|
|
144
|
+
def test_stats_collector_integration(self):
|
|
145
|
+
"""Test stats collector is available in MCP server."""
|
|
146
|
+
from kubectl_mcp_tool.observability import get_stats_collector
|
|
147
|
+
|
|
148
|
+
stats = get_stats_collector()
|
|
149
|
+
assert stats is not None
|
|
150
|
+
|
|
151
|
+
# Record some calls
|
|
152
|
+
stats.record_tool_call("integration_test_tool", success=True, duration=0.1)
|
|
153
|
+
tool_stats = stats.get_tool_stats("integration_test_tool")
|
|
154
|
+
assert tool_stats is not None
|
|
155
|
+
assert tool_stats["calls"] >= 1
|
|
156
|
+
assert tool_stats["errors"] == 0
|
|
157
|
+
|
|
158
|
+
def test_metrics_availability(self):
|
|
159
|
+
"""Test Prometheus metrics availability check."""
|
|
160
|
+
from kubectl_mcp_tool.observability import is_prometheus_available, get_metrics
|
|
161
|
+
|
|
162
|
+
# Check availability (may or may not be installed)
|
|
163
|
+
available = is_prometheus_available()
|
|
164
|
+
|
|
165
|
+
if available:
|
|
166
|
+
metrics = get_metrics()
|
|
167
|
+
assert isinstance(metrics, str)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TestMCPServerConfig:
|
|
171
|
+
"""Test config integration in MCP server."""
|
|
172
|
+
|
|
173
|
+
def test_load_config(self):
|
|
174
|
+
"""Test config loading."""
|
|
175
|
+
from kubectl_mcp_tool.config import load_config
|
|
176
|
+
|
|
177
|
+
config = load_config()
|
|
178
|
+
assert config is not None
|
|
179
|
+
assert hasattr(config, 'server')
|
|
180
|
+
assert hasattr(config, 'safety')
|
|
181
|
+
assert hasattr(config, 'browser')
|
|
182
|
+
|
|
183
|
+
def test_config_reload_callbacks(self):
|
|
184
|
+
"""Test config reload callback registration."""
|
|
185
|
+
from kubectl_mcp_tool.config import (
|
|
186
|
+
register_reload_callback,
|
|
187
|
+
unregister_reload_callback,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
callback_called = []
|
|
191
|
+
|
|
192
|
+
def test_callback(config):
|
|
193
|
+
callback_called.append(config)
|
|
194
|
+
|
|
195
|
+
register_reload_callback(test_callback)
|
|
196
|
+
|
|
197
|
+
# Unregister to clean up
|
|
198
|
+
unregister_reload_callback(test_callback)
|
|
199
|
+
|
|
200
|
+
# Verify unregister worked
|
|
201
|
+
assert len(callback_called) == 0 # Not called since we unregistered
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestMCPServerSafety:
|
|
205
|
+
"""Test safety mode integration in MCP server."""
|
|
206
|
+
|
|
207
|
+
def test_safety_mode_info(self):
|
|
208
|
+
"""Test safety mode info retrieval."""
|
|
209
|
+
from kubectl_mcp_tool.safety import (
|
|
210
|
+
SafetyMode,
|
|
211
|
+
set_safety_mode,
|
|
212
|
+
get_mode_info,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
216
|
+
info = get_mode_info()
|
|
217
|
+
assert info["mode"] == "normal"
|
|
218
|
+
assert "description" in info
|
|
219
|
+
assert info["blocked_operations"] == []
|
|
220
|
+
|
|
221
|
+
set_safety_mode(SafetyMode.READ_ONLY)
|
|
222
|
+
info = get_mode_info()
|
|
223
|
+
assert info["mode"] == "read_only"
|
|
224
|
+
assert len(info["blocked_operations"]) > 0
|
|
225
|
+
|
|
226
|
+
def test_operation_allowed_check(self):
|
|
227
|
+
"""Test operation allowed check."""
|
|
228
|
+
from kubectl_mcp_tool.safety import (
|
|
229
|
+
SafetyMode,
|
|
230
|
+
set_safety_mode,
|
|
231
|
+
is_operation_allowed,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
235
|
+
allowed, reason = is_operation_allowed("delete_pod")
|
|
236
|
+
assert allowed is True
|
|
237
|
+
assert reason == ""
|
|
238
|
+
|
|
239
|
+
set_safety_mode(SafetyMode.READ_ONLY)
|
|
240
|
+
allowed, reason = is_operation_allowed("delete_pod")
|
|
241
|
+
assert allowed is False
|
|
242
|
+
assert "blocked" in reason.lower()
|
|
243
|
+
|
|
244
|
+
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
245
|
+
allowed, reason = is_operation_allowed("delete_pod")
|
|
246
|
+
assert allowed is False
|
|
247
|
+
assert "blocked" in reason.lower()
|
|
248
|
+
|
|
249
|
+
# Non-destructive write should be allowed
|
|
250
|
+
allowed, reason = is_operation_allowed("create_deployment")
|
|
251
|
+
assert allowed is True
|