kubectl-mcp-server 1.23.0__py3-none-any.whl → 1.23.1__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.23.0.dist-info → kubectl_mcp_server-1.23.1.dist-info}/METADATA +1 -1
- {kubectl_mcp_server-1.23.0.dist-info → kubectl_mcp_server-1.23.1.dist-info}/RECORD +28 -27
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/mcp_server.py +8 -24
- kubectl_mcp_tool/providers.py +0 -1
- kubectl_mcp_tool/safety.py +1 -53
- kubectl_mcp_tool/tools/_cli_utils.py +75 -0
- kubectl_mcp_tool/tools/cluster.py +2 -8
- kubectl_mcp_tool/tools/core.py +2 -8
- kubectl_mcp_tool/tools/cost.py +5 -12
- kubectl_mcp_tool/tools/deployments.py +2 -8
- kubectl_mcp_tool/tools/diagnostics.py +2 -9
- kubectl_mcp_tool/tools/helm.py +44 -44
- kubectl_mcp_tool/tools/kind.py +4 -8
- kubectl_mcp_tool/tools/networking.py +18 -14
- kubectl_mcp_tool/tools/operations.py +39 -13
- kubectl_mcp_tool/tools/pods.py +9 -12
- kubectl_mcp_tool/tools/vind.py +2 -7
- tests/test_browser.py +2 -3
- tests/test_ecosystem.py +2 -4
- tests/test_mcp_integration.py +4 -26
- tests/test_safety.py +0 -120
- tests/test_server.py +4 -5
- tests/test_tools.py +48 -40
- {kubectl_mcp_server-1.23.0.dist-info → kubectl_mcp_server-1.23.1.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.23.0.dist-info → kubectl_mcp_server-1.23.1.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.23.0.dist-info → kubectl_mcp_server-1.23.1.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.23.0.dist-info → kubectl_mcp_server-1.23.1.dist-info}/top_level.txt +0 -0
|
@@ -4,19 +4,12 @@ from typing import Any, Dict, List, Optional
|
|
|
4
4
|
|
|
5
5
|
from mcp.types import ToolAnnotations
|
|
6
6
|
|
|
7
|
-
from ..k8s_config import get_k8s_client, get_networking_client
|
|
7
|
+
from ..k8s_config import get_k8s_client, get_networking_client, _get_kubectl_context_args
|
|
8
8
|
|
|
9
9
|
logger = logging.getLogger("mcp-server")
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def
|
|
13
|
-
"""Get kubectl context arguments if context is specified."""
|
|
14
|
-
if context:
|
|
15
|
-
return ["--context", context]
|
|
16
|
-
return []
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def register_networking_tools(server, non_destructive: bool):
|
|
12
|
+
def register_networking_tools(server: "FastMCP", non_destructive: bool):
|
|
20
13
|
"""Register networking-related tools."""
|
|
21
14
|
|
|
22
15
|
@server.tool(
|
|
@@ -271,7 +264,7 @@ def register_networking_tools(server, non_destructive: bool):
|
|
|
271
264
|
# Get endpoints
|
|
272
265
|
try:
|
|
273
266
|
endpoints = v1.read_namespaced_endpoints(service_name, namespace)
|
|
274
|
-
except:
|
|
267
|
+
except Exception:
|
|
275
268
|
endpoints = None
|
|
276
269
|
|
|
277
270
|
# Get pods matching selector
|
|
@@ -354,11 +347,22 @@ def register_networking_tools(server, non_destructive: bool):
|
|
|
354
347
|
namespace: Namespace of the pod
|
|
355
348
|
context: Kubernetes context to use (uses current context if not specified)
|
|
356
349
|
"""
|
|
350
|
+
if non_destructive:
|
|
351
|
+
return {
|
|
352
|
+
"success": False,
|
|
353
|
+
"error": "port_forward is not allowed in non-destructive mode"
|
|
354
|
+
}
|
|
357
355
|
try:
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
356
|
+
cmd = ["kubectl"] + _get_kubectl_context_args(context) + [
|
|
357
|
+
"port-forward", pod_name,
|
|
358
|
+
f"{int(local_port)}:{int(pod_port)}",
|
|
359
|
+
"-n", namespace
|
|
360
|
+
]
|
|
361
|
+
subprocess.Popen(
|
|
362
|
+
cmd,
|
|
363
|
+
stdout=subprocess.DEVNULL,
|
|
364
|
+
stderr=subprocess.DEVNULL,
|
|
365
|
+
)
|
|
362
366
|
return {
|
|
363
367
|
"success": True,
|
|
364
368
|
"context": context or "current",
|
|
@@ -7,17 +7,12 @@ from typing import Any, Dict, List, Optional
|
|
|
7
7
|
|
|
8
8
|
from mcp.types import ToolAnnotations
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
from kubectl_mcp_tool.k8s_config import _get_kubectl_context_args
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
"""Get kubectl context arguments if context is specified."""
|
|
15
|
-
if context:
|
|
16
|
-
return ["--context", context]
|
|
17
|
-
return []
|
|
12
|
+
logger = logging.getLogger("mcp-server")
|
|
18
13
|
|
|
19
14
|
|
|
20
|
-
def register_operations_tools(server, non_destructive: bool):
|
|
15
|
+
def register_operations_tools(server: "FastMCP", non_destructive: bool):
|
|
21
16
|
"""Register kubectl operations tools (apply, describe, patch, etc.)."""
|
|
22
17
|
|
|
23
18
|
def check_destructive():
|
|
@@ -43,6 +38,7 @@ def register_operations_tools(server, non_destructive: bool):
|
|
|
43
38
|
blocked = check_destructive()
|
|
44
39
|
if blocked:
|
|
45
40
|
return blocked
|
|
41
|
+
temp_path = None
|
|
46
42
|
try:
|
|
47
43
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
48
44
|
f.write(manifest)
|
|
@@ -51,8 +47,6 @@ def register_operations_tools(server, non_destructive: bool):
|
|
|
51
47
|
cmd = ["kubectl"] + _get_kubectl_context_args(context) + ["apply", "-f", temp_path, "-n", namespace]
|
|
52
48
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
53
49
|
|
|
54
|
-
os.unlink(temp_path)
|
|
55
|
-
|
|
56
50
|
if result.returncode == 0:
|
|
57
51
|
return {"success": True, "context": context or "current", "output": result.stdout.strip()}
|
|
58
52
|
else:
|
|
@@ -60,6 +54,12 @@ def register_operations_tools(server, non_destructive: bool):
|
|
|
60
54
|
except Exception as e:
|
|
61
55
|
logger.error(f"Error applying manifest: {e}")
|
|
62
56
|
return {"success": False, "error": str(e)}
|
|
57
|
+
finally:
|
|
58
|
+
if temp_path:
|
|
59
|
+
try:
|
|
60
|
+
os.unlink(temp_path)
|
|
61
|
+
except OSError:
|
|
62
|
+
pass
|
|
63
63
|
|
|
64
64
|
@server.tool(
|
|
65
65
|
annotations=ToolAnnotations(
|
|
@@ -105,7 +105,12 @@ def register_operations_tools(server, non_destructive: bool):
|
|
|
105
105
|
# Security: validate command starts with allowed operations
|
|
106
106
|
allowed_prefixes = [
|
|
107
107
|
"get", "describe", "logs", "top", "explain", "api-resources",
|
|
108
|
-
"
|
|
108
|
+
"version", "cluster-info"
|
|
109
|
+
]
|
|
110
|
+
allowed_auth_subcommands = ["can-i"]
|
|
111
|
+
allowed_config_subcommands = [
|
|
112
|
+
"config view", "config current-context", "config get-contexts",
|
|
113
|
+
"config get-clusters", "config use-context"
|
|
109
114
|
]
|
|
110
115
|
cmd_parts = shlex.split(command)
|
|
111
116
|
if not cmd_parts:
|
|
@@ -115,10 +120,31 @@ def register_operations_tools(server, non_destructive: bool):
|
|
|
115
120
|
if cmd_parts[0] == "kubectl":
|
|
116
121
|
cmd_parts = cmd_parts[1:]
|
|
117
122
|
|
|
118
|
-
if not cmd_parts
|
|
123
|
+
if not cmd_parts:
|
|
124
|
+
return {"success": False, "error": "Empty command"}
|
|
125
|
+
|
|
126
|
+
if cmd_parts[0] == "config":
|
|
127
|
+
cmd_prefix = " ".join(cmd_parts[:2])
|
|
128
|
+
if cmd_prefix not in allowed_config_subcommands:
|
|
129
|
+
return {
|
|
130
|
+
"success": False,
|
|
131
|
+
"error": f"Config subcommand not allowed. Allowed: {', '.join(allowed_config_subcommands)}"
|
|
132
|
+
}
|
|
133
|
+
if "--raw" in cmd_parts:
|
|
134
|
+
return {
|
|
135
|
+
"success": False,
|
|
136
|
+
"error": "config view --raw is not allowed (may expose secrets)"
|
|
137
|
+
}
|
|
138
|
+
elif cmd_parts[0] == "auth":
|
|
139
|
+
if len(cmd_parts) < 2 or cmd_parts[1] not in allowed_auth_subcommands:
|
|
140
|
+
return {
|
|
141
|
+
"success": False,
|
|
142
|
+
"error": f"Auth subcommand not allowed. Allowed: {', '.join(allowed_auth_subcommands)}"
|
|
143
|
+
}
|
|
144
|
+
elif cmd_parts[0] not in allowed_prefixes:
|
|
119
145
|
return {
|
|
120
146
|
"success": False,
|
|
121
|
-
"error": f"Command not allowed. Allowed: {', '.join(allowed_prefixes)}"
|
|
147
|
+
"error": f"Command not allowed. Allowed: {', '.join(allowed_prefixes + ['config', 'auth can-i'])}"
|
|
122
148
|
}
|
|
123
149
|
|
|
124
150
|
full_cmd = ["kubectl"] + _get_kubectl_context_args(context) + cmd_parts
|
kubectl_mcp_tool/tools/pods.py
CHANGED
|
@@ -8,24 +8,17 @@ import json
|
|
|
8
8
|
import logging
|
|
9
9
|
import shlex
|
|
10
10
|
import subprocess
|
|
11
|
-
from typing import Any,
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
12
|
|
|
13
13
|
from mcp.types import ToolAnnotations
|
|
14
14
|
|
|
15
|
-
from kubectl_mcp_tool.k8s_config import get_k8s_client
|
|
15
|
+
from kubectl_mcp_tool.k8s_config import get_k8s_client, _get_kubectl_context_args
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger("mcp-server")
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def _get_kubectl_context_args(context: str = "") -> List[str]:
|
|
21
|
-
"""Get kubectl context arguments."""
|
|
22
|
-
if context:
|
|
23
|
-
return ["--context", context]
|
|
24
|
-
return []
|
|
25
|
-
|
|
26
|
-
|
|
27
20
|
def register_pod_tools(
|
|
28
|
-
server,
|
|
21
|
+
server: "FastMCP",
|
|
29
22
|
non_destructive: bool
|
|
30
23
|
):
|
|
31
24
|
"""Register all Pod-related tools with the MCP server.
|
|
@@ -210,6 +203,8 @@ def register_pod_tools(
|
|
|
210
203
|
container: Container name (for multi-container pods)
|
|
211
204
|
context: Kubernetes context to use (uses current context if not specified)
|
|
212
205
|
"""
|
|
206
|
+
if non_destructive:
|
|
207
|
+
return {"success": False, "error": "Blocked: non-destructive mode"}
|
|
213
208
|
try:
|
|
214
209
|
cmd = ["kubectl"] + _get_kubectl_context_args(context) + [
|
|
215
210
|
"exec", pod_name, "-n", namespace
|
|
@@ -778,8 +773,10 @@ def register_pod_tools(
|
|
|
778
773
|
},
|
|
779
774
|
"evictedPods": evicted,
|
|
780
775
|
"recommendations": [
|
|
781
|
-
|
|
782
|
-
|
|
776
|
+
rec for rec in [
|
|
777
|
+
"DiskPressure: Clean up disk space or increase ephemeral-storage limits" if "DiskPressure" in by_reason else None,
|
|
778
|
+
"MemoryPressure: Increase memory limits or add more nodes" if "MemoryPressure" in by_reason else None
|
|
779
|
+
] if rec is not None
|
|
783
780
|
]
|
|
784
781
|
}
|
|
785
782
|
except Exception as e:
|
kubectl_mcp_tool/tools/vind.py
CHANGED
|
@@ -9,12 +9,7 @@ import json
|
|
|
9
9
|
import re
|
|
10
10
|
from typing import Dict, Any, List, Optional
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
from fastmcp import FastMCP
|
|
14
|
-
from fastmcp.tools import ToolAnnotations
|
|
15
|
-
except ImportError:
|
|
16
|
-
from mcp.server.fastmcp import FastMCP
|
|
17
|
-
from mcp.types import ToolAnnotations
|
|
12
|
+
from mcp.types import ToolAnnotations
|
|
18
13
|
|
|
19
14
|
|
|
20
15
|
def _vcluster_available() -> bool:
|
|
@@ -584,7 +579,7 @@ def vind_platform_start(
|
|
|
584
579
|
return result
|
|
585
580
|
|
|
586
581
|
|
|
587
|
-
def register_vind_tools(mcp: FastMCP, non_destructive: bool = False):
|
|
582
|
+
def register_vind_tools(mcp: "FastMCP", non_destructive: bool = False):
|
|
588
583
|
"""Register vind (vCluster in Docker) tools with the MCP server."""
|
|
589
584
|
|
|
590
585
|
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
tests/test_browser.py
CHANGED
|
@@ -501,11 +501,10 @@ 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 (224 + 26 = 250)
|
|
505
504
|
assert "browser_open" in tool_names
|
|
506
505
|
assert "browser_screenshot" in tool_names
|
|
507
|
-
assert "browser_connect_cdp" in tool_names
|
|
508
|
-
assert len(tools) ==
|
|
506
|
+
assert "browser_connect_cdp" in tool_names
|
|
507
|
+
assert len(tools) == 301, f"Expected 301 tools (275 + 26), got {len(tools)}"
|
|
509
508
|
|
|
510
509
|
|
|
511
510
|
import asyncio
|
tests/test_ecosystem.py
CHANGED
|
@@ -153,9 +153,8 @@ class TestGitOpsTools:
|
|
|
153
153
|
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
154
154
|
|
|
155
155
|
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
156
|
-
server = MCPServer(name="test",
|
|
156
|
+
server = MCPServer(name="test", disable_destructive=True)
|
|
157
157
|
|
|
158
|
-
# Server should initialize with non_destructive=True
|
|
159
158
|
assert server.non_destructive is True
|
|
160
159
|
|
|
161
160
|
|
|
@@ -263,9 +262,8 @@ class TestBackupTools:
|
|
|
263
262
|
from kubectl_mcp_tool.mcp_server import MCPServer
|
|
264
263
|
|
|
265
264
|
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
266
|
-
server = MCPServer(name="test",
|
|
265
|
+
server = MCPServer(name="test", disable_destructive=True)
|
|
267
266
|
|
|
268
|
-
# Server should initialize with non_destructive=True
|
|
269
267
|
assert server.non_destructive is True
|
|
270
268
|
|
|
271
269
|
|
tests/test_mcp_integration.py
CHANGED
|
@@ -205,7 +205,7 @@ class TestMCPServerSafety:
|
|
|
205
205
|
"""Test safety mode integration in MCP server."""
|
|
206
206
|
|
|
207
207
|
def test_safety_mode_info(self):
|
|
208
|
-
"""Test safety mode info
|
|
208
|
+
"""Test safety mode info for all modes."""
|
|
209
209
|
from kubectl_mcp_tool.safety import (
|
|
210
210
|
SafetyMode,
|
|
211
211
|
set_safety_mode,
|
|
@@ -223,29 +223,7 @@ class TestMCPServerSafety:
|
|
|
223
223
|
assert info["mode"] == "read_only"
|
|
224
224
|
assert len(info["blocked_operations"]) > 0
|
|
225
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
226
|
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
245
|
-
|
|
246
|
-
assert
|
|
247
|
-
assert "
|
|
248
|
-
|
|
249
|
-
# Non-destructive write should be allowed
|
|
250
|
-
allowed, reason = is_operation_allowed("create_deployment")
|
|
251
|
-
assert allowed is True
|
|
227
|
+
info = get_mode_info()
|
|
228
|
+
assert info["mode"] == "disable_destructive"
|
|
229
|
+
assert "delete_pod" in info["blocked_operations"]
|
tests/test_safety.py
CHANGED
|
@@ -5,8 +5,6 @@ from kubectl_mcp_tool.safety import (
|
|
|
5
5
|
SafetyMode,
|
|
6
6
|
get_safety_mode,
|
|
7
7
|
set_safety_mode,
|
|
8
|
-
is_operation_allowed,
|
|
9
|
-
check_safety_mode,
|
|
10
8
|
get_mode_info,
|
|
11
9
|
WRITE_OPERATIONS,
|
|
12
10
|
DESTRUCTIVE_OPERATIONS,
|
|
@@ -35,124 +33,6 @@ class TestSafetyMode:
|
|
|
35
33
|
assert get_safety_mode() == SafetyMode.DISABLE_DESTRUCTIVE
|
|
36
34
|
|
|
37
35
|
|
|
38
|
-
class TestOperationAllowed:
|
|
39
|
-
"""Test is_operation_allowed function."""
|
|
40
|
-
|
|
41
|
-
def setup_method(self):
|
|
42
|
-
"""Reset safety mode to NORMAL before each test."""
|
|
43
|
-
set_safety_mode(SafetyMode.NORMAL)
|
|
44
|
-
|
|
45
|
-
def test_all_operations_allowed_in_normal_mode(self):
|
|
46
|
-
"""Test that all operations are allowed in NORMAL mode."""
|
|
47
|
-
for op in WRITE_OPERATIONS | DESTRUCTIVE_OPERATIONS:
|
|
48
|
-
allowed, reason = is_operation_allowed(op)
|
|
49
|
-
assert allowed is True
|
|
50
|
-
assert reason == ""
|
|
51
|
-
|
|
52
|
-
def test_read_operations_allowed_in_all_modes(self):
|
|
53
|
-
"""Test that read operations are allowed in all modes."""
|
|
54
|
-
read_ops = ["get_pods", "list_namespaces", "describe_deployment", "get_logs"]
|
|
55
|
-
|
|
56
|
-
for mode in SafetyMode:
|
|
57
|
-
set_safety_mode(mode)
|
|
58
|
-
for op in read_ops:
|
|
59
|
-
allowed, reason = is_operation_allowed(op)
|
|
60
|
-
assert allowed is True
|
|
61
|
-
|
|
62
|
-
def test_write_operations_blocked_in_read_only_mode(self):
|
|
63
|
-
"""Test that write operations are blocked in READ_ONLY mode."""
|
|
64
|
-
set_safety_mode(SafetyMode.READ_ONLY)
|
|
65
|
-
|
|
66
|
-
for op in WRITE_OPERATIONS:
|
|
67
|
-
allowed, reason = is_operation_allowed(op)
|
|
68
|
-
assert allowed is False
|
|
69
|
-
assert "read-only mode" in reason
|
|
70
|
-
|
|
71
|
-
def test_destructive_operations_blocked_in_read_only_mode(self):
|
|
72
|
-
"""Test that destructive operations are blocked in READ_ONLY mode."""
|
|
73
|
-
set_safety_mode(SafetyMode.READ_ONLY)
|
|
74
|
-
|
|
75
|
-
for op in DESTRUCTIVE_OPERATIONS:
|
|
76
|
-
allowed, reason = is_operation_allowed(op)
|
|
77
|
-
assert allowed is False
|
|
78
|
-
assert "read-only mode" in reason
|
|
79
|
-
|
|
80
|
-
def test_write_operations_allowed_in_disable_destructive_mode(self):
|
|
81
|
-
"""Test that non-destructive write operations are allowed in DISABLE_DESTRUCTIVE mode."""
|
|
82
|
-
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
83
|
-
|
|
84
|
-
# Operations that are write but not destructive
|
|
85
|
-
non_destructive_writes = WRITE_OPERATIONS - DESTRUCTIVE_OPERATIONS
|
|
86
|
-
for op in non_destructive_writes:
|
|
87
|
-
allowed, reason = is_operation_allowed(op)
|
|
88
|
-
assert allowed is True
|
|
89
|
-
|
|
90
|
-
def test_destructive_operations_blocked_in_disable_destructive_mode(self):
|
|
91
|
-
"""Test that destructive operations are blocked in DISABLE_DESTRUCTIVE mode."""
|
|
92
|
-
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
93
|
-
|
|
94
|
-
for op in DESTRUCTIVE_OPERATIONS:
|
|
95
|
-
allowed, reason = is_operation_allowed(op)
|
|
96
|
-
assert allowed is False
|
|
97
|
-
assert "destructive operations are disabled" in reason
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class TestCheckSafetyModeDecorator:
|
|
101
|
-
"""Test the check_safety_mode decorator."""
|
|
102
|
-
|
|
103
|
-
def setup_method(self):
|
|
104
|
-
"""Reset safety mode to NORMAL before each test."""
|
|
105
|
-
set_safety_mode(SafetyMode.NORMAL)
|
|
106
|
-
|
|
107
|
-
def test_decorator_allows_in_normal_mode(self):
|
|
108
|
-
"""Test that decorated function executes in NORMAL mode."""
|
|
109
|
-
@check_safety_mode
|
|
110
|
-
def delete_pod():
|
|
111
|
-
return {"success": True, "message": "Pod deleted"}
|
|
112
|
-
|
|
113
|
-
result = delete_pod()
|
|
114
|
-
assert result["success"] is True
|
|
115
|
-
assert result["message"] == "Pod deleted"
|
|
116
|
-
|
|
117
|
-
def test_decorator_blocks_write_in_read_only_mode(self):
|
|
118
|
-
"""Test that decorated function is blocked in READ_ONLY mode."""
|
|
119
|
-
set_safety_mode(SafetyMode.READ_ONLY)
|
|
120
|
-
|
|
121
|
-
@check_safety_mode
|
|
122
|
-
def run_pod():
|
|
123
|
-
return {"success": True, "message": "Pod created"}
|
|
124
|
-
|
|
125
|
-
result = run_pod()
|
|
126
|
-
assert result["success"] is False
|
|
127
|
-
assert "blocked" in result["error"]
|
|
128
|
-
assert result["blocked_by"] == "read_only"
|
|
129
|
-
assert result["operation"] == "run_pod"
|
|
130
|
-
|
|
131
|
-
def test_decorator_blocks_destructive_in_disable_destructive_mode(self):
|
|
132
|
-
"""Test that destructive operations are blocked."""
|
|
133
|
-
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
134
|
-
|
|
135
|
-
@check_safety_mode
|
|
136
|
-
def delete_deployment():
|
|
137
|
-
return {"success": True, "message": "Deployment deleted"}
|
|
138
|
-
|
|
139
|
-
result = delete_deployment()
|
|
140
|
-
assert result["success"] is False
|
|
141
|
-
assert "blocked" in result["error"]
|
|
142
|
-
assert result["blocked_by"] == "disable_destructive"
|
|
143
|
-
|
|
144
|
-
def test_decorator_allows_non_destructive_write_in_disable_destructive_mode(self):
|
|
145
|
-
"""Test that non-destructive writes are allowed in DISABLE_DESTRUCTIVE mode."""
|
|
146
|
-
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
147
|
-
|
|
148
|
-
@check_safety_mode
|
|
149
|
-
def scale_deployment():
|
|
150
|
-
return {"success": True, "message": "Deployment scaled"}
|
|
151
|
-
|
|
152
|
-
result = scale_deployment()
|
|
153
|
-
assert result["success"] is True
|
|
154
|
-
|
|
155
|
-
|
|
156
36
|
class TestGetModeInfo:
|
|
157
37
|
"""Test get_mode_info function."""
|
|
158
38
|
|
tests/test_server.py
CHANGED
|
@@ -57,7 +57,7 @@ class TestServerInitialization:
|
|
|
57
57
|
|
|
58
58
|
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
59
59
|
with patch("kubernetes.config.load_kube_config"):
|
|
60
|
-
server = MCPServer(name="test",
|
|
60
|
+
server = MCPServer(name="test", disable_destructive=True)
|
|
61
61
|
|
|
62
62
|
assert server.non_destructive is True
|
|
63
63
|
|
|
@@ -215,7 +215,7 @@ class TestNonDestructiveMode:
|
|
|
215
215
|
|
|
216
216
|
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
217
217
|
with patch("kubernetes.config.load_kube_config"):
|
|
218
|
-
server = MCPServer(name="test"
|
|
218
|
+
server = MCPServer(name="test")
|
|
219
219
|
|
|
220
220
|
result = server._check_destructive()
|
|
221
221
|
assert result is None
|
|
@@ -227,12 +227,11 @@ class TestNonDestructiveMode:
|
|
|
227
227
|
|
|
228
228
|
with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
|
|
229
229
|
with patch("kubernetes.config.load_kube_config"):
|
|
230
|
-
server = MCPServer(name="test",
|
|
230
|
+
server = MCPServer(name="test", disable_destructive=True)
|
|
231
231
|
|
|
232
232
|
result = server._check_destructive()
|
|
233
233
|
assert result is not None
|
|
234
234
|
assert result["success"] is False
|
|
235
|
-
assert "non-destructive mode" in result["error"]
|
|
236
235
|
|
|
237
236
|
|
|
238
237
|
class TestSecretMasking:
|
|
@@ -349,7 +348,7 @@ class TestServerConfiguration:
|
|
|
349
348
|
with patch("kubernetes.config.load_kube_config"):
|
|
350
349
|
server = MCPServer(
|
|
351
350
|
name="custom-server",
|
|
352
|
-
|
|
351
|
+
disable_destructive=True
|
|
353
352
|
)
|
|
354
353
|
|
|
355
354
|
assert server.name == "custom-server"
|