kubectl-mcp-server 1.16.0__tar.gz → 1.17.0__tar.gz
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.16.0 → kubectl_mcp_server-1.17.0}/PKG-INFO +1 -1
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_server.egg-info/PKG-INFO +1 -1
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_server.egg-info/SOURCES.txt +14 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/__init__.py +1 -1
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/cli/cli.py +83 -9
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/cli/output.py +14 -0
- kubectl_mcp_server-1.17.0/kubectl_mcp_tool/config/__init__.py +46 -0
- kubectl_mcp_server-1.17.0/kubectl_mcp_tool/config/loader.py +386 -0
- kubectl_mcp_server-1.17.0/kubectl_mcp_tool/config/schema.py +184 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/mcp_server.py +219 -8
- kubectl_mcp_server-1.17.0/kubectl_mcp_tool/observability/__init__.py +59 -0
- kubectl_mcp_server-1.17.0/kubectl_mcp_tool/observability/metrics.py +223 -0
- kubectl_mcp_server-1.17.0/kubectl_mcp_tool/observability/stats.py +255 -0
- kubectl_mcp_server-1.17.0/kubectl_mcp_tool/observability/tracing.py +335 -0
- kubectl_mcp_server-1.17.0/kubectl_mcp_tool/prompts/__init__.py +48 -0
- kubectl_mcp_server-1.17.0/kubectl_mcp_tool/prompts/builtin.py +695 -0
- kubectl_mcp_server-1.17.0/kubectl_mcp_tool/prompts/custom.py +298 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/prompts/prompts.py +180 -4
- kubectl_mcp_server-1.17.0/kubectl_mcp_tool/safety.py +155 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/cluster.py +384 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/setup.py +1 -1
- kubectl_mcp_server-1.17.0/tests/test_config.py +386 -0
- kubectl_mcp_server-1.17.0/tests/test_mcp_integration.py +251 -0
- kubectl_mcp_server-1.17.0/tests/test_observability.py +521 -0
- kubectl_mcp_server-1.17.0/tests/test_prompts.py +1252 -0
- kubectl_mcp_server-1.17.0/tests/test_safety.py +218 -0
- kubectl_mcp_server-1.16.0/kubectl_mcp_tool/prompts/__init__.py +0 -5
- kubectl_mcp_server-1.16.0/tests/test_prompts.py +0 -536
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/LICENSE +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/README.md +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_server.egg-info/dependency_links.txt +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_server.egg-info/entry_points.txt +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_server.egg-info/requires.txt +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_server.egg-info/top_level.txt +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/__main__.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/auth/__init__.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/auth/config.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/auth/scopes.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/auth/verifier.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/cli/__init__.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/cli/__main__.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/cli/errors.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/crd_detector.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/diagnostics.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/k8s_config.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/resources/__init__.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/resources/resources.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/__init__.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/backup.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/browser.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/capi.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/certs.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/cilium.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/core.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/cost.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/deployments.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/diagnostics.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/gitops.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/helm.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/keda.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/kiali.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/kubevirt.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/networking.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/operations.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/pods.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/policy.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/rollouts.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/security.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/storage.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/tools/ui.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/utils/__init__.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_tool/utils/helpers.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/setup.cfg +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/tests/__init__.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/tests/conftest.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/tests/test_auth.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/tests/test_browser.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/tests/test_cli.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/tests/test_ecosystem.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/tests/test_resources.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/tests/test_server.py +0 -0
- {kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kubectl-mcp-server
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.17.0
|
|
4
4
|
Summary: A Model Context Protocol (MCP) server for Kubernetes with 220+ tools, 8 resources, and 8 prompts
|
|
5
5
|
Home-page: https://github.com/rohitg00/kubectl-mcp-server
|
|
6
6
|
Author: Rohit Ghumare
|
{kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_server.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kubectl-mcp-server
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.17.0
|
|
4
4
|
Summary: A Model Context Protocol (MCP) server for Kubernetes with 220+ tools, 8 resources, and 8 prompts
|
|
5
5
|
Home-page: https://github.com/rohitg00/kubectl-mcp-server
|
|
6
6
|
Author: Rohit Ghumare
|
{kubectl_mcp_server-1.16.0 → kubectl_mcp_server-1.17.0}/kubectl_mcp_server.egg-info/SOURCES.txt
RENAMED
|
@@ -13,6 +13,7 @@ kubectl_mcp_tool/crd_detector.py
|
|
|
13
13
|
kubectl_mcp_tool/diagnostics.py
|
|
14
14
|
kubectl_mcp_tool/k8s_config.py
|
|
15
15
|
kubectl_mcp_tool/mcp_server.py
|
|
16
|
+
kubectl_mcp_tool/safety.py
|
|
16
17
|
kubectl_mcp_tool/auth/__init__.py
|
|
17
18
|
kubectl_mcp_tool/auth/config.py
|
|
18
19
|
kubectl_mcp_tool/auth/scopes.py
|
|
@@ -22,7 +23,16 @@ kubectl_mcp_tool/cli/__main__.py
|
|
|
22
23
|
kubectl_mcp_tool/cli/cli.py
|
|
23
24
|
kubectl_mcp_tool/cli/errors.py
|
|
24
25
|
kubectl_mcp_tool/cli/output.py
|
|
26
|
+
kubectl_mcp_tool/config/__init__.py
|
|
27
|
+
kubectl_mcp_tool/config/loader.py
|
|
28
|
+
kubectl_mcp_tool/config/schema.py
|
|
29
|
+
kubectl_mcp_tool/observability/__init__.py
|
|
30
|
+
kubectl_mcp_tool/observability/metrics.py
|
|
31
|
+
kubectl_mcp_tool/observability/stats.py
|
|
32
|
+
kubectl_mcp_tool/observability/tracing.py
|
|
25
33
|
kubectl_mcp_tool/prompts/__init__.py
|
|
34
|
+
kubectl_mcp_tool/prompts/builtin.py
|
|
35
|
+
kubectl_mcp_tool/prompts/custom.py
|
|
26
36
|
kubectl_mcp_tool/prompts/prompts.py
|
|
27
37
|
kubectl_mcp_tool/resources/__init__.py
|
|
28
38
|
kubectl_mcp_tool/resources/resources.py
|
|
@@ -57,8 +67,12 @@ tests/conftest.py
|
|
|
57
67
|
tests/test_auth.py
|
|
58
68
|
tests/test_browser.py
|
|
59
69
|
tests/test_cli.py
|
|
70
|
+
tests/test_config.py
|
|
60
71
|
tests/test_ecosystem.py
|
|
72
|
+
tests/test_mcp_integration.py
|
|
73
|
+
tests/test_observability.py
|
|
61
74
|
tests/test_prompts.py
|
|
62
75
|
tests/test_resources.py
|
|
76
|
+
tests/test_safety.py
|
|
63
77
|
tests/test_server.py
|
|
64
78
|
tests/test_tools.py
|
|
@@ -7,7 +7,7 @@ with Kubernetes clusters through natural language commands.
|
|
|
7
7
|
For more information, see: https://github.com/rohitg00/kubectl-mcp-server
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
__version__ = "1.
|
|
10
|
+
__version__ = "1.17.0"
|
|
11
11
|
|
|
12
12
|
from .mcp_server import MCPServer
|
|
13
13
|
from .diagnostics import run_diagnostics, check_kubectl_installation, check_cluster_connection
|
|
@@ -37,6 +37,7 @@ from .output import (
|
|
|
37
37
|
format_error,
|
|
38
38
|
format_success,
|
|
39
39
|
)
|
|
40
|
+
from ..safety import SafetyMode, set_safety_mode, get_mode_info
|
|
40
41
|
|
|
41
42
|
# Logging setup
|
|
42
43
|
log_file = os.environ.get("MCP_LOG_FILE")
|
|
@@ -56,18 +57,49 @@ logging.basicConfig(
|
|
|
56
57
|
logger = logging.getLogger("kubectl-mcp-cli")
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
async def serve_stdio(
|
|
60
|
-
|
|
60
|
+
async def serve_stdio(
|
|
61
|
+
read_only: bool = False,
|
|
62
|
+
disable_destructive: bool = False,
|
|
63
|
+
config_file: Optional[str] = None,
|
|
64
|
+
):
|
|
65
|
+
server = MCPServer(
|
|
66
|
+
"kubernetes",
|
|
67
|
+
read_only=read_only,
|
|
68
|
+
disable_destructive=disable_destructive,
|
|
69
|
+
config_file=config_file,
|
|
70
|
+
)
|
|
61
71
|
await server.serve_stdio()
|
|
62
72
|
|
|
63
73
|
|
|
64
|
-
async def serve_sse(
|
|
65
|
-
|
|
74
|
+
async def serve_sse(
|
|
75
|
+
host: str,
|
|
76
|
+
port: int,
|
|
77
|
+
read_only: bool = False,
|
|
78
|
+
disable_destructive: bool = False,
|
|
79
|
+
config_file: Optional[str] = None,
|
|
80
|
+
):
|
|
81
|
+
server = MCPServer(
|
|
82
|
+
"kubernetes",
|
|
83
|
+
read_only=read_only,
|
|
84
|
+
disable_destructive=disable_destructive,
|
|
85
|
+
config_file=config_file,
|
|
86
|
+
)
|
|
66
87
|
await server.serve_sse(host=host, port=port)
|
|
67
88
|
|
|
68
89
|
|
|
69
|
-
async def serve_http(
|
|
70
|
-
|
|
90
|
+
async def serve_http(
|
|
91
|
+
host: str,
|
|
92
|
+
port: int,
|
|
93
|
+
read_only: bool = False,
|
|
94
|
+
disable_destructive: bool = False,
|
|
95
|
+
config_file: Optional[str] = None,
|
|
96
|
+
):
|
|
97
|
+
server = MCPServer(
|
|
98
|
+
"kubernetes",
|
|
99
|
+
read_only=read_only,
|
|
100
|
+
disable_destructive=disable_destructive,
|
|
101
|
+
config_file=config_file,
|
|
102
|
+
)
|
|
71
103
|
await server.serve_http(host=host, port=port)
|
|
72
104
|
|
|
73
105
|
|
|
@@ -282,12 +314,16 @@ def cmd_info(args):
|
|
|
282
314
|
except Exception:
|
|
283
315
|
pass
|
|
284
316
|
|
|
317
|
+
# Get safety mode info
|
|
318
|
+
mode_info = get_mode_info()
|
|
319
|
+
|
|
285
320
|
print(format_server_info(
|
|
286
321
|
version=__version__,
|
|
287
322
|
tool_count=len(tools),
|
|
288
323
|
resource_count=len(resources),
|
|
289
324
|
prompt_count=len(prompts),
|
|
290
325
|
context=context,
|
|
326
|
+
safety_mode=mode_info,
|
|
291
327
|
as_json=getattr(args, 'json', False)
|
|
292
328
|
))
|
|
293
329
|
return ErrorCode.SUCCESS
|
|
@@ -407,6 +443,16 @@ def cmd_doctor(args):
|
|
|
407
443
|
except ImportError:
|
|
408
444
|
checks.append({"name": "fastmcp", "status": "error", "details": "fastmcp not installed"})
|
|
409
445
|
|
|
446
|
+
# Check safety mode
|
|
447
|
+
mode_info = get_mode_info()
|
|
448
|
+
mode = mode_info.get("mode", "normal")
|
|
449
|
+
if mode == "normal":
|
|
450
|
+
checks.append({"name": "safety_mode", "status": "ok", "version": mode, "details": "All operations allowed"})
|
|
451
|
+
elif mode == "read_only":
|
|
452
|
+
checks.append({"name": "safety_mode", "status": "warning", "version": mode, "details": "Write operations blocked"})
|
|
453
|
+
else:
|
|
454
|
+
checks.append({"name": "safety_mode", "status": "warning", "version": mode, "details": "Destructive operations blocked"})
|
|
455
|
+
|
|
410
456
|
print(format_doctor_results(checks, as_json=getattr(args, 'json', False)))
|
|
411
457
|
|
|
412
458
|
# Return error code if any critical checks failed
|
|
@@ -453,6 +499,9 @@ Environment Variables:
|
|
|
453
499
|
)
|
|
454
500
|
serve_parser.add_argument("--host", type=str, default="0.0.0.0", help="Host for SSE/HTTP (default: 0.0.0.0)")
|
|
455
501
|
serve_parser.add_argument("--port", type=int, default=8000, help="Port for SSE/HTTP (default: 8000)")
|
|
502
|
+
serve_parser.add_argument("--config", type=str, default=None, help="Path to TOML configuration file")
|
|
503
|
+
serve_parser.add_argument("--read-only", action="store_true", help="Enable read-only mode (block all write operations)")
|
|
504
|
+
serve_parser.add_argument("--disable-destructive", action="store_true", help="Disable destructive operations (allow create/update, block delete)")
|
|
456
505
|
|
|
457
506
|
# version command (existing)
|
|
458
507
|
subparsers.add_parser("version", help="Show version")
|
|
@@ -507,12 +556,37 @@ Environment Variables:
|
|
|
507
556
|
|
|
508
557
|
try:
|
|
509
558
|
if args.command == "serve":
|
|
559
|
+
# Log safety mode (actual mode is applied in MCPServer)
|
|
560
|
+
if args.read_only:
|
|
561
|
+
logger.info("Starting server in READ-ONLY mode")
|
|
562
|
+
elif args.disable_destructive:
|
|
563
|
+
logger.info("Starting server with destructive operations disabled")
|
|
564
|
+
|
|
565
|
+
if args.config:
|
|
566
|
+
logger.info(f"Loading configuration from: {args.config}")
|
|
567
|
+
|
|
510
568
|
if args.transport == "stdio":
|
|
511
|
-
asyncio.run(serve_stdio(
|
|
569
|
+
asyncio.run(serve_stdio(
|
|
570
|
+
read_only=args.read_only,
|
|
571
|
+
disable_destructive=args.disable_destructive,
|
|
572
|
+
config_file=args.config,
|
|
573
|
+
))
|
|
512
574
|
elif args.transport == "sse":
|
|
513
|
-
asyncio.run(serve_sse(
|
|
575
|
+
asyncio.run(serve_sse(
|
|
576
|
+
host=args.host,
|
|
577
|
+
port=args.port,
|
|
578
|
+
read_only=args.read_only,
|
|
579
|
+
disable_destructive=args.disable_destructive,
|
|
580
|
+
config_file=args.config,
|
|
581
|
+
))
|
|
514
582
|
elif args.transport in ("http", "streamable-http"):
|
|
515
|
-
asyncio.run(serve_http(
|
|
583
|
+
asyncio.run(serve_http(
|
|
584
|
+
host=args.host,
|
|
585
|
+
port=args.port,
|
|
586
|
+
read_only=args.read_only,
|
|
587
|
+
disable_destructive=args.disable_destructive,
|
|
588
|
+
config_file=args.config,
|
|
589
|
+
))
|
|
516
590
|
|
|
517
591
|
elif args.command == "version":
|
|
518
592
|
from .. import __version__
|
|
@@ -266,6 +266,7 @@ def format_server_info(
|
|
|
266
266
|
resource_count: int,
|
|
267
267
|
prompt_count: int,
|
|
268
268
|
context: Optional[str] = None,
|
|
269
|
+
safety_mode: Optional[Dict[str, Any]] = None,
|
|
269
270
|
as_json: bool = False
|
|
270
271
|
) -> str:
|
|
271
272
|
info = {
|
|
@@ -276,6 +277,9 @@ def format_server_info(
|
|
|
276
277
|
"k8s_context": context,
|
|
277
278
|
}
|
|
278
279
|
|
|
280
|
+
if safety_mode:
|
|
281
|
+
info["safety_mode"] = safety_mode
|
|
282
|
+
|
|
279
283
|
if as_json:
|
|
280
284
|
return json.dumps(info, indent=2)
|
|
281
285
|
|
|
@@ -291,6 +295,16 @@ def format_server_info(
|
|
|
291
295
|
if context:
|
|
292
296
|
lines.append(f" {cyan('K8s Context:')} {context}")
|
|
293
297
|
|
|
298
|
+
if safety_mode:
|
|
299
|
+
mode = safety_mode.get("mode", "normal")
|
|
300
|
+
if mode == "normal":
|
|
301
|
+
mode_str = green(mode)
|
|
302
|
+
elif mode == "read_only":
|
|
303
|
+
mode_str = yellow(mode) + " (write operations blocked)"
|
|
304
|
+
else:
|
|
305
|
+
mode_str = yellow(mode) + " (destructive operations blocked)"
|
|
306
|
+
lines.append(f" {cyan('Safety Mode:')} {mode_str}")
|
|
307
|
+
|
|
294
308
|
return "\n".join(lines)
|
|
295
309
|
|
|
296
310
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Configuration management for kubectl-mcp-server.
|
|
2
|
+
|
|
3
|
+
This module provides TOML-based configuration with:
|
|
4
|
+
- Main config file support
|
|
5
|
+
- Drop-in directory for modular configuration
|
|
6
|
+
- SIGHUP reload for runtime updates
|
|
7
|
+
- Environment variable overrides
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .loader import (
|
|
11
|
+
Config,
|
|
12
|
+
load_config,
|
|
13
|
+
get_config,
|
|
14
|
+
reload_config,
|
|
15
|
+
get_config_paths,
|
|
16
|
+
setup_sighup_handler,
|
|
17
|
+
register_reload_callback,
|
|
18
|
+
unregister_reload_callback,
|
|
19
|
+
)
|
|
20
|
+
from .schema import (
|
|
21
|
+
ServerConfig,
|
|
22
|
+
SafetyConfig,
|
|
23
|
+
BrowserConfig,
|
|
24
|
+
MetricsConfig,
|
|
25
|
+
LoggingConfig,
|
|
26
|
+
validate_config,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# Config loading
|
|
31
|
+
"Config",
|
|
32
|
+
"load_config",
|
|
33
|
+
"get_config",
|
|
34
|
+
"reload_config",
|
|
35
|
+
"get_config_paths",
|
|
36
|
+
"setup_sighup_handler",
|
|
37
|
+
"register_reload_callback",
|
|
38
|
+
"unregister_reload_callback",
|
|
39
|
+
# Config schemas
|
|
40
|
+
"ServerConfig",
|
|
41
|
+
"SafetyConfig",
|
|
42
|
+
"BrowserConfig",
|
|
43
|
+
"MetricsConfig",
|
|
44
|
+
"LoggingConfig",
|
|
45
|
+
"validate_config",
|
|
46
|
+
]
|
|
@@ -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
|