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.
Files changed (45) hide show
  1. kubectl_mcp_server-1.12.0.dist-info/METADATA +711 -0
  2. kubectl_mcp_server-1.12.0.dist-info/RECORD +45 -0
  3. kubectl_mcp_server-1.12.0.dist-info/WHEEL +5 -0
  4. kubectl_mcp_server-1.12.0.dist-info/entry_points.txt +3 -0
  5. kubectl_mcp_server-1.12.0.dist-info/licenses/LICENSE +21 -0
  6. kubectl_mcp_server-1.12.0.dist-info/top_level.txt +2 -0
  7. kubectl_mcp_tool/__init__.py +21 -0
  8. kubectl_mcp_tool/__main__.py +46 -0
  9. kubectl_mcp_tool/auth/__init__.py +13 -0
  10. kubectl_mcp_tool/auth/config.py +71 -0
  11. kubectl_mcp_tool/auth/scopes.py +148 -0
  12. kubectl_mcp_tool/auth/verifier.py +82 -0
  13. kubectl_mcp_tool/cli/__init__.py +9 -0
  14. kubectl_mcp_tool/cli/__main__.py +10 -0
  15. kubectl_mcp_tool/cli/cli.py +111 -0
  16. kubectl_mcp_tool/diagnostics.py +355 -0
  17. kubectl_mcp_tool/k8s_config.py +289 -0
  18. kubectl_mcp_tool/mcp_server.py +530 -0
  19. kubectl_mcp_tool/prompts/__init__.py +5 -0
  20. kubectl_mcp_tool/prompts/prompts.py +823 -0
  21. kubectl_mcp_tool/resources/__init__.py +5 -0
  22. kubectl_mcp_tool/resources/resources.py +305 -0
  23. kubectl_mcp_tool/tools/__init__.py +28 -0
  24. kubectl_mcp_tool/tools/browser.py +371 -0
  25. kubectl_mcp_tool/tools/cluster.py +315 -0
  26. kubectl_mcp_tool/tools/core.py +421 -0
  27. kubectl_mcp_tool/tools/cost.py +680 -0
  28. kubectl_mcp_tool/tools/deployments.py +381 -0
  29. kubectl_mcp_tool/tools/diagnostics.py +174 -0
  30. kubectl_mcp_tool/tools/helm.py +1561 -0
  31. kubectl_mcp_tool/tools/networking.py +296 -0
  32. kubectl_mcp_tool/tools/operations.py +501 -0
  33. kubectl_mcp_tool/tools/pods.py +582 -0
  34. kubectl_mcp_tool/tools/security.py +333 -0
  35. kubectl_mcp_tool/tools/storage.py +133 -0
  36. kubectl_mcp_tool/utils/__init__.py +17 -0
  37. kubectl_mcp_tool/utils/helpers.py +80 -0
  38. tests/__init__.py +9 -0
  39. tests/conftest.py +379 -0
  40. tests/test_auth.py +256 -0
  41. tests/test_browser.py +349 -0
  42. tests/test_prompts.py +536 -0
  43. tests/test_resources.py +343 -0
  44. tests/test_server.py +384 -0
  45. tests/test_tools.py +659 -0
tests/conftest.py ADDED
@@ -0,0 +1,379 @@
1
+ """
2
+ Pytest configuration and shared fixtures for kubectl-mcp-server tests.
3
+ """
4
+
5
+ import pytest
6
+ import json
7
+ import sys
8
+ from unittest.mock import Mock, MagicMock, patch
9
+ from typing import Dict, Any, List
10
+ from datetime import datetime
11
+
12
+
13
+ @pytest.fixture
14
+ def mock_kube_config():
15
+ """Mock kubernetes config loading."""
16
+ with patch("kubernetes.config.load_kube_config") as mock:
17
+ mock.return_value = None
18
+ yield mock
19
+
20
+
21
+ @pytest.fixture
22
+ def mock_kube_contexts():
23
+ """Mock kubernetes contexts."""
24
+ contexts = [
25
+ {
26
+ "name": "minikube",
27
+ "context": {
28
+ "cluster": "minikube",
29
+ "user": "minikube",
30
+ "namespace": "default"
31
+ }
32
+ },
33
+ {
34
+ "name": "production",
35
+ "context": {
36
+ "cluster": "prod-cluster",
37
+ "user": "admin",
38
+ "namespace": "production"
39
+ }
40
+ }
41
+ ]
42
+ active_context = contexts[0]
43
+
44
+ with patch("kubernetes.config.list_kube_config_contexts") as mock:
45
+ mock.return_value = (contexts, active_context)
46
+ yield mock
47
+
48
+
49
+ @pytest.fixture
50
+ def mock_pod():
51
+ """Create a mock Pod object."""
52
+ pod = MagicMock()
53
+ pod.metadata.name = "test-pod"
54
+ pod.metadata.namespace = "default"
55
+ pod.metadata.labels = {"app": "test"}
56
+ pod.metadata.creation_timestamp = datetime.now()
57
+ pod.status.phase = "Running"
58
+ pod.status.pod_ip = "10.0.0.1"
59
+ pod.status.conditions = []
60
+ pod.spec.containers = [MagicMock(name="container1", image="nginx:latest")]
61
+ pod.spec.node_name = "node-1"
62
+ return pod
63
+
64
+
65
+ @pytest.fixture
66
+ def mock_deployment():
67
+ """Create a mock Deployment object."""
68
+ deployment = MagicMock()
69
+ deployment.metadata.name = "test-deployment"
70
+ deployment.metadata.namespace = "default"
71
+ deployment.metadata.labels = {"app": "test"}
72
+ deployment.metadata.creation_timestamp = datetime.now()
73
+ deployment.spec.replicas = 3
74
+ deployment.status.ready_replicas = 3
75
+ deployment.status.available_replicas = 3
76
+ deployment.status.replicas = 3
77
+ return deployment
78
+
79
+
80
+ @pytest.fixture
81
+ def mock_service():
82
+ """Create a mock Service object."""
83
+ service = MagicMock()
84
+ service.metadata.name = "test-service"
85
+ service.metadata.namespace = "default"
86
+ service.spec.type = "ClusterIP"
87
+ service.spec.cluster_ip = "10.96.0.1"
88
+ service.spec.ports = [MagicMock(port=80, target_port=8080, protocol="TCP")]
89
+ return service
90
+
91
+
92
+ @pytest.fixture
93
+ def mock_node():
94
+ """Create a mock Node object."""
95
+ node = MagicMock()
96
+ node.metadata.name = "test-node"
97
+ node.metadata.labels = {"node-role.kubernetes.io/control-plane": ""}
98
+ node.status.conditions = [MagicMock(type="Ready", status="True")]
99
+ node.status.node_info.kubelet_version = "v1.28.0"
100
+ node.status.node_info.os_image = "Ubuntu 22.04"
101
+ node.status.node_info.architecture = "amd64"
102
+ node.status.capacity = {"cpu": "4", "memory": "8Gi", "pods": "110"}
103
+ node.status.allocatable = {"cpu": "3800m", "memory": "7Gi", "pods": "100"}
104
+ return node
105
+
106
+
107
+ @pytest.fixture
108
+ def mock_namespace():
109
+ """Create a mock Namespace object."""
110
+ namespace = MagicMock()
111
+ namespace.metadata.name = "test-namespace"
112
+ namespace.metadata.labels = {"env": "test"}
113
+ namespace.metadata.creation_timestamp = datetime.now()
114
+ namespace.status.phase = "Active"
115
+ return namespace
116
+
117
+
118
+ @pytest.fixture
119
+ def mock_configmap():
120
+ """Create a mock ConfigMap object."""
121
+ configmap = MagicMock()
122
+ configmap.metadata.name = "test-configmap"
123
+ configmap.metadata.namespace = "default"
124
+ configmap.data = {"key1": "value1", "key2": "value2"}
125
+ return configmap
126
+
127
+
128
+ @pytest.fixture
129
+ def mock_secret():
130
+ """Create a mock Secret object."""
131
+ secret = MagicMock()
132
+ secret.metadata.name = "test-secret"
133
+ secret.metadata.namespace = "default"
134
+ secret.type = "Opaque"
135
+ secret.data = {"password": "c2VjcmV0"} # base64 encoded
136
+ return secret
137
+
138
+
139
+ @pytest.fixture
140
+ def mock_core_v1_api(mock_pod, mock_service, mock_namespace, mock_configmap, mock_secret, mock_node):
141
+ """Mock CoreV1Api."""
142
+ api = MagicMock()
143
+
144
+ # Pod methods
145
+ api.list_namespaced_pod.return_value.items = [mock_pod]
146
+ api.list_pod_for_all_namespaces.return_value.items = [mock_pod]
147
+ api.read_namespaced_pod.return_value = mock_pod
148
+ api.read_namespaced_pod_log.return_value = "Test log output"
149
+
150
+ # Service methods
151
+ api.list_namespaced_service.return_value.items = [mock_service]
152
+ api.list_service_for_all_namespaces.return_value.items = [mock_service]
153
+ api.read_namespaced_service.return_value = mock_service
154
+
155
+ # Namespace methods
156
+ api.list_namespace.return_value.items = [mock_namespace]
157
+ api.read_namespace.return_value = mock_namespace
158
+ api.create_namespace.return_value = mock_namespace
159
+
160
+ # ConfigMap methods
161
+ api.list_namespaced_config_map.return_value.items = [mock_configmap]
162
+ api.read_namespaced_config_map.return_value = mock_configmap
163
+
164
+ # Secret methods
165
+ api.list_namespaced_secret.return_value.items = [mock_secret]
166
+ api.read_namespaced_secret.return_value = mock_secret
167
+
168
+ # Node methods
169
+ api.list_node.return_value.items = [mock_node]
170
+ api.read_node.return_value = mock_node
171
+
172
+ # Events
173
+ api.list_namespaced_event.return_value.items = []
174
+
175
+ return api
176
+
177
+
178
+ @pytest.fixture
179
+ def mock_apps_v1_api(mock_deployment):
180
+ """Mock AppsV1Api."""
181
+ api = MagicMock()
182
+
183
+ api.list_namespaced_deployment.return_value.items = [mock_deployment]
184
+ api.list_deployment_for_all_namespaces.return_value.items = [mock_deployment]
185
+ api.read_namespaced_deployment.return_value = mock_deployment
186
+ api.read_namespaced_deployment_scale.return_value.spec.replicas = 3
187
+
188
+ api.list_namespaced_stateful_set.return_value.items = []
189
+ api.list_namespaced_daemon_set.return_value.items = []
190
+ api.list_namespaced_replica_set.return_value.items = []
191
+
192
+ return api
193
+
194
+
195
+ @pytest.fixture
196
+ def mock_networking_v1_api():
197
+ """Mock NetworkingV1Api."""
198
+ api = MagicMock()
199
+
200
+ ingress = MagicMock()
201
+ ingress.metadata.name = "test-ingress"
202
+ ingress.metadata.namespace = "default"
203
+ api.list_namespaced_ingress.return_value.items = [ingress]
204
+ api.read_namespaced_ingress.return_value = ingress
205
+
206
+ network_policy = MagicMock()
207
+ network_policy.metadata.name = "test-policy"
208
+ api.list_namespaced_network_policy.return_value.items = [network_policy]
209
+
210
+ return api
211
+
212
+
213
+ @pytest.fixture
214
+ def mock_batch_v1_api():
215
+ """Mock BatchV1Api."""
216
+ api = MagicMock()
217
+
218
+ job = MagicMock()
219
+ job.metadata.name = "test-job"
220
+ job.metadata.namespace = "default"
221
+ api.list_namespaced_job.return_value.items = [job]
222
+
223
+ cronjob = MagicMock()
224
+ cronjob.metadata.name = "test-cronjob"
225
+ api.list_namespaced_cron_job.return_value.items = [cronjob]
226
+
227
+ return api
228
+
229
+
230
+ @pytest.fixture
231
+ def mock_version_api():
232
+ """Mock VersionApi."""
233
+ api = MagicMock()
234
+
235
+ version_info = MagicMock()
236
+ version_info.git_version = "v1.28.0"
237
+ version_info.major = "1"
238
+ version_info.minor = "28"
239
+ version_info.platform = "linux/amd64"
240
+ version_info.build_date = "2024-01-01T00:00:00Z"
241
+ version_info.go_version = "go1.21.0"
242
+ version_info.compiler = "gc"
243
+
244
+ api.get_code.return_value = version_info
245
+ return api
246
+
247
+
248
+ @pytest.fixture
249
+ def mock_kubectl_subprocess():
250
+ """Mock subprocess calls for kubectl."""
251
+ with patch("subprocess.run") as mock_run:
252
+ mock_run.return_value = MagicMock(
253
+ returncode=0,
254
+ stdout="Command executed successfully",
255
+ stderr=""
256
+ )
257
+ yield mock_run
258
+
259
+
260
+ @pytest.fixture
261
+ def mock_helm_subprocess():
262
+ """Mock subprocess calls for helm."""
263
+ with patch("subprocess.run") as mock_run:
264
+ mock_run.return_value = MagicMock(
265
+ returncode=0,
266
+ stdout=json.dumps([
267
+ {"name": "test-release", "namespace": "default", "status": "deployed"}
268
+ ]),
269
+ stderr=""
270
+ )
271
+ yield mock_run
272
+
273
+
274
+ @pytest.fixture
275
+ def mcp_server(mock_kube_config):
276
+ """Create an MCPServer instance with mocked dependencies."""
277
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies") as mock_deps:
278
+ mock_deps.return_value = True
279
+ from kubectl_mcp_tool.mcp_server import MCPServer
280
+ server = MCPServer(name="test-server")
281
+ yield server
282
+
283
+
284
+ @pytest.fixture
285
+ def mock_all_kubernetes_apis(
286
+ mock_kube_config,
287
+ mock_kube_contexts,
288
+ mock_core_v1_api,
289
+ mock_apps_v1_api,
290
+ mock_networking_v1_api,
291
+ mock_batch_v1_api,
292
+ mock_version_api
293
+ ):
294
+ """Patch all Kubernetes API clients."""
295
+ with patch("kubernetes.client.CoreV1Api", return_value=mock_core_v1_api), \
296
+ patch("kubernetes.client.AppsV1Api", return_value=mock_apps_v1_api), \
297
+ patch("kubernetes.client.NetworkingV1Api", return_value=mock_networking_v1_api), \
298
+ patch("kubernetes.client.BatchV1Api", return_value=mock_batch_v1_api), \
299
+ patch("kubernetes.client.VersionApi", return_value=mock_version_api), \
300
+ patch("kubernetes.client.ApiClient") as mock_api_client:
301
+
302
+ mock_api_client.return_value.sanitize_for_serialization.return_value = {}
303
+
304
+ yield {
305
+ "core_v1": mock_core_v1_api,
306
+ "apps_v1": mock_apps_v1_api,
307
+ "networking_v1": mock_networking_v1_api,
308
+ "batch_v1": mock_batch_v1_api,
309
+ "version": mock_version_api,
310
+ }
311
+
312
+
313
+ class MockResponse:
314
+ """Mock HTTP response for testing."""
315
+ def __init__(self, json_data: Dict, status_code: int = 200):
316
+ self.json_data = json_data
317
+ self.status_code = status_code
318
+ self.text = json.dumps(json_data)
319
+
320
+ def json(self):
321
+ return self.json_data
322
+
323
+
324
+ @pytest.fixture
325
+ def sample_pod_yaml():
326
+ """Sample pod YAML for testing."""
327
+ return """
328
+ apiVersion: v1
329
+ kind: Pod
330
+ metadata:
331
+ name: test-pod
332
+ namespace: default
333
+ spec:
334
+ containers:
335
+ - name: nginx
336
+ image: nginx:latest
337
+ ports:
338
+ - containerPort: 80
339
+ """
340
+
341
+
342
+ @pytest.fixture
343
+ def sample_deployment_yaml():
344
+ """Sample deployment YAML for testing."""
345
+ return """
346
+ apiVersion: apps/v1
347
+ kind: Deployment
348
+ metadata:
349
+ name: test-deployment
350
+ namespace: default
351
+ spec:
352
+ replicas: 3
353
+ selector:
354
+ matchLabels:
355
+ app: test
356
+ template:
357
+ metadata:
358
+ labels:
359
+ app: test
360
+ spec:
361
+ containers:
362
+ - name: nginx
363
+ image: nginx:latest
364
+ ports:
365
+ - containerPort: 80
366
+ """
367
+
368
+
369
+ def pytest_configure(config):
370
+ """Configure pytest markers."""
371
+ config.addinivalue_line(
372
+ "markers", "unit: mark test as a unit test"
373
+ )
374
+ config.addinivalue_line(
375
+ "markers", "integration: mark test as an integration test"
376
+ )
377
+ config.addinivalue_line(
378
+ "markers", "slow: mark test as slow running"
379
+ )
tests/test_auth.py ADDED
@@ -0,0 +1,256 @@
1
+ """Tests for the MCP Authorization module."""
2
+
3
+ import os
4
+ import pytest
5
+ from unittest.mock import patch, MagicMock
6
+
7
+
8
+ class TestAuthConfig:
9
+ """Tests for authentication configuration."""
10
+
11
+ def test_auth_disabled_by_default(self):
12
+ """Test that auth is disabled by default."""
13
+ with patch.dict(os.environ, {}, clear=True):
14
+ from kubectl_mcp_tool.auth.config import get_auth_config
15
+ config = get_auth_config()
16
+ assert config.enabled is False
17
+
18
+ def test_auth_enabled_via_env(self):
19
+ """Test enabling auth via environment variable."""
20
+ with patch.dict(os.environ, {"MCP_AUTH_ENABLED": "true"}, clear=True):
21
+ from kubectl_mcp_tool.auth.config import get_auth_config
22
+ config = get_auth_config()
23
+ assert config.enabled is True
24
+
25
+ def test_auth_issuer_from_env(self):
26
+ """Test issuer URL from environment."""
27
+ with patch.dict(os.environ, {
28
+ "MCP_AUTH_ENABLED": "true",
29
+ "MCP_AUTH_ISSUER": "https://auth.example.com"
30
+ }, clear=True):
31
+ from kubectl_mcp_tool.auth.config import get_auth_config
32
+ config = get_auth_config()
33
+ assert config.issuer_url == "https://auth.example.com"
34
+
35
+ def test_auth_audience_default(self):
36
+ """Test default audience value."""
37
+ with patch.dict(os.environ, {}, clear=True):
38
+ from kubectl_mcp_tool.auth.config import get_auth_config
39
+ config = get_auth_config()
40
+ assert config.audience == "kubectl-mcp-server"
41
+
42
+ def test_auth_audience_custom(self):
43
+ """Test custom audience value."""
44
+ with patch.dict(os.environ, {
45
+ "MCP_AUTH_AUDIENCE": "custom-audience"
46
+ }, clear=True):
47
+ from kubectl_mcp_tool.auth.config import get_auth_config
48
+ config = get_auth_config()
49
+ assert config.audience == "custom-audience"
50
+
51
+ def test_auth_required_scopes_default(self):
52
+ """Test default required scopes."""
53
+ with patch.dict(os.environ, {}, clear=True):
54
+ from kubectl_mcp_tool.auth.config import get_auth_config
55
+ config = get_auth_config()
56
+ assert "mcp:tools" in config.required_scopes
57
+
58
+ def test_auth_required_scopes_custom(self):
59
+ """Test custom required scopes."""
60
+ with patch.dict(os.environ, {
61
+ "MCP_AUTH_REQUIRED_SCOPES": "mcp:read,mcp:write"
62
+ }, clear=True):
63
+ from kubectl_mcp_tool.auth.config import get_auth_config
64
+ config = get_auth_config()
65
+ assert "mcp:read" in config.required_scopes
66
+ assert "mcp:write" in config.required_scopes
67
+
68
+ def test_effective_jwks_uri_derived(self):
69
+ """Test JWKS URI is derived from issuer."""
70
+ with patch.dict(os.environ, {
71
+ "MCP_AUTH_ISSUER": "https://auth.example.com"
72
+ }, clear=True):
73
+ from kubectl_mcp_tool.auth.config import get_auth_config
74
+ config = get_auth_config()
75
+ assert config.effective_jwks_uri == "https://auth.example.com/.well-known/jwks.json"
76
+
77
+ def test_effective_jwks_uri_explicit(self):
78
+ """Test explicit JWKS URI takes precedence."""
79
+ with patch.dict(os.environ, {
80
+ "MCP_AUTH_ISSUER": "https://auth.example.com",
81
+ "MCP_AUTH_JWKS_URI": "https://custom.example.com/jwks"
82
+ }, clear=True):
83
+ from kubectl_mcp_tool.auth.config import get_auth_config
84
+ config = get_auth_config()
85
+ assert config.effective_jwks_uri == "https://custom.example.com/jwks"
86
+
87
+ def test_config_validation_disabled(self):
88
+ """Test validation passes when auth is disabled."""
89
+ with patch.dict(os.environ, {}, clear=True):
90
+ from kubectl_mcp_tool.auth.config import get_auth_config
91
+ config = get_auth_config()
92
+ assert config.validate() is True
93
+
94
+ def test_config_validation_enabled_without_issuer(self):
95
+ """Test validation fails when enabled without issuer."""
96
+ with patch.dict(os.environ, {
97
+ "MCP_AUTH_ENABLED": "true"
98
+ }, clear=True):
99
+ from kubectl_mcp_tool.auth.config import get_auth_config
100
+ config = get_auth_config()
101
+ assert config.validate() is False
102
+
103
+ def test_config_validation_enabled_with_issuer(self):
104
+ """Test validation passes when properly configured."""
105
+ with patch.dict(os.environ, {
106
+ "MCP_AUTH_ENABLED": "true",
107
+ "MCP_AUTH_ISSUER": "https://auth.example.com"
108
+ }, clear=True):
109
+ from kubectl_mcp_tool.auth.config import get_auth_config
110
+ config = get_auth_config()
111
+ assert config.validate() is True
112
+
113
+
114
+ class TestMCPScopes:
115
+ """Tests for MCP scope definitions."""
116
+
117
+ def test_all_scopes_returns_list(self):
118
+ """Test all_scopes returns a list."""
119
+ from kubectl_mcp_tool.auth.scopes import MCPScopes
120
+ scopes = MCPScopes.all_scopes()
121
+ assert isinstance(scopes, list)
122
+ assert len(scopes) > 0
123
+
124
+ def test_scope_values(self):
125
+ """Test scope enum values."""
126
+ from kubectl_mcp_tool.auth.scopes import MCPScopes
127
+ assert MCPScopes.READ.value == "mcp:read"
128
+ assert MCPScopes.WRITE.value == "mcp:write"
129
+ assert MCPScopes.ADMIN.value == "mcp:admin"
130
+ assert MCPScopes.TOOLS.value == "mcp:tools"
131
+ assert MCPScopes.HELM.value == "mcp:helm"
132
+
133
+ def test_read_scopes(self):
134
+ """Test read-only scopes."""
135
+ from kubectl_mcp_tool.auth.scopes import MCPScopes
136
+ scopes = MCPScopes.read_scopes()
137
+ assert "mcp:read" in scopes
138
+ assert "mcp:diagnostics" in scopes
139
+
140
+ def test_admin_scopes_includes_all(self):
141
+ """Test admin scopes include all scopes."""
142
+ from kubectl_mcp_tool.auth.scopes import MCPScopes
143
+ admin_scopes = MCPScopes.admin_scopes()
144
+ all_scopes = MCPScopes.all_scopes()
145
+ assert set(admin_scopes) == set(all_scopes)
146
+
147
+
148
+ class TestToolScopes:
149
+ """Tests for tool-to-scope mappings."""
150
+
151
+ def test_get_required_scopes_read_tool(self):
152
+ """Test read tools require read scope."""
153
+ from kubectl_mcp_tool.auth.scopes import get_required_scopes, MCPScopes
154
+ scopes = get_required_scopes("get_pods")
155
+ assert MCPScopes.READ.value in scopes
156
+
157
+ def test_get_required_scopes_write_tool(self):
158
+ """Test write tools require write scope."""
159
+ from kubectl_mcp_tool.auth.scopes import get_required_scopes, MCPScopes
160
+ scopes = get_required_scopes("scale_deployment")
161
+ assert MCPScopes.WRITE.value in scopes
162
+
163
+ def test_get_required_scopes_admin_tool(self):
164
+ """Test admin tools require admin scope."""
165
+ from kubectl_mcp_tool.auth.scopes import get_required_scopes, MCPScopes
166
+ scopes = get_required_scopes("drain_node")
167
+ assert MCPScopes.ADMIN.value in scopes
168
+
169
+ def test_get_required_scopes_helm_tool(self):
170
+ """Test helm tools require helm scope."""
171
+ from kubectl_mcp_tool.auth.scopes import get_required_scopes, MCPScopes
172
+ scopes = get_required_scopes("helm_list_releases")
173
+ assert MCPScopes.HELM.value in scopes
174
+
175
+ def test_get_required_scopes_unknown_tool(self):
176
+ """Test unknown tools require default scope."""
177
+ from kubectl_mcp_tool.auth.scopes import get_required_scopes, MCPScopes
178
+ scopes = get_required_scopes("unknown_tool_xyz")
179
+ assert MCPScopes.TOOLS.value in scopes
180
+
181
+ def test_has_required_scopes_with_tools_scope(self):
182
+ """Test mcp:tools grants access to all tools."""
183
+ from kubectl_mcp_tool.auth.scopes import has_required_scopes, MCPScopes
184
+ token_scopes = {MCPScopes.TOOLS.value}
185
+ assert has_required_scopes(token_scopes, "get_pods") is True
186
+ assert has_required_scopes(token_scopes, "drain_node") is True
187
+
188
+ def test_has_required_scopes_with_admin_scope(self):
189
+ """Test mcp:admin grants access to all tools."""
190
+ from kubectl_mcp_tool.auth.scopes import has_required_scopes, MCPScopes
191
+ token_scopes = {MCPScopes.ADMIN.value}
192
+ assert has_required_scopes(token_scopes, "get_pods") is True
193
+ assert has_required_scopes(token_scopes, "drain_node") is True
194
+
195
+ def test_has_required_scopes_read_only(self):
196
+ """Test read scope grants access to read tools only."""
197
+ from kubectl_mcp_tool.auth.scopes import has_required_scopes, MCPScopes
198
+ token_scopes = {MCPScopes.READ.value}
199
+ assert has_required_scopes(token_scopes, "get_pods") is True
200
+ assert has_required_scopes(token_scopes, "scale_deployment") is False
201
+
202
+ def test_has_required_scopes_write_not_admin(self):
203
+ """Test write scope doesn't grant admin access."""
204
+ from kubectl_mcp_tool.auth.scopes import has_required_scopes, MCPScopes
205
+ token_scopes = {MCPScopes.WRITE.value}
206
+ assert has_required_scopes(token_scopes, "scale_deployment") is True
207
+ assert has_required_scopes(token_scopes, "drain_node") is False
208
+
209
+
210
+ class TestAuthVerifier:
211
+ """Tests for authentication verifier creation."""
212
+
213
+ def test_create_verifier_auth_disabled(self):
214
+ """Test verifier is None when auth disabled."""
215
+ with patch.dict(os.environ, {}, clear=True):
216
+ from kubectl_mcp_tool.auth import get_auth_config, create_auth_verifier
217
+ config = get_auth_config()
218
+ verifier = create_auth_verifier(config)
219
+ assert verifier is None
220
+
221
+ def test_create_verifier_missing_fastmcp_auth(self):
222
+ """Test graceful handling when FastMCP auth not available."""
223
+ with patch.dict(os.environ, {
224
+ "MCP_AUTH_ENABLED": "true",
225
+ "MCP_AUTH_ISSUER": "https://auth.example.com"
226
+ }, clear=True):
227
+ from kubectl_mcp_tool.auth import get_auth_config, create_auth_verifier
228
+ config = get_auth_config()
229
+
230
+ # Mock the import to fail
231
+ with patch.dict('sys.modules', {'fastmcp.server.auth': None}):
232
+ verifier = create_auth_verifier(config)
233
+ # Should return None gracefully when auth module not available
234
+ assert verifier is None
235
+
236
+
237
+ class TestMCPServerAuth:
238
+ """Tests for MCP server with authentication."""
239
+
240
+ def test_server_initializes_without_auth(self):
241
+ """Test server initializes with auth disabled."""
242
+ with patch.dict(os.environ, {}, clear=True):
243
+ from kubectl_mcp_tool.mcp_server import MCPServer
244
+ server = MCPServer(name="test-server")
245
+ assert server.auth_config.enabled is False
246
+
247
+ def test_server_with_auth_enabled(self):
248
+ """Test server with auth enabled loads config."""
249
+ with patch.dict(os.environ, {
250
+ "MCP_AUTH_ENABLED": "true",
251
+ "MCP_AUTH_ISSUER": "https://auth.example.com"
252
+ }, clear=True):
253
+ from kubectl_mcp_tool.mcp_server import MCPServer
254
+ server = MCPServer(name="test-server")
255
+ assert server.auth_config.enabled is True
256
+ assert server.auth_config.issuer_url == "https://auth.example.com"