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
@@ -0,0 +1,331 @@
1
+ """
2
+ Unit tests for ecosystem tools (GitOps, Cert-Manager, Policy, Backup).
3
+
4
+ This module tests the CRD detector and all ecosystem toolsets.
5
+ """
6
+
7
+ import pytest
8
+ from unittest.mock import patch, MagicMock
9
+ import subprocess
10
+
11
+
12
+ class TestCRDDetector:
13
+ """Tests for the CRD auto-discovery framework."""
14
+
15
+ @pytest.mark.unit
16
+ def test_crd_detector_imports(self):
17
+ """Test that CRD detector module can be imported."""
18
+ from kubectl_mcp_tool.crd_detector import (
19
+ CRD_GROUPS,
20
+ detect_crds,
21
+ crd_exists,
22
+ get_enabled_toolsets,
23
+ get_crd_status_summary,
24
+ FeatureNotInstalledError,
25
+ require_crd,
26
+ require_any_crd,
27
+ )
28
+ assert CRD_GROUPS is not None
29
+ assert callable(detect_crds)
30
+ assert callable(crd_exists)
31
+ assert callable(get_enabled_toolsets)
32
+ assert callable(get_crd_status_summary)
33
+ assert issubclass(FeatureNotInstalledError, Exception)
34
+ assert callable(require_crd)
35
+ assert callable(require_any_crd)
36
+
37
+ @pytest.mark.unit
38
+ def test_crd_groups_structure(self):
39
+ """Test that CRD_GROUPS has expected structure."""
40
+ from kubectl_mcp_tool.crd_detector import CRD_GROUPS
41
+
42
+ expected_groups = ["flux", "argocd", "certmanager", "kyverno", "gatekeeper", "velero"]
43
+ for group in expected_groups:
44
+ assert group in CRD_GROUPS, f"Expected group '{group}' in CRD_GROUPS"
45
+ assert isinstance(CRD_GROUPS[group], list), f"CRD_GROUPS['{group}'] should be a list"
46
+ assert len(CRD_GROUPS[group]) > 0, f"CRD_GROUPS['{group}'] should not be empty"
47
+
48
+ @pytest.mark.unit
49
+ def test_detect_crds_with_mocked_kubectl(self):
50
+ """Test CRD detection with mocked kubectl."""
51
+ from kubectl_mcp_tool.crd_detector import detect_crds, _crd_cache
52
+
53
+ # Clear cache before test
54
+ _crd_cache.clear()
55
+
56
+ mock_output = """NAME CREATED AT
57
+ applications.argoproj.io 2024-01-01T00:00:00Z
58
+ certificates.cert-manager.io 2024-01-01T00:00:00Z
59
+ """
60
+ with patch("subprocess.run") as mock_run:
61
+ mock_run.return_value = MagicMock(
62
+ returncode=0,
63
+ stdout=mock_output
64
+ )
65
+ result = detect_crds(force_refresh=True)
66
+
67
+ assert isinstance(result, dict)
68
+ assert "argocd" in result
69
+ assert "certmanager" in result
70
+
71
+ @pytest.mark.unit
72
+ def test_detect_crds_handles_kubectl_failure(self):
73
+ """Test CRD detection handles kubectl failure gracefully."""
74
+ from kubectl_mcp_tool.crd_detector import detect_crds, _crd_cache
75
+
76
+ _crd_cache.clear()
77
+
78
+ with patch("subprocess.run") as mock_run:
79
+ mock_run.side_effect = subprocess.SubprocessError("Command failed")
80
+ result = detect_crds(force_refresh=True)
81
+
82
+ assert isinstance(result, dict)
83
+ # All groups should be False when kubectl fails
84
+ for value in result.values():
85
+ assert value is False
86
+
87
+ @pytest.mark.unit
88
+ def test_feature_not_installed_error(self):
89
+ """Test FeatureNotInstalledError exception."""
90
+ from kubectl_mcp_tool.crd_detector import FeatureNotInstalledError
91
+
92
+ error = FeatureNotInstalledError("velero", ["backups.velero.io"])
93
+ assert "velero" in str(error)
94
+ assert "backups.velero.io" in str(error)
95
+ assert error.toolset == "velero"
96
+ assert "backups.velero.io" in error.required_crds
97
+
98
+ @pytest.mark.unit
99
+ def test_get_crd_status_summary(self):
100
+ """Test CRD status summary generation."""
101
+ from kubectl_mcp_tool.crd_detector import get_crd_status_summary, _crd_cache
102
+
103
+ _crd_cache.clear()
104
+
105
+ with patch("subprocess.run") as mock_run:
106
+ mock_run.return_value = MagicMock(
107
+ returncode=0,
108
+ stdout="applications.argoproj.io 2024-01-01T00:00:00Z\n"
109
+ )
110
+ summary = get_crd_status_summary()
111
+
112
+ # Summary returns a dict with crd_groups and enabled_toolsets
113
+ assert isinstance(summary, dict)
114
+ assert "crd_groups" in summary
115
+ assert "enabled_toolsets" in summary
116
+
117
+
118
+ class TestGitOpsTools:
119
+ """Tests for GitOps toolset (Flux and ArgoCD)."""
120
+
121
+ @pytest.mark.unit
122
+ def test_gitops_tools_import(self):
123
+ """Test that GitOps tools can be imported."""
124
+ from kubectl_mcp_tool.tools.gitops import register_gitops_tools
125
+ assert callable(register_gitops_tools)
126
+
127
+ @pytest.mark.unit
128
+ def test_gitops_tools_register(self, mock_all_kubernetes_apis):
129
+ """Test that GitOps tools register correctly."""
130
+ from kubectl_mcp_tool.mcp_server import MCPServer
131
+ import asyncio
132
+
133
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
134
+ server = MCPServer(name="test")
135
+
136
+ async def get_tools():
137
+ return await server.server.list_tools()
138
+
139
+ tools = asyncio.run(get_tools())
140
+ tool_names = {t.name for t in tools}
141
+
142
+ gitops_tools = [
143
+ "gitops_apps_list_tool", "gitops_app_get_tool", "gitops_app_sync_tool",
144
+ "gitops_app_status_tool", "gitops_sources_list_tool", "gitops_source_get_tool",
145
+ "gitops_detect_engine_tool"
146
+ ]
147
+ for tool in gitops_tools:
148
+ assert tool in tool_names, f"GitOps tool '{tool}' not registered"
149
+
150
+ @pytest.mark.unit
151
+ def test_gitops_non_destructive_mode(self, mock_all_kubernetes_apis):
152
+ """Test that GitOps sync is blocked in non-destructive mode."""
153
+ from kubectl_mcp_tool.mcp_server import MCPServer
154
+
155
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
156
+ server = MCPServer(name="test", non_destructive=True)
157
+
158
+ # Server should initialize with non_destructive=True
159
+ assert server.non_destructive is True
160
+
161
+
162
+ class TestCertManagerTools:
163
+ """Tests for Cert-Manager toolset."""
164
+
165
+ @pytest.mark.unit
166
+ def test_certs_tools_import(self):
167
+ """Test that Cert-Manager tools can be imported."""
168
+ from kubectl_mcp_tool.tools.certs import register_certs_tools
169
+ assert callable(register_certs_tools)
170
+
171
+ @pytest.mark.unit
172
+ def test_certs_tools_register(self, mock_all_kubernetes_apis):
173
+ """Test that Cert-Manager tools register correctly."""
174
+ from kubectl_mcp_tool.mcp_server import MCPServer
175
+ import asyncio
176
+
177
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
178
+ server = MCPServer(name="test")
179
+
180
+ async def get_tools():
181
+ return await server.server.list_tools()
182
+
183
+ tools = asyncio.run(get_tools())
184
+ tool_names = {t.name for t in tools}
185
+
186
+ certs_tools = [
187
+ "certs_list_tool", "certs_get_tool", "certs_issuers_list_tool", "certs_issuer_get_tool",
188
+ "certs_renew_tool", "certs_status_explain_tool", "certs_challenges_list_tool",
189
+ "certs_requests_list_tool", "certs_detect_tool"
190
+ ]
191
+ for tool in certs_tools:
192
+ assert tool in tool_names, f"Cert-Manager tool '{tool}' not registered"
193
+
194
+
195
+ class TestPolicyTools:
196
+ """Tests for Policy toolset (Kyverno and Gatekeeper)."""
197
+
198
+ @pytest.mark.unit
199
+ def test_policy_tools_import(self):
200
+ """Test that Policy tools can be imported."""
201
+ from kubectl_mcp_tool.tools.policy import register_policy_tools
202
+ assert callable(register_policy_tools)
203
+
204
+ @pytest.mark.unit
205
+ def test_policy_tools_register(self, mock_all_kubernetes_apis):
206
+ """Test that Policy tools register correctly."""
207
+ from kubectl_mcp_tool.mcp_server import MCPServer
208
+ import asyncio
209
+
210
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
211
+ server = MCPServer(name="test")
212
+
213
+ async def get_tools():
214
+ return await server.server.list_tools()
215
+
216
+ tools = asyncio.run(get_tools())
217
+ tool_names = {t.name for t in tools}
218
+
219
+ policy_tools = [
220
+ "policy_list_tool", "policy_get_tool", "policy_violations_list_tool",
221
+ "policy_explain_denial_tool", "policy_audit_tool", "policy_detect_tool"
222
+ ]
223
+ for tool in policy_tools:
224
+ assert tool in tool_names, f"Policy tool '{tool}' not registered"
225
+
226
+
227
+ class TestBackupTools:
228
+ """Tests for Backup toolset (Velero)."""
229
+
230
+ @pytest.mark.unit
231
+ def test_backup_tools_import(self):
232
+ """Test that Backup tools can be imported."""
233
+ from kubectl_mcp_tool.tools.backup import register_backup_tools
234
+ assert callable(register_backup_tools)
235
+
236
+ @pytest.mark.unit
237
+ def test_backup_tools_register(self, mock_all_kubernetes_apis):
238
+ """Test that Backup tools register correctly."""
239
+ from kubectl_mcp_tool.mcp_server import MCPServer
240
+ import asyncio
241
+
242
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
243
+ server = MCPServer(name="test")
244
+
245
+ async def get_tools():
246
+ return await server.server.list_tools()
247
+
248
+ tools = asyncio.run(get_tools())
249
+ tool_names = {t.name for t in tools}
250
+
251
+ backup_tools = [
252
+ "backup_list_tool", "backup_get_tool", "backup_create_tool", "backup_delete_tool",
253
+ "restore_list_tool", "restore_create_tool", "restore_get_tool",
254
+ "backup_locations_list_tool", "backup_schedules_list_tool",
255
+ "backup_schedule_create_tool", "backup_detect_tool"
256
+ ]
257
+ for tool in backup_tools:
258
+ assert tool in tool_names, f"Backup tool '{tool}' not registered"
259
+
260
+ @pytest.mark.unit
261
+ def test_backup_non_destructive_mode(self, mock_all_kubernetes_apis):
262
+ """Test that backup operations are blocked in non-destructive mode."""
263
+ from kubectl_mcp_tool.mcp_server import MCPServer
264
+
265
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
266
+ server = MCPServer(name="test", non_destructive=True)
267
+
268
+ # Server should initialize with non_destructive=True
269
+ assert server.non_destructive is True
270
+
271
+
272
+ class TestEcosystemToolsIntegration:
273
+ """Integration tests for ecosystem tools."""
274
+
275
+ @pytest.mark.unit
276
+ def test_all_ecosystem_tools_have_descriptions(self, mock_all_kubernetes_apis):
277
+ """Test that all ecosystem tools have descriptions."""
278
+ from kubectl_mcp_tool.mcp_server import MCPServer
279
+ import asyncio
280
+
281
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
282
+ server = MCPServer(name="test")
283
+
284
+ async def get_tools():
285
+ return await server.server.list_tools()
286
+
287
+ tools = asyncio.run(get_tools())
288
+
289
+ ecosystem_prefixes = ["gitops_", "certs_", "policy_", "backup_", "restore_"]
290
+ ecosystem_tools = [t for t in tools if any(t.name.startswith(p) for p in ecosystem_prefixes)]
291
+
292
+ tools_without_description = [
293
+ t.name for t in ecosystem_tools
294
+ if not t.description or len(t.description.strip()) == 0
295
+ ]
296
+ assert not tools_without_description, f"Ecosystem tools without descriptions: {tools_without_description}"
297
+
298
+ @pytest.mark.unit
299
+ def test_ecosystem_tool_count(self, mock_all_kubernetes_apis):
300
+ """Test that correct number of ecosystem tools are registered."""
301
+ from kubectl_mcp_tool.mcp_server import MCPServer
302
+ import asyncio
303
+
304
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
305
+ server = MCPServer(name="test")
306
+
307
+ async def get_tools():
308
+ return await server.server.list_tools()
309
+
310
+ tools = asyncio.run(get_tools())
311
+
312
+ # Filter ecosystem tools, but exclude backup_resource which is in operations.py
313
+ ecosystem_tool_names = [
314
+ "gitops_apps_list_tool", "gitops_app_get_tool", "gitops_app_sync_tool",
315
+ "gitops_app_status_tool", "gitops_sources_list_tool", "gitops_source_get_tool",
316
+ "gitops_detect_engine_tool",
317
+ "certs_list_tool", "certs_get_tool", "certs_issuers_list_tool", "certs_issuer_get_tool",
318
+ "certs_renew_tool", "certs_status_explain_tool", "certs_challenges_list_tool",
319
+ "certs_requests_list_tool", "certs_detect_tool",
320
+ "policy_list_tool", "policy_get_tool", "policy_violations_list_tool",
321
+ "policy_explain_denial_tool", "policy_audit_tool", "policy_detect_tool",
322
+ "backup_list_tool", "backup_get_tool", "backup_create_tool", "backup_delete_tool",
323
+ "restore_list_tool", "restore_create_tool", "restore_get_tool",
324
+ "backup_locations_list_tool", "backup_schedules_list_tool",
325
+ "backup_schedule_create_tool", "backup_detect_tool"
326
+ ]
327
+ tool_names = {t.name for t in tools}
328
+ ecosystem_tools = [name for name in ecosystem_tool_names if name in tool_names]
329
+
330
+ # 7 GitOps + 9 Certs + 6 Policy + 11 Backup = 33 ecosystem tools
331
+ assert len(ecosystem_tools) == 33, f"Expected 33 ecosystem tools, got {len(ecosystem_tools)}"
@@ -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