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.
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/METADATA +34 -13
- kubectl_mcp_server-1.17.0.dist-info/RECORD +75 -0
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/cli/cli.py +83 -9
- kubectl_mcp_tool/cli/output.py +14 -0
- kubectl_mcp_tool/config/__init__.py +46 -0
- kubectl_mcp_tool/config/loader.py +386 -0
- kubectl_mcp_tool/config/schema.py +184 -0
- kubectl_mcp_tool/crd_detector.py +247 -0
- kubectl_mcp_tool/k8s_config.py +19 -0
- kubectl_mcp_tool/mcp_server.py +246 -8
- kubectl_mcp_tool/observability/__init__.py +59 -0
- kubectl_mcp_tool/observability/metrics.py +223 -0
- kubectl_mcp_tool/observability/stats.py +255 -0
- kubectl_mcp_tool/observability/tracing.py +335 -0
- kubectl_mcp_tool/prompts/__init__.py +43 -0
- kubectl_mcp_tool/prompts/builtin.py +695 -0
- kubectl_mcp_tool/prompts/custom.py +298 -0
- kubectl_mcp_tool/prompts/prompts.py +180 -4
- kubectl_mcp_tool/safety.py +155 -0
- kubectl_mcp_tool/tools/__init__.py +20 -0
- kubectl_mcp_tool/tools/backup.py +881 -0
- kubectl_mcp_tool/tools/capi.py +727 -0
- kubectl_mcp_tool/tools/certs.py +709 -0
- kubectl_mcp_tool/tools/cilium.py +582 -0
- kubectl_mcp_tool/tools/cluster.py +384 -0
- kubectl_mcp_tool/tools/gitops.py +552 -0
- kubectl_mcp_tool/tools/keda.py +464 -0
- kubectl_mcp_tool/tools/kiali.py +652 -0
- kubectl_mcp_tool/tools/kubevirt.py +803 -0
- kubectl_mcp_tool/tools/policy.py +554 -0
- kubectl_mcp_tool/tools/rollouts.py +790 -0
- tests/test_browser.py +2 -2
- tests/test_config.py +386 -0
- tests/test_ecosystem.py +331 -0
- tests/test_mcp_integration.py +251 -0
- tests/test_observability.py +521 -0
- tests/test_prompts.py +716 -0
- tests/test_safety.py +218 -0
- tests/test_tools.py +70 -8
- kubectl_mcp_server-1.15.0.dist-info/RECORD +0 -49
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""Configuration loader with TOML support, drop-in directories, and SIGHUP reload.
|
|
2
|
+
|
|
3
|
+
This module handles:
|
|
4
|
+
- Loading main config from ~/.config/kubectl-mcp-server/config.toml
|
|
5
|
+
- Merging drop-in configs from ~/.config/kubectl-mcp-server/config.d/*.toml
|
|
6
|
+
- Environment variable overrides
|
|
7
|
+
- SIGHUP signal handling for runtime reloads
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import signal
|
|
13
|
+
import sys
|
|
14
|
+
from dataclasses import fields
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
17
|
+
|
|
18
|
+
from .schema import (
|
|
19
|
+
BrowserConfig,
|
|
20
|
+
Config,
|
|
21
|
+
KubernetesConfig,
|
|
22
|
+
LoggingConfig,
|
|
23
|
+
MetricsConfig,
|
|
24
|
+
SafetyConfig,
|
|
25
|
+
ServerConfig,
|
|
26
|
+
validate_config,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Try to import tomli/tomllib for TOML parsing
|
|
32
|
+
try:
|
|
33
|
+
import tomllib # Python 3.11+
|
|
34
|
+
except ImportError:
|
|
35
|
+
try:
|
|
36
|
+
import tomli as tomllib # type: ignore
|
|
37
|
+
except ImportError:
|
|
38
|
+
tomllib = None # type: ignore
|
|
39
|
+
|
|
40
|
+
# Global config instance
|
|
41
|
+
_config: Optional[Config] = None
|
|
42
|
+
_config_callbacks: List[Callable[[Config], None]] = []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_config_paths() -> Dict[str, Path]:
|
|
46
|
+
"""Get standard configuration paths.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dictionary with paths for config_dir, main_config, and drop_in_dir
|
|
50
|
+
"""
|
|
51
|
+
# Check XDG_CONFIG_HOME first, then fall back to ~/.config
|
|
52
|
+
xdg_config = os.environ.get("XDG_CONFIG_HOME")
|
|
53
|
+
if xdg_config:
|
|
54
|
+
base_dir = Path(xdg_config)
|
|
55
|
+
else:
|
|
56
|
+
base_dir = Path.home() / ".config"
|
|
57
|
+
|
|
58
|
+
config_dir = base_dir / "kubectl-mcp-server"
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
"config_dir": config_dir,
|
|
62
|
+
"main_config": config_dir / "config.toml",
|
|
63
|
+
"drop_in_dir": config_dir / "config.d",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _load_toml_file(path: Path) -> Dict[str, Any]:
|
|
68
|
+
"""Load a TOML file and return its contents as a dictionary.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
path: Path to the TOML file
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Dictionary containing the TOML data
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
RuntimeError: If tomllib/tomli is not available
|
|
78
|
+
FileNotFoundError: If the file doesn't exist
|
|
79
|
+
ValueError: If the TOML is invalid
|
|
80
|
+
"""
|
|
81
|
+
if tomllib is None:
|
|
82
|
+
raise RuntimeError(
|
|
83
|
+
"TOML parsing requires Python 3.11+ or the 'tomli' package. "
|
|
84
|
+
"Install with: pip install tomli"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
with open(path, "rb") as f:
|
|
88
|
+
return tomllib.load(f)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
|
92
|
+
"""Deep merge two dictionaries, with override taking precedence.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
base: Base dictionary
|
|
96
|
+
override: Override dictionary (takes precedence)
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Merged dictionary
|
|
100
|
+
"""
|
|
101
|
+
result = base.copy()
|
|
102
|
+
|
|
103
|
+
for key, value in override.items():
|
|
104
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
105
|
+
result[key] = _deep_merge(result[key], value)
|
|
106
|
+
else:
|
|
107
|
+
result[key] = value
|
|
108
|
+
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _apply_env_overrides(config_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
113
|
+
"""Apply environment variable overrides to configuration.
|
|
114
|
+
|
|
115
|
+
Environment variables follow the pattern:
|
|
116
|
+
MCP_<SECTION>_<KEY>=value
|
|
117
|
+
|
|
118
|
+
Examples:
|
|
119
|
+
MCP_SERVER_PORT=9000
|
|
120
|
+
MCP_SAFETY_MODE=read-only
|
|
121
|
+
MCP_BROWSER_ENABLED=true
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
config_dict: Configuration dictionary
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Configuration with environment overrides applied
|
|
128
|
+
"""
|
|
129
|
+
result = config_dict.copy()
|
|
130
|
+
|
|
131
|
+
env_mappings = {
|
|
132
|
+
# Server settings
|
|
133
|
+
"MCP_SERVER_TRANSPORT": ("server", "transport"),
|
|
134
|
+
"MCP_SERVER_HOST": ("server", "host"),
|
|
135
|
+
"MCP_SERVER_PORT": ("server", "port", int),
|
|
136
|
+
"MCP_DEBUG": ("server", "debug", lambda x: x.lower() in ("true", "1", "yes")),
|
|
137
|
+
"MCP_LOG_FILE": ("server", "log_file"),
|
|
138
|
+
"MCP_LOG_LEVEL": ("server", "log_level"),
|
|
139
|
+
# Safety settings
|
|
140
|
+
"MCP_SAFETY_MODE": ("safety", "mode"),
|
|
141
|
+
# Browser settings
|
|
142
|
+
"MCP_BROWSER_ENABLED": ("browser", "enabled", lambda x: x.lower() in ("true", "1", "yes")),
|
|
143
|
+
"MCP_BROWSER_PROVIDER": ("browser", "provider"),
|
|
144
|
+
"MCP_BROWSER_HEADED": ("browser", "headed", lambda x: x.lower() in ("true", "1", "yes")),
|
|
145
|
+
"BROWSERBASE_API_KEY": ("browser", "browserbase_api_key"),
|
|
146
|
+
"BROWSERBASE_PROJECT_ID": ("browser", "browserbase_project_id"),
|
|
147
|
+
"BROWSER_USE_API_KEY": ("browser", "browseruse_api_key"),
|
|
148
|
+
"MCP_BROWSER_CDP_URL": ("browser", "cdp_url"),
|
|
149
|
+
# Metrics settings
|
|
150
|
+
"MCP_METRICS_ENABLED": ("metrics", "enabled", lambda x: x.lower() in ("true", "1", "yes")),
|
|
151
|
+
"MCP_TRACING_ENABLED": ("metrics", "tracing_enabled", lambda x: x.lower() in ("true", "1", "yes")),
|
|
152
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT": ("metrics", "otlp_endpoint"),
|
|
153
|
+
"OTEL_SERVICE_NAME": ("metrics", "service_name"),
|
|
154
|
+
# Kubernetes settings
|
|
155
|
+
"KUBECONFIG": ("kubernetes", "kubeconfig"),
|
|
156
|
+
"MCP_K8S_CONTEXT": ("kubernetes", "context"),
|
|
157
|
+
"MCP_K8S_NAMESPACE": ("kubernetes", "default_namespace"),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for env_var, mapping in env_mappings.items():
|
|
161
|
+
value = os.environ.get(env_var)
|
|
162
|
+
if value is not None:
|
|
163
|
+
section = mapping[0]
|
|
164
|
+
key = mapping[1]
|
|
165
|
+
converter = mapping[2] if len(mapping) > 2 else str
|
|
166
|
+
|
|
167
|
+
if section not in result:
|
|
168
|
+
result[section] = {}
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
result[section][key] = converter(value)
|
|
172
|
+
except (ValueError, TypeError) as e:
|
|
173
|
+
logger.warning(f"Invalid value for {env_var}: {value} ({e})")
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _dict_to_config(config_dict: Dict[str, Any]) -> Config:
|
|
179
|
+
"""Convert a configuration dictionary to a Config dataclass.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
config_dict: Configuration dictionary
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Config dataclass instance
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def make_dataclass(cls, data: Dict[str, Any]):
|
|
189
|
+
"""Create a dataclass instance from a dictionary, ignoring extra keys."""
|
|
190
|
+
valid_keys = {f.name for f in fields(cls)}
|
|
191
|
+
filtered = {k: v for k, v in data.items() if k in valid_keys}
|
|
192
|
+
return cls(**filtered)
|
|
193
|
+
|
|
194
|
+
server = make_dataclass(ServerConfig, config_dict.get("server", {}))
|
|
195
|
+
safety = make_dataclass(SafetyConfig, config_dict.get("safety", {}))
|
|
196
|
+
browser = make_dataclass(BrowserConfig, config_dict.get("browser", {}))
|
|
197
|
+
metrics = make_dataclass(MetricsConfig, config_dict.get("metrics", {}))
|
|
198
|
+
logging_config = make_dataclass(LoggingConfig, config_dict.get("logging", {}))
|
|
199
|
+
kubernetes = make_dataclass(KubernetesConfig, config_dict.get("kubernetes", {}))
|
|
200
|
+
|
|
201
|
+
# Collect custom/unknown sections
|
|
202
|
+
known_sections = {"server", "safety", "browser", "metrics", "logging", "kubernetes"}
|
|
203
|
+
custom = {k: v for k, v in config_dict.items() if k not in known_sections}
|
|
204
|
+
|
|
205
|
+
return Config(
|
|
206
|
+
server=server,
|
|
207
|
+
safety=safety,
|
|
208
|
+
browser=browser,
|
|
209
|
+
metrics=metrics,
|
|
210
|
+
logging=logging_config,
|
|
211
|
+
kubernetes=kubernetes,
|
|
212
|
+
custom=custom,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def load_config(
|
|
217
|
+
config_file: Optional[Union[str, Path]] = None,
|
|
218
|
+
skip_env: bool = False,
|
|
219
|
+
) -> Config:
|
|
220
|
+
"""Load configuration from files and environment.
|
|
221
|
+
|
|
222
|
+
Loading order (later takes precedence):
|
|
223
|
+
1. Default values
|
|
224
|
+
2. Main config file (~/.config/kubectl-mcp-server/config.toml)
|
|
225
|
+
3. Drop-in files (~/.config/kubectl-mcp-server/config.d/*.toml) in sorted order
|
|
226
|
+
4. Custom config file (if specified)
|
|
227
|
+
5. Environment variables (unless skip_env=True)
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
config_file: Optional path to a specific config file
|
|
231
|
+
skip_env: If True, skip environment variable overrides
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Loaded Config instance
|
|
235
|
+
"""
|
|
236
|
+
global _config
|
|
237
|
+
|
|
238
|
+
config_dict: Dict[str, Any] = {}
|
|
239
|
+
paths = get_config_paths()
|
|
240
|
+
|
|
241
|
+
# 1. Load main config file if it exists
|
|
242
|
+
main_config = paths["main_config"]
|
|
243
|
+
if main_config.exists():
|
|
244
|
+
try:
|
|
245
|
+
loaded = _load_toml_file(main_config)
|
|
246
|
+
config_dict = _deep_merge(config_dict, loaded)
|
|
247
|
+
logger.debug(f"Loaded config from {main_config}")
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.warning(f"Failed to load {main_config}: {e}")
|
|
250
|
+
|
|
251
|
+
# 2. Load drop-in configs in sorted order
|
|
252
|
+
drop_in_dir = paths["drop_in_dir"]
|
|
253
|
+
if drop_in_dir.exists() and drop_in_dir.is_dir():
|
|
254
|
+
drop_in_files = sorted(drop_in_dir.glob("*.toml"))
|
|
255
|
+
for drop_in_file in drop_in_files:
|
|
256
|
+
try:
|
|
257
|
+
loaded = _load_toml_file(drop_in_file)
|
|
258
|
+
config_dict = _deep_merge(config_dict, loaded)
|
|
259
|
+
logger.debug(f"Loaded drop-in config from {drop_in_file}")
|
|
260
|
+
except Exception as e:
|
|
261
|
+
logger.warning(f"Failed to load {drop_in_file}: {e}")
|
|
262
|
+
|
|
263
|
+
# 3. Load custom config file if specified
|
|
264
|
+
if config_file:
|
|
265
|
+
config_path = Path(config_file)
|
|
266
|
+
if config_path.exists():
|
|
267
|
+
try:
|
|
268
|
+
loaded = _load_toml_file(config_path)
|
|
269
|
+
config_dict = _deep_merge(config_dict, loaded)
|
|
270
|
+
logger.debug(f"Loaded custom config from {config_path}")
|
|
271
|
+
except Exception as e:
|
|
272
|
+
logger.warning(f"Failed to load {config_path}: {e}")
|
|
273
|
+
else:
|
|
274
|
+
logger.warning(f"Config file not found: {config_path}")
|
|
275
|
+
|
|
276
|
+
# 4. Apply environment variable overrides
|
|
277
|
+
if not skip_env:
|
|
278
|
+
config_dict = _apply_env_overrides(config_dict)
|
|
279
|
+
|
|
280
|
+
# 5. Validate configuration
|
|
281
|
+
errors = validate_config(config_dict)
|
|
282
|
+
if errors:
|
|
283
|
+
for error in errors:
|
|
284
|
+
logger.error(f"Config validation error: {error}")
|
|
285
|
+
|
|
286
|
+
# 6. Convert to Config dataclass
|
|
287
|
+
_config = _dict_to_config(config_dict)
|
|
288
|
+
|
|
289
|
+
return _config
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_config() -> Config:
|
|
293
|
+
"""Get the current configuration, loading if necessary.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Current Config instance
|
|
297
|
+
"""
|
|
298
|
+
global _config
|
|
299
|
+
if _config is None:
|
|
300
|
+
_config = load_config()
|
|
301
|
+
return _config
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def reload_config() -> Config:
|
|
305
|
+
"""Reload configuration from files.
|
|
306
|
+
|
|
307
|
+
This is called by the SIGHUP handler for runtime config updates.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Newly loaded Config instance
|
|
311
|
+
"""
|
|
312
|
+
global _config
|
|
313
|
+
|
|
314
|
+
logger.info("Reloading configuration...")
|
|
315
|
+
old_config = _config
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
_config = load_config()
|
|
319
|
+
logger.info("Configuration reloaded successfully")
|
|
320
|
+
|
|
321
|
+
# Notify callbacks
|
|
322
|
+
for callback in _config_callbacks:
|
|
323
|
+
try:
|
|
324
|
+
callback(_config)
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.error(f"Config reload callback failed: {e}")
|
|
327
|
+
|
|
328
|
+
except Exception as e:
|
|
329
|
+
logger.error(f"Failed to reload configuration: {e}")
|
|
330
|
+
_config = old_config
|
|
331
|
+
raise
|
|
332
|
+
|
|
333
|
+
return _config
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def register_reload_callback(callback: Callable[[Config], None]) -> None:
|
|
337
|
+
"""Register a callback to be called when configuration is reloaded.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
callback: Function that takes the new Config as argument
|
|
341
|
+
"""
|
|
342
|
+
_config_callbacks.append(callback)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def unregister_reload_callback(callback: Callable[[Config], None]) -> None:
|
|
346
|
+
"""Unregister a previously registered reload callback.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
callback: The callback function to remove
|
|
350
|
+
"""
|
|
351
|
+
try:
|
|
352
|
+
_config_callbacks.remove(callback)
|
|
353
|
+
except ValueError:
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _sighup_handler(signum: int, frame: Any) -> None:
|
|
358
|
+
"""Handle SIGHUP signal for configuration reload."""
|
|
359
|
+
logger.info("Received SIGHUP, reloading configuration...")
|
|
360
|
+
try:
|
|
361
|
+
reload_config()
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.error(f"Configuration reload failed: {e}")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def setup_sighup_handler() -> bool:
|
|
367
|
+
"""Set up SIGHUP handler for runtime configuration reload.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
True if handler was set up, False if not supported (e.g., Windows)
|
|
371
|
+
"""
|
|
372
|
+
if sys.platform == "win32":
|
|
373
|
+
logger.debug("SIGHUP not supported on Windows")
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
signal.signal(signal.SIGHUP, _sighup_handler)
|
|
378
|
+
logger.debug("SIGHUP handler installed for config reload")
|
|
379
|
+
return True
|
|
380
|
+
except (OSError, AttributeError) as e:
|
|
381
|
+
logger.warning(f"Could not install SIGHUP handler: {e}")
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# Re-export Config from schema
|
|
386
|
+
Config = Config
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Configuration schema definitions for kubectl-mcp-server.
|
|
2
|
+
|
|
3
|
+
Defines dataclasses for type-safe configuration with validation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ServerConfig:
|
|
15
|
+
"""MCP server configuration."""
|
|
16
|
+
|
|
17
|
+
transport: str = "streamable-http"
|
|
18
|
+
host: str = "127.0.0.1"
|
|
19
|
+
port: int = 8000
|
|
20
|
+
debug: bool = False
|
|
21
|
+
log_file: Optional[str] = None
|
|
22
|
+
log_level: str = "INFO"
|
|
23
|
+
|
|
24
|
+
def __post_init__(self):
|
|
25
|
+
"""Validate configuration values."""
|
|
26
|
+
valid_transports = {"stdio", "sse", "streamable-http"}
|
|
27
|
+
if self.transport not in valid_transports:
|
|
28
|
+
raise ValueError(f"Invalid transport: {self.transport}. Must be one of {valid_transports}")
|
|
29
|
+
|
|
30
|
+
if not 1 <= self.port <= 65535:
|
|
31
|
+
raise ValueError(f"Invalid port: {self.port}. Must be between 1 and 65535")
|
|
32
|
+
|
|
33
|
+
valid_log_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
|
34
|
+
if self.log_level.upper() not in valid_log_levels:
|
|
35
|
+
raise ValueError(f"Invalid log_level: {self.log_level}. Must be one of {valid_log_levels}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class SafetyConfig:
|
|
40
|
+
"""Safety mode configuration."""
|
|
41
|
+
|
|
42
|
+
mode: str = "normal"
|
|
43
|
+
|
|
44
|
+
# Additional safety settings
|
|
45
|
+
confirm_destructive: bool = False
|
|
46
|
+
max_delete_count: int = 10
|
|
47
|
+
blocked_namespaces: List[str] = field(default_factory=lambda: ["kube-system", "kube-public"])
|
|
48
|
+
|
|
49
|
+
def __post_init__(self):
|
|
50
|
+
"""Validate safety mode."""
|
|
51
|
+
valid_modes = {"normal", "read-only", "disable-destructive"}
|
|
52
|
+
if self.mode not in valid_modes:
|
|
53
|
+
raise ValueError(f"Invalid safety mode: {self.mode}. Must be one of {valid_modes}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class BrowserConfig:
|
|
58
|
+
"""Browser automation configuration."""
|
|
59
|
+
|
|
60
|
+
enabled: bool = False
|
|
61
|
+
provider: str = "local"
|
|
62
|
+
headed: bool = False
|
|
63
|
+
timeout: int = 60
|
|
64
|
+
max_retries: int = 3
|
|
65
|
+
|
|
66
|
+
# Provider-specific settings
|
|
67
|
+
browserbase_api_key: Optional[str] = None
|
|
68
|
+
browserbase_project_id: Optional[str] = None
|
|
69
|
+
browseruse_api_key: Optional[str] = None
|
|
70
|
+
cdp_url: Optional[str] = None
|
|
71
|
+
|
|
72
|
+
def __post_init__(self):
|
|
73
|
+
"""Validate browser configuration."""
|
|
74
|
+
valid_providers = {"local", "browserbase", "browseruse", "cdp"}
|
|
75
|
+
if self.provider not in valid_providers:
|
|
76
|
+
raise ValueError(f"Invalid browser provider: {self.provider}. Must be one of {valid_providers}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class MetricsConfig:
|
|
81
|
+
"""Metrics and observability configuration."""
|
|
82
|
+
|
|
83
|
+
enabled: bool = False
|
|
84
|
+
endpoint: str = "/metrics"
|
|
85
|
+
|
|
86
|
+
# Tracing settings
|
|
87
|
+
tracing_enabled: bool = False
|
|
88
|
+
otlp_endpoint: Optional[str] = None
|
|
89
|
+
service_name: str = "kubectl-mcp-server"
|
|
90
|
+
sample_rate: float = 1.0
|
|
91
|
+
|
|
92
|
+
def __post_init__(self):
|
|
93
|
+
"""Validate metrics configuration."""
|
|
94
|
+
if not 0.0 <= self.sample_rate <= 1.0:
|
|
95
|
+
raise ValueError(f"Invalid sample_rate: {self.sample_rate}. Must be between 0.0 and 1.0")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class LoggingConfig:
|
|
100
|
+
"""Logging configuration."""
|
|
101
|
+
|
|
102
|
+
level: str = "INFO"
|
|
103
|
+
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
104
|
+
file: Optional[str] = None
|
|
105
|
+
max_size_mb: int = 10
|
|
106
|
+
backup_count: int = 5
|
|
107
|
+
|
|
108
|
+
def __post_init__(self):
|
|
109
|
+
"""Validate logging configuration."""
|
|
110
|
+
valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
|
111
|
+
if self.level.upper() not in valid_levels:
|
|
112
|
+
raise ValueError(f"Invalid log level: {self.level}. Must be one of {valid_levels}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class KubernetesConfig:
|
|
117
|
+
"""Kubernetes client configuration."""
|
|
118
|
+
|
|
119
|
+
context: Optional[str] = None
|
|
120
|
+
kubeconfig: Optional[str] = None
|
|
121
|
+
in_cluster: bool = False
|
|
122
|
+
default_namespace: str = "default"
|
|
123
|
+
timeout: int = 30
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class Config:
|
|
128
|
+
"""Root configuration container."""
|
|
129
|
+
|
|
130
|
+
server: ServerConfig = field(default_factory=ServerConfig)
|
|
131
|
+
safety: SafetyConfig = field(default_factory=SafetyConfig)
|
|
132
|
+
browser: BrowserConfig = field(default_factory=BrowserConfig)
|
|
133
|
+
metrics: MetricsConfig = field(default_factory=MetricsConfig)
|
|
134
|
+
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
|
135
|
+
kubernetes: KubernetesConfig = field(default_factory=KubernetesConfig)
|
|
136
|
+
|
|
137
|
+
# Custom settings from drop-in configs
|
|
138
|
+
custom: Dict[str, Any] = field(default_factory=dict)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def validate_config(config_dict: Dict[str, Any]) -> List[str]:
|
|
142
|
+
"""Validate configuration dictionary and return list of errors.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
config_dict: Configuration dictionary to validate
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of validation error messages (empty if valid)
|
|
149
|
+
"""
|
|
150
|
+
errors = []
|
|
151
|
+
|
|
152
|
+
# Validate server section
|
|
153
|
+
if "server" in config_dict:
|
|
154
|
+
server = config_dict["server"]
|
|
155
|
+
valid_transports = {"stdio", "sse", "streamable-http"}
|
|
156
|
+
if server.get("transport") and server["transport"] not in valid_transports:
|
|
157
|
+
errors.append(f"Invalid server.transport: {server['transport']}")
|
|
158
|
+
|
|
159
|
+
port = server.get("port")
|
|
160
|
+
if port is not None and not (1 <= port <= 65535):
|
|
161
|
+
errors.append(f"Invalid server.port: {port}")
|
|
162
|
+
|
|
163
|
+
# Validate safety section
|
|
164
|
+
if "safety" in config_dict:
|
|
165
|
+
safety = config_dict["safety"]
|
|
166
|
+
valid_modes = {"normal", "read-only", "disable-destructive"}
|
|
167
|
+
if safety.get("mode") and safety["mode"] not in valid_modes:
|
|
168
|
+
errors.append(f"Invalid safety.mode: {safety['mode']}")
|
|
169
|
+
|
|
170
|
+
# Validate browser section
|
|
171
|
+
if "browser" in config_dict:
|
|
172
|
+
browser = config_dict["browser"]
|
|
173
|
+
valid_providers = {"local", "browserbase", "browseruse", "cdp"}
|
|
174
|
+
if browser.get("provider") and browser["provider"] not in valid_providers:
|
|
175
|
+
errors.append(f"Invalid browser.provider: {browser['provider']}")
|
|
176
|
+
|
|
177
|
+
# Validate metrics section
|
|
178
|
+
if "metrics" in config_dict:
|
|
179
|
+
metrics = config_dict["metrics"]
|
|
180
|
+
sample_rate = metrics.get("sample_rate")
|
|
181
|
+
if sample_rate is not None and not (0.0 <= sample_rate <= 1.0):
|
|
182
|
+
errors.append(f"Invalid metrics.sample_rate: {sample_rate}")
|
|
183
|
+
|
|
184
|
+
return errors
|