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,298 @@
1
+ """
2
+ Custom prompt loading and rendering system for kubectl-mcp-server.
3
+
4
+ Supports user-defined workflow prompts via TOML configuration file with
5
+ Mustache-style template syntax ({{variable}}) and conditional sections.
6
+ """
7
+
8
+ import re
9
+ import logging
10
+ from typing import List, Dict, Any, Optional
11
+ from dataclasses import dataclass, field
12
+
13
+ logger = logging.getLogger("mcp-server")
14
+
15
+
16
+ @dataclass
17
+ class PromptArgument:
18
+ """Definition of a prompt argument."""
19
+ name: str
20
+ description: str = ""
21
+ required: bool = False
22
+ default: str = ""
23
+
24
+
25
+ @dataclass
26
+ class PromptMessage:
27
+ """A single message in a prompt conversation."""
28
+ role: str # "user" or "assistant"
29
+ content: str
30
+
31
+
32
+ @dataclass
33
+ class CustomPrompt:
34
+ """A custom prompt definition."""
35
+ name: str
36
+ description: str
37
+ title: str = ""
38
+ arguments: List[PromptArgument] = field(default_factory=list)
39
+ messages: List[PromptMessage] = field(default_factory=list)
40
+
41
+
42
+ def render_prompt(prompt: CustomPrompt, args: Dict[str, str]) -> List[PromptMessage]:
43
+ """
44
+ Render prompt messages with argument substitution using {{arg_name}} syntax.
45
+
46
+ Supports:
47
+ - Simple substitution: {{variable}} -> value
48
+ - Conditional sections: {{#variable}}content{{/variable}} (shown if variable is truthy)
49
+ - Inverse sections: {{^variable}}content{{/variable}} (shown if variable is falsy)
50
+
51
+ Args:
52
+ prompt: The CustomPrompt to render
53
+ args: Dictionary of argument names to values
54
+
55
+ Returns:
56
+ List of PromptMessage with rendered content
57
+ """
58
+ rendered = []
59
+ for msg in prompt.messages:
60
+ content = msg.content
61
+
62
+ # Process conditional sections ({{#var}}...{{/var}})
63
+ # These are shown only if the variable exists and is truthy
64
+ def process_conditional(match):
65
+ var_name = match.group(1)
66
+ section_content = match.group(2)
67
+ var_value = args.get(var_name, "")
68
+ # Only render section if variable exists and is not empty/false
69
+ if var_value and var_value.lower() not in ("false", "0", "no"):
70
+ # Recursively process the section content for variable substitution
71
+ processed = section_content
72
+ for key, value in args.items():
73
+ processed = processed.replace(f"{{{{{key}}}}}", str(value))
74
+ return processed
75
+ return ""
76
+
77
+ content = re.sub(
78
+ r'\{\{#(\w+)\}\}(.*?)\{\{/\1\}\}',
79
+ process_conditional,
80
+ content,
81
+ flags=re.DOTALL
82
+ )
83
+
84
+ # Process inverse sections ({{^var}}...{{/var}})
85
+ # These are shown only if the variable is missing or falsy
86
+ def process_inverse(match):
87
+ var_name = match.group(1)
88
+ section_content = match.group(2)
89
+ var_value = args.get(var_name, "")
90
+ # Only render section if variable is missing or falsy
91
+ if not var_value or var_value.lower() in ("false", "0", "no"):
92
+ processed = section_content
93
+ for key, value in args.items():
94
+ processed = processed.replace(f"{{{{{key}}}}}", str(value))
95
+ return processed
96
+ return ""
97
+
98
+ content = re.sub(
99
+ r'\{\{\^(\w+)\}\}(.*?)\{\{/\1\}\}',
100
+ process_inverse,
101
+ content,
102
+ flags=re.DOTALL
103
+ )
104
+
105
+ # Simple variable substitution
106
+ for key, value in args.items():
107
+ content = content.replace(f"{{{{{key}}}}}", str(value))
108
+
109
+ # Remove unsubstituted optional placeholders (simple variables only)
110
+ content = re.sub(r'\{\{[^#^/][^}]*\}\}', '', content)
111
+
112
+ # Clean up any remaining empty lines from removed sections
113
+ content = re.sub(r'\n\s*\n\s*\n', '\n\n', content)
114
+
115
+ rendered.append(PromptMessage(role=msg.role, content=content.strip()))
116
+
117
+ return rendered
118
+
119
+
120
+ def load_prompts_from_config(config: dict) -> List[CustomPrompt]:
121
+ """
122
+ Load prompts from config dict (from TOML).
123
+
124
+ Expected config structure:
125
+ {
126
+ "prompts": [
127
+ {
128
+ "name": "debug-pod",
129
+ "title": "Debug Pod Issues",
130
+ "description": "Diagnose pod problems",
131
+ "arguments": [
132
+ {"name": "pod_name", "required": True, "description": "Pod to debug"},
133
+ {"name": "namespace", "required": False, "default": "default"}
134
+ ],
135
+ "messages": [
136
+ {"role": "user", "content": "Debug pod {{pod_name}} in namespace {{namespace}}"}
137
+ ]
138
+ }
139
+ ]
140
+ }
141
+
142
+ Args:
143
+ config: Dictionary loaded from TOML configuration
144
+
145
+ Returns:
146
+ List of CustomPrompt objects
147
+ """
148
+ prompts = []
149
+
150
+ prompt_configs = config.get("prompts", [])
151
+ if not prompt_configs:
152
+ return prompts
153
+
154
+ for prompt_config in prompt_configs:
155
+ try:
156
+ # Parse arguments
157
+ arguments = []
158
+ for arg_config in prompt_config.get("arguments", []):
159
+ arg = PromptArgument(
160
+ name=arg_config.get("name", ""),
161
+ description=arg_config.get("description", ""),
162
+ required=arg_config.get("required", False),
163
+ default=arg_config.get("default", "")
164
+ )
165
+ if arg.name: # Only add valid arguments
166
+ arguments.append(arg)
167
+
168
+ # Parse messages
169
+ messages = []
170
+ for msg_config in prompt_config.get("messages", []):
171
+ msg = PromptMessage(
172
+ role=msg_config.get("role", "user"),
173
+ content=msg_config.get("content", "")
174
+ )
175
+ if msg.content: # Only add non-empty messages
176
+ messages.append(msg)
177
+
178
+ # Create prompt
179
+ prompt = CustomPrompt(
180
+ name=prompt_config.get("name", ""),
181
+ title=prompt_config.get("title", prompt_config.get("name", "")),
182
+ description=prompt_config.get("description", ""),
183
+ arguments=arguments,
184
+ messages=messages
185
+ )
186
+
187
+ if prompt.name: # Only add valid prompts
188
+ prompts.append(prompt)
189
+ logger.debug(f"Loaded custom prompt: {prompt.name}")
190
+
191
+ except Exception as e:
192
+ logger.warning(f"Failed to parse prompt config: {e}")
193
+ continue
194
+
195
+ return prompts
196
+
197
+
198
+ def load_prompts_from_toml_file(file_path: str) -> List[CustomPrompt]:
199
+ """
200
+ Load prompts from a TOML file.
201
+
202
+ Args:
203
+ file_path: Path to the TOML configuration file
204
+
205
+ Returns:
206
+ List of CustomPrompt objects
207
+ """
208
+ try:
209
+ import tomllib
210
+ except ImportError:
211
+ try:
212
+ import tomli as tomllib
213
+ except ImportError:
214
+ logger.warning("TOML parsing not available. Install tomli for Python < 3.11")
215
+ return []
216
+
217
+ try:
218
+ with open(file_path, "rb") as f:
219
+ config = tomllib.load(f)
220
+ return load_prompts_from_config(config)
221
+ except FileNotFoundError:
222
+ logger.debug(f"Custom prompts file not found: {file_path}")
223
+ return []
224
+ except Exception as e:
225
+ logger.warning(f"Failed to load prompts from {file_path}: {e}")
226
+ return []
227
+
228
+
229
+ def validate_prompt_args(prompt: CustomPrompt, args: Dict[str, str]) -> List[str]:
230
+ """
231
+ Validate that all required arguments are provided.
232
+
233
+ Args:
234
+ prompt: The CustomPrompt to validate against
235
+ args: Dictionary of argument names to values
236
+
237
+ Returns:
238
+ List of error messages (empty if valid)
239
+ """
240
+ errors = []
241
+
242
+ for arg in prompt.arguments:
243
+ if arg.required and arg.name not in args:
244
+ errors.append(f"Missing required argument: {arg.name}")
245
+ elif arg.required and not args.get(arg.name):
246
+ errors.append(f"Required argument cannot be empty: {arg.name}")
247
+
248
+ return errors
249
+
250
+
251
+ def apply_defaults(prompt: CustomPrompt, args: Dict[str, str]) -> Dict[str, str]:
252
+ """
253
+ Apply default values for missing optional arguments.
254
+
255
+ Args:
256
+ prompt: The CustomPrompt with argument definitions
257
+ args: Dictionary of argument names to values
258
+
259
+ Returns:
260
+ New dictionary with defaults applied
261
+ """
262
+ result = dict(args)
263
+
264
+ for arg in prompt.arguments:
265
+ if arg.name not in result and arg.default:
266
+ result[arg.name] = arg.default
267
+
268
+ return result
269
+
270
+
271
+ def get_prompt_schema(prompt: CustomPrompt) -> Dict[str, Any]:
272
+ """
273
+ Generate JSON Schema for prompt arguments.
274
+
275
+ Args:
276
+ prompt: The CustomPrompt to generate schema for
277
+
278
+ Returns:
279
+ JSON Schema dictionary
280
+ """
281
+ properties = {}
282
+ required = []
283
+
284
+ for arg in prompt.arguments:
285
+ properties[arg.name] = {
286
+ "type": "string",
287
+ "description": arg.description or f"Argument: {arg.name}"
288
+ }
289
+ if arg.default:
290
+ properties[arg.name]["default"] = arg.default
291
+ if arg.required:
292
+ required.append(arg.name)
293
+
294
+ return {
295
+ "type": "object",
296
+ "properties": properties,
297
+ "required": required
298
+ }
@@ -1,14 +1,190 @@
1
+ """
2
+ MCP prompts registration for kubectl-mcp-server.
3
+
4
+ This module handles registration of both built-in and custom prompts.
5
+ Custom prompts can be loaded from a TOML configuration file.
6
+ """
7
+
1
8
  import logging
2
- from typing import Optional
9
+ import os
10
+ from typing import Optional, Dict, Any
11
+
12
+ from .custom import (
13
+ CustomPrompt,
14
+ PromptMessage,
15
+ render_prompt,
16
+ load_prompts_from_toml_file,
17
+ validate_prompt_args,
18
+ apply_defaults,
19
+ )
20
+ from .builtin import get_builtin_prompts
3
21
 
4
22
  logger = logging.getLogger("mcp-server")
5
23
 
6
24
 
7
- def register_prompts(server):
8
- """Register all MCP prompts for Kubernetes workflows.
25
+ # Default paths for custom prompts configuration
26
+ DEFAULT_CONFIG_PATHS = [
27
+ os.path.expanduser("~/.kubectl-mcp/prompts.toml"),
28
+ os.path.expanduser("~/.config/kubectl-mcp/prompts.toml"),
29
+ "./kubectl-mcp-prompts.toml",
30
+ ]
31
+
32
+
33
+ def _get_custom_prompts_path() -> Optional[str]:
34
+ """
35
+ Get the path to custom prompts configuration file.
36
+
37
+ Checks (in order):
38
+ 1. MCP_PROMPTS_FILE environment variable
39
+ 2. Default config paths
40
+
41
+ Returns:
42
+ Path to config file if found, None otherwise
43
+ """
44
+ # Check environment variable first
45
+ env_path = os.environ.get("MCP_PROMPTS_FILE")
46
+ if env_path and os.path.isfile(env_path):
47
+ return env_path
48
+
49
+ # Check default paths
50
+ for path in DEFAULT_CONFIG_PATHS:
51
+ if os.path.isfile(path):
52
+ return path
53
+
54
+ return None
55
+
56
+
57
+ def _merge_prompts(builtin: list, custom: list) -> Dict[str, CustomPrompt]:
58
+ """
59
+ Merge built-in and custom prompts.
60
+
61
+ Custom prompts override built-in prompts with the same name.
62
+
63
+ Args:
64
+ builtin: List of built-in CustomPrompt objects
65
+ custom: List of custom CustomPrompt objects
66
+
67
+ Returns:
68
+ Dictionary of prompt name -> CustomPrompt
69
+ """
70
+ prompts = {}
71
+
72
+ # Add built-in prompts first
73
+ for prompt in builtin:
74
+ prompts[prompt.name] = prompt
75
+
76
+ # Custom prompts override built-in
77
+ for prompt in custom:
78
+ if prompt.name in prompts:
79
+ logger.info(f"Custom prompt '{prompt.name}' overrides built-in prompt")
80
+ prompts[prompt.name] = prompt
81
+
82
+ return prompts
83
+
84
+
85
+ def register_prompts(server, config_path: Optional[str] = None):
86
+ """
87
+ Register all MCP prompts for Kubernetes workflows.
88
+
89
+ Registers:
90
+ 1. Built-in prompts from builtin.py
91
+ 2. Custom prompts from configuration file (if found)
92
+ 3. Original inline prompts for backward compatibility
93
+
94
+ Custom prompts can override built-in prompts by using the same name.
95
+
96
+ Args:
97
+ server: FastMCP server instance
98
+ config_path: Optional path to custom prompts TOML file
99
+ """
100
+ # Load built-in prompts
101
+ builtin_prompts = get_builtin_prompts()
102
+ logger.debug(f"Loaded {len(builtin_prompts)} built-in prompts")
103
+
104
+ # Load custom prompts
105
+ prompts_file = config_path or _get_custom_prompts_path()
106
+ custom_prompts = []
107
+ if prompts_file:
108
+ custom_prompts = load_prompts_from_toml_file(prompts_file)
109
+ logger.info(f"Loaded {len(custom_prompts)} custom prompts from {prompts_file}")
110
+
111
+ # Merge prompts (custom overrides built-in)
112
+ all_prompts = _merge_prompts(builtin_prompts, custom_prompts)
113
+
114
+ # Register each configurable prompt
115
+ for prompt in all_prompts.values():
116
+ _register_custom_prompt(server, prompt)
117
+
118
+ logger.debug(f"Registered {len(all_prompts)} configurable prompts")
119
+
120
+ # Register original inline prompts for backward compatibility
121
+ _register_inline_prompts(server)
122
+
123
+
124
+ def _register_custom_prompt(server, prompt: CustomPrompt):
125
+ """
126
+ Register a single CustomPrompt with the server.
9
127
 
10
128
  Args:
11
129
  server: FastMCP server instance
130
+ prompt: CustomPrompt to register
131
+ """
132
+ # Build the argument schema for FastMCP
133
+ def create_prompt_handler(p: CustomPrompt):
134
+ """Create a closure that captures the prompt."""
135
+ def handler(**kwargs) -> str:
136
+ # Apply defaults for missing optional arguments
137
+ args = apply_defaults(p, kwargs)
138
+
139
+ # Validate required arguments
140
+ errors = validate_prompt_args(p, args)
141
+ if errors:
142
+ return f"Error: {'; '.join(errors)}"
143
+
144
+ # Render the prompt messages
145
+ rendered = render_prompt(p, args)
146
+
147
+ # Return the content (for now, just the first message)
148
+ # MCP prompts typically return a single string
149
+ if rendered:
150
+ return rendered[0].content
151
+ return f"Prompt '{p.name}' has no messages defined."
152
+
153
+ return handler
154
+
155
+ # Create the handler
156
+ handler = create_prompt_handler(prompt)
157
+
158
+ # Set function metadata for FastMCP registration
159
+ handler.__name__ = prompt.name.replace("-", "_")
160
+ handler.__doc__ = prompt.description or prompt.title
161
+
162
+ # Build parameter annotations from arguments
163
+ params = {}
164
+ for arg in prompt.arguments:
165
+ # All prompt arguments are strings with Optional if not required
166
+ if arg.required:
167
+ params[arg.name] = str
168
+ else:
169
+ params[arg.name] = Optional[str]
170
+
171
+ handler.__annotations__ = params
172
+ handler.__annotations__["return"] = str
173
+
174
+ # Register with server
175
+ try:
176
+ server.prompt()(handler)
177
+ logger.debug(f"Registered configurable prompt: {prompt.name}")
178
+ except Exception as e:
179
+ logger.warning(f"Failed to register prompt '{prompt.name}': {e}")
180
+
181
+
182
+ def _register_inline_prompts(server):
183
+ """
184
+ Register original inline prompts for backward compatibility.
185
+
186
+ These prompts are kept for users who may be using them directly.
187
+ They can be overridden by custom prompts with the same name.
12
188
  """
13
189
 
14
190
  @server.prompt()
@@ -605,7 +781,7 @@ Target Replicas: {target_replicas}
605
781
 
606
782
  ### Step 2: Capacity Planning
607
783
  Calculate required resources:
608
- - Current pod resources × {target_replicas} = Total needed
784
+ - Current pod resources x {target_replicas} = Total needed
609
785
  - Check node capacity: `kubectl_top("nodes")`
610
786
  - Verify cluster can accommodate new pods
611
787
 
@@ -0,0 +1,155 @@
1
+ """
2
+ Safety mode implementation for kubectl-mcp-server.
3
+
4
+ Provides read-only and disable-destructive modes to prevent accidental cluster mutations.
5
+ """
6
+
7
+ from enum import Enum
8
+ from functools import wraps
9
+ from typing import Any, Callable, Dict, Set
10
+ import logging
11
+
12
+ logger = logging.getLogger("mcp-server")
13
+
14
+
15
+ class SafetyMode(Enum):
16
+ """Safety mode levels for the MCP server."""
17
+ NORMAL = "normal"
18
+ READ_ONLY = "read_only"
19
+ DISABLE_DESTRUCTIVE = "disable_destructive"
20
+
21
+
22
+ # Global safety mode state
23
+ _current_mode: SafetyMode = SafetyMode.NORMAL
24
+
25
+
26
+ # Operations that modify cluster state (blocked in READ_ONLY mode)
27
+ WRITE_OPERATIONS: Set[str] = {
28
+ # Pod operations
29
+ "run_pod", "delete_pod",
30
+ # Deployment operations
31
+ "scale_deployment", "restart_deployment", "delete_deployment",
32
+ "rollback_deployment", "create_deployment", "update_deployment",
33
+ # StatefulSet operations
34
+ "scale_statefulset", "restart_statefulset", "delete_statefulset",
35
+ # DaemonSet operations
36
+ "restart_daemonset", "delete_daemonset",
37
+ # Service operations
38
+ "create_service", "delete_service", "update_service",
39
+ # ConfigMap/Secret operations
40
+ "create_configmap", "delete_configmap", "update_configmap",
41
+ "create_secret", "delete_secret", "update_secret",
42
+ # Namespace operations
43
+ "create_namespace", "delete_namespace",
44
+ # Helm operations
45
+ "install_helm_chart", "upgrade_helm_chart", "uninstall_helm_chart",
46
+ "rollback_helm_release",
47
+ # kubectl operations
48
+ "apply_manifest", "delete_resource", "patch_resource",
49
+ "create_resource", "replace_resource",
50
+ # Context operations
51
+ "switch_context", "set_namespace_for_context",
52
+ # Rollout operations
53
+ "rollout_promote_tool", "rollout_abort_tool", "rollout_retry_tool",
54
+ "rollout_restart_tool",
55
+ # KubeVirt operations
56
+ "kubevirt_vm_start_tool", "kubevirt_vm_stop_tool", "kubevirt_vm_restart_tool",
57
+ "kubevirt_vm_pause_tool", "kubevirt_vm_unpause_tool", "kubevirt_vm_migrate_tool",
58
+ # CAPI operations
59
+ "capi_machinedeployment_scale_tool",
60
+ }
61
+
62
+ # Operations that are destructive (blocked in DISABLE_DESTRUCTIVE mode)
63
+ DESTRUCTIVE_OPERATIONS: Set[str] = {
64
+ # Delete operations
65
+ "delete_pod", "delete_deployment", "delete_statefulset", "delete_daemonset",
66
+ "delete_service", "delete_configmap", "delete_secret", "delete_namespace",
67
+ "delete_resource",
68
+ # Helm uninstall
69
+ "uninstall_helm_chart",
70
+ # Rollout abort
71
+ "rollout_abort_tool",
72
+ # VM stop
73
+ "kubevirt_vm_stop_tool",
74
+ }
75
+
76
+
77
+ def get_safety_mode() -> SafetyMode:
78
+ """Get the current safety mode."""
79
+ return _current_mode
80
+
81
+
82
+ def set_safety_mode(mode: SafetyMode) -> None:
83
+ """Set the safety mode globally."""
84
+ global _current_mode
85
+ _current_mode = mode
86
+ logger.info(f"Safety mode set to: {mode.value}")
87
+
88
+
89
+ def is_operation_allowed(operation_name: str) -> tuple[bool, str]:
90
+ """
91
+ Check if an operation is allowed under the current safety mode.
92
+
93
+ Returns:
94
+ Tuple of (allowed: bool, reason: str)
95
+ """
96
+ mode = get_safety_mode()
97
+
98
+ if mode == SafetyMode.NORMAL:
99
+ return True, ""
100
+
101
+ if mode == SafetyMode.READ_ONLY:
102
+ if operation_name in WRITE_OPERATIONS or operation_name in DESTRUCTIVE_OPERATIONS:
103
+ return False, f"Operation '{operation_name}' blocked: read-only mode is enabled"
104
+
105
+ if mode == SafetyMode.DISABLE_DESTRUCTIVE:
106
+ if operation_name in DESTRUCTIVE_OPERATIONS:
107
+ return False, f"Operation '{operation_name}' blocked: destructive operations are disabled"
108
+
109
+ return True, ""
110
+
111
+
112
+ def check_safety_mode(func: Callable) -> Callable:
113
+ """
114
+ Decorator to check safety mode before executing a tool function.
115
+
116
+ Usage:
117
+ @check_safety_mode
118
+ def delete_pod(...):
119
+ ...
120
+ """
121
+ @wraps(func)
122
+ def wrapper(*args, **kwargs) -> Dict[str, Any]:
123
+ operation_name = func.__name__
124
+ allowed, reason = is_operation_allowed(operation_name)
125
+
126
+ if not allowed:
127
+ logger.warning(f"Blocked operation: {operation_name} (mode: {get_safety_mode().value})")
128
+ return {
129
+ "success": False,
130
+ "error": reason,
131
+ "blocked_by": get_safety_mode().value,
132
+ "operation": operation_name
133
+ }
134
+
135
+ return func(*args, **kwargs)
136
+
137
+ return wrapper
138
+
139
+
140
+ def get_mode_info() -> Dict[str, Any]:
141
+ """Get information about the current safety mode."""
142
+ mode = get_safety_mode()
143
+ return {
144
+ "mode": mode.value,
145
+ "description": {
146
+ SafetyMode.NORMAL: "All operations allowed",
147
+ SafetyMode.READ_ONLY: "Only read operations allowed (no create/update/delete)",
148
+ SafetyMode.DISABLE_DESTRUCTIVE: "Create/update allowed, delete operations blocked",
149
+ }[mode],
150
+ "blocked_operations": {
151
+ SafetyMode.NORMAL: [],
152
+ SafetyMode.READ_ONLY: sorted(WRITE_OPERATIONS | DESTRUCTIVE_OPERATIONS),
153
+ SafetyMode.DISABLE_DESTRUCTIVE: sorted(DESTRUCTIVE_OPERATIONS),
154
+ }[mode]
155
+ }
@@ -11,6 +11,16 @@ from .diagnostics import register_diagnostics_tools
11
11
  from .cost import register_cost_tools
12
12
  from .browser import register_browser_tools, is_browser_available
13
13
  from .ui import register_ui_tools, is_ui_available
14
+ from .gitops import register_gitops_tools
15
+ from .certs import register_certs_tools
16
+ from .policy import register_policy_tools
17
+ from .backup import register_backup_tools
18
+ from .keda import register_keda_tools
19
+ from .cilium import register_cilium_tools
20
+ from .rollouts import register_rollouts_tools
21
+ from .capi import register_capi_tools
22
+ from .kubevirt import register_kubevirt_tools
23
+ from .kiali import register_istio_tools
14
24
 
15
25
  __all__ = [
16
26
  "register_helm_tools",
@@ -28,4 +38,14 @@ __all__ = [
28
38
  "is_browser_available",
29
39
  "register_ui_tools",
30
40
  "is_ui_available",
41
+ "register_gitops_tools",
42
+ "register_certs_tools",
43
+ "register_policy_tools",
44
+ "register_backup_tools",
45
+ "register_keda_tools",
46
+ "register_cilium_tools",
47
+ "register_rollouts_tools",
48
+ "register_capi_tools",
49
+ "register_kubevirt_tools",
50
+ "register_istio_tools",
31
51
  ]