kubectl-mcp-server 1.15.0__py3-none-any.whl → 1.17.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.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/METADATA +34 -13
  2. kubectl_mcp_server-1.17.0.dist-info/RECORD +75 -0
  3. kubectl_mcp_tool/__init__.py +1 -1
  4. kubectl_mcp_tool/cli/cli.py +83 -9
  5. kubectl_mcp_tool/cli/output.py +14 -0
  6. kubectl_mcp_tool/config/__init__.py +46 -0
  7. kubectl_mcp_tool/config/loader.py +386 -0
  8. kubectl_mcp_tool/config/schema.py +184 -0
  9. kubectl_mcp_tool/crd_detector.py +247 -0
  10. kubectl_mcp_tool/k8s_config.py +19 -0
  11. kubectl_mcp_tool/mcp_server.py +246 -8
  12. kubectl_mcp_tool/observability/__init__.py +59 -0
  13. kubectl_mcp_tool/observability/metrics.py +223 -0
  14. kubectl_mcp_tool/observability/stats.py +255 -0
  15. kubectl_mcp_tool/observability/tracing.py +335 -0
  16. kubectl_mcp_tool/prompts/__init__.py +43 -0
  17. kubectl_mcp_tool/prompts/builtin.py +695 -0
  18. kubectl_mcp_tool/prompts/custom.py +298 -0
  19. kubectl_mcp_tool/prompts/prompts.py +180 -4
  20. kubectl_mcp_tool/safety.py +155 -0
  21. kubectl_mcp_tool/tools/__init__.py +20 -0
  22. kubectl_mcp_tool/tools/backup.py +881 -0
  23. kubectl_mcp_tool/tools/capi.py +727 -0
  24. kubectl_mcp_tool/tools/certs.py +709 -0
  25. kubectl_mcp_tool/tools/cilium.py +582 -0
  26. kubectl_mcp_tool/tools/cluster.py +384 -0
  27. kubectl_mcp_tool/tools/gitops.py +552 -0
  28. kubectl_mcp_tool/tools/keda.py +464 -0
  29. kubectl_mcp_tool/tools/kiali.py +652 -0
  30. kubectl_mcp_tool/tools/kubevirt.py +803 -0
  31. kubectl_mcp_tool/tools/policy.py +554 -0
  32. kubectl_mcp_tool/tools/rollouts.py +790 -0
  33. tests/test_browser.py +2 -2
  34. tests/test_config.py +386 -0
  35. tests/test_ecosystem.py +331 -0
  36. tests/test_mcp_integration.py +251 -0
  37. tests/test_observability.py +521 -0
  38. tests/test_prompts.py +716 -0
  39. tests/test_safety.py +218 -0
  40. tests/test_tools.py +70 -8
  41. kubectl_mcp_server-1.15.0.dist-info/RECORD +0 -49
  42. {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/WHEEL +0 -0
  43. {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/entry_points.txt +0 -0
  44. {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/licenses/LICENSE +0 -0
  45. {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/top_level.txt +0 -0
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 (131 + 26 = 157)
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) == 157, f"Expected 157 tools (131 + 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
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"