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.
Files changed (30) hide show
  1. {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/METADATA +48 -1
  2. {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/RECORD +30 -15
  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/k8s_config.py +127 -1
  10. kubectl_mcp_tool/mcp_server.py +219 -8
  11. kubectl_mcp_tool/observability/__init__.py +59 -0
  12. kubectl_mcp_tool/observability/metrics.py +223 -0
  13. kubectl_mcp_tool/observability/stats.py +255 -0
  14. kubectl_mcp_tool/observability/tracing.py +335 -0
  15. kubectl_mcp_tool/prompts/__init__.py +43 -0
  16. kubectl_mcp_tool/prompts/builtin.py +695 -0
  17. kubectl_mcp_tool/prompts/custom.py +298 -0
  18. kubectl_mcp_tool/prompts/prompts.py +180 -4
  19. kubectl_mcp_tool/providers.py +347 -0
  20. kubectl_mcp_tool/safety.py +155 -0
  21. kubectl_mcp_tool/tools/cluster.py +384 -0
  22. tests/test_config.py +386 -0
  23. tests/test_mcp_integration.py +251 -0
  24. tests/test_observability.py +521 -0
  25. tests/test_prompts.py +716 -0
  26. tests/test_safety.py +218 -0
  27. {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/WHEEL +0 -0
  28. {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/entry_points.txt +0 -0
  29. {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/licenses/LICENSE +0 -0
  30. {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