kubectl-mcp-server 1.16.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.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/METADATA +1 -1
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/RECORD +28 -14
- 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/mcp_server.py +219 -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/cluster.py +384 -0
- tests/test_config.py +386 -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
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/top_level.txt +0 -0
kubectl_mcp_tool/mcp_server.py
CHANGED
|
@@ -23,12 +23,49 @@ import logging
|
|
|
23
23
|
import asyncio
|
|
24
24
|
import os
|
|
25
25
|
import platform
|
|
26
|
-
|
|
26
|
+
import signal
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import List, Optional, Any, Dict
|
|
27
29
|
|
|
28
30
|
# Import k8s_config early to patch kubernetes config for in-cluster support
|
|
29
31
|
# This must be done before any tools are imported
|
|
30
32
|
import kubectl_mcp_tool.k8s_config # noqa: F401
|
|
31
33
|
|
|
34
|
+
# Import safety mode for operation control
|
|
35
|
+
from kubectl_mcp_tool.safety import (
|
|
36
|
+
SafetyMode,
|
|
37
|
+
set_safety_mode,
|
|
38
|
+
get_safety_mode,
|
|
39
|
+
get_mode_info,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Import observability for metrics and tracing
|
|
43
|
+
from kubectl_mcp_tool.observability import (
|
|
44
|
+
get_stats_collector,
|
|
45
|
+
get_metrics,
|
|
46
|
+
init_tracing,
|
|
47
|
+
shutdown_tracing,
|
|
48
|
+
is_prometheus_available,
|
|
49
|
+
is_tracing_available,
|
|
50
|
+
record_tool_call_metric,
|
|
51
|
+
record_tool_duration_metric,
|
|
52
|
+
record_tool_error_metric,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Import config loader
|
|
56
|
+
from kubectl_mcp_tool.config import (
|
|
57
|
+
load_config,
|
|
58
|
+
get_config,
|
|
59
|
+
register_reload_callback,
|
|
60
|
+
setup_sighup_handler,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Import custom prompts
|
|
64
|
+
from kubectl_mcp_tool.prompts import (
|
|
65
|
+
load_prompts_from_config,
|
|
66
|
+
get_builtin_prompts,
|
|
67
|
+
)
|
|
68
|
+
|
|
32
69
|
from kubectl_mcp_tool.tools import (
|
|
33
70
|
register_helm_tools,
|
|
34
71
|
register_pod_tools,
|
|
@@ -116,12 +153,20 @@ except ImportError:
|
|
|
116
153
|
class MCPServer:
|
|
117
154
|
"""MCP server implementation."""
|
|
118
155
|
|
|
119
|
-
def __init__(
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
name: str,
|
|
159
|
+
read_only: bool = False,
|
|
160
|
+
disable_destructive: bool = False,
|
|
161
|
+
config_file: Optional[str] = None,
|
|
162
|
+
):
|
|
120
163
|
"""Initialize the MCP server.
|
|
121
164
|
|
|
122
165
|
Args:
|
|
123
166
|
name: Server name for identification
|
|
124
|
-
|
|
167
|
+
read_only: If True, block all write operations (read-only mode)
|
|
168
|
+
disable_destructive: If True, block only destructive operations
|
|
169
|
+
config_file: Optional path to TOML config file
|
|
125
170
|
|
|
126
171
|
Environment Variables:
|
|
127
172
|
MCP_AUTH_ENABLED: Enable OAuth 2.1 authentication (default: false)
|
|
@@ -131,9 +176,29 @@ class MCPServer:
|
|
|
131
176
|
MCP_AUTH_REQUIRED_SCOPES: Required scopes (default: mcp:tools)
|
|
132
177
|
"""
|
|
133
178
|
self.name = name
|
|
134
|
-
self.non_destructive = non_destructive
|
|
135
179
|
self._dependencies_checked = False
|
|
136
180
|
self._dependencies_available = None
|
|
181
|
+
self._stats = get_stats_collector()
|
|
182
|
+
|
|
183
|
+
# Persist CLI safety overrides for reloads
|
|
184
|
+
self._cli_read_only = read_only
|
|
185
|
+
self._cli_disable_destructive = disable_destructive
|
|
186
|
+
|
|
187
|
+
# Load configuration from file and environment
|
|
188
|
+
self.config = self._load_configuration(config_file)
|
|
189
|
+
|
|
190
|
+
# Apply safety mode from config or parameters
|
|
191
|
+
self._apply_safety_mode(self._cli_read_only, self._cli_disable_destructive)
|
|
192
|
+
|
|
193
|
+
# For backward compatibility, expose non_destructive
|
|
194
|
+
self.non_destructive = get_safety_mode() != SafetyMode.NORMAL
|
|
195
|
+
|
|
196
|
+
# Initialize observability (tracing, metrics)
|
|
197
|
+
self._init_observability()
|
|
198
|
+
|
|
199
|
+
# Register config reload callback and set up SIGHUP handler
|
|
200
|
+
register_reload_callback(self._on_config_reload)
|
|
201
|
+
setup_sighup_handler()
|
|
137
202
|
|
|
138
203
|
# Load authentication configuration
|
|
139
204
|
self.auth_config = get_auth_config()
|
|
@@ -150,6 +215,71 @@ class MCPServer:
|
|
|
150
215
|
self.setup_resources()
|
|
151
216
|
self.setup_prompts()
|
|
152
217
|
|
|
218
|
+
# Log startup info
|
|
219
|
+
mode_info = get_mode_info()
|
|
220
|
+
logger.info(f"MCP Server initialized: {name}")
|
|
221
|
+
logger.info(f"Safety mode: {mode_info['mode']} - {mode_info['description']}")
|
|
222
|
+
|
|
223
|
+
def _load_configuration(self, config_file: Optional[str]) -> Any:
|
|
224
|
+
"""Load configuration from TOML file and environment."""
|
|
225
|
+
try:
|
|
226
|
+
config = load_config(config_file=config_file if config_file else None)
|
|
227
|
+
logger.debug(f"Configuration loaded successfully")
|
|
228
|
+
return config
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.warning(f"Failed to load config file: {e}. Using defaults.")
|
|
231
|
+
return load_config(skip_env=False)
|
|
232
|
+
|
|
233
|
+
def _apply_safety_mode(self, read_only: bool, disable_destructive: bool) -> None:
|
|
234
|
+
"""Apply safety mode from config or CLI parameters.
|
|
235
|
+
|
|
236
|
+
CLI parameters take precedence over config file settings.
|
|
237
|
+
"""
|
|
238
|
+
# Check config first
|
|
239
|
+
config_mode = getattr(self.config.safety, 'mode', 'normal') if hasattr(self.config, 'safety') else 'normal'
|
|
240
|
+
|
|
241
|
+
# CLI parameters override config
|
|
242
|
+
if read_only:
|
|
243
|
+
set_safety_mode(SafetyMode.READ_ONLY)
|
|
244
|
+
elif disable_destructive:
|
|
245
|
+
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
246
|
+
elif config_mode == 'read-only' or config_mode == 'read_only':
|
|
247
|
+
set_safety_mode(SafetyMode.READ_ONLY)
|
|
248
|
+
elif config_mode == 'disable-destructive' or config_mode == 'disable_destructive':
|
|
249
|
+
set_safety_mode(SafetyMode.DISABLE_DESTRUCTIVE)
|
|
250
|
+
else:
|
|
251
|
+
set_safety_mode(SafetyMode.NORMAL)
|
|
252
|
+
|
|
253
|
+
def _init_observability(self) -> None:
|
|
254
|
+
"""Initialize observability components (tracing, metrics)."""
|
|
255
|
+
# Check if tracing is enabled in config
|
|
256
|
+
tracing_enabled = getattr(self.config.metrics, 'tracing_enabled', False) if hasattr(self.config, 'metrics') else False
|
|
257
|
+
otlp_endpoint = getattr(self.config.metrics, 'otlp_endpoint', None) if hasattr(self.config, 'metrics') else None
|
|
258
|
+
|
|
259
|
+
if tracing_enabled or otlp_endpoint or os.environ.get('OTEL_EXPORTER_OTLP_ENDPOINT'):
|
|
260
|
+
try:
|
|
261
|
+
init_tracing()
|
|
262
|
+
logger.info("OpenTelemetry tracing initialized")
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.warning(f"Failed to initialize tracing: {e}")
|
|
265
|
+
|
|
266
|
+
if is_prometheus_available():
|
|
267
|
+
logger.debug("Prometheus metrics available")
|
|
268
|
+
|
|
269
|
+
def _on_config_reload(self, new_config: Any) -> None:
|
|
270
|
+
"""Handle configuration reload (called on SIGHUP)."""
|
|
271
|
+
logger.info("Configuration reloaded")
|
|
272
|
+
self.config = new_config
|
|
273
|
+
|
|
274
|
+
# Re-apply safety mode from new config, honoring CLI precedence
|
|
275
|
+
self._apply_safety_mode(self._cli_read_only, self._cli_disable_destructive)
|
|
276
|
+
|
|
277
|
+
# Refresh non_destructive flag
|
|
278
|
+
self.non_destructive = get_safety_mode() != SafetyMode.NORMAL
|
|
279
|
+
|
|
280
|
+
mode_info = get_mode_info()
|
|
281
|
+
logger.info(f"Safety mode after reload: {mode_info['mode']}")
|
|
282
|
+
|
|
153
283
|
def _setup_auth(self) -> Optional[Any]:
|
|
154
284
|
"""Set up authentication if enabled."""
|
|
155
285
|
if not self.auth_config.enabled:
|
|
@@ -228,8 +358,12 @@ class MCPServer:
|
|
|
228
358
|
register_resources(self.server)
|
|
229
359
|
|
|
230
360
|
def setup_prompts(self):
|
|
231
|
-
"""Set up MCP prompts."""
|
|
232
|
-
|
|
361
|
+
"""Set up MCP prompts from built-in and custom config."""
|
|
362
|
+
# Get custom prompts path from config if specified
|
|
363
|
+
prompts_config_path = None
|
|
364
|
+
if hasattr(self.config, 'prompts') and hasattr(self.config.prompts, 'file'):
|
|
365
|
+
prompts_config_path = self.config.prompts.file
|
|
366
|
+
register_prompts(self.server, config_path=prompts_config_path)
|
|
233
367
|
|
|
234
368
|
def _check_dependencies(self) -> bool:
|
|
235
369
|
"""Check if required dependencies are available."""
|
|
@@ -358,17 +492,51 @@ class MCPServer:
|
|
|
358
492
|
try:
|
|
359
493
|
# FastMCP 3 uses create_sse_app() to create a Starlette ASGI app
|
|
360
494
|
from fastmcp.server.http import create_sse_app
|
|
495
|
+
from starlette.applications import Starlette
|
|
496
|
+
from starlette.responses import JSONResponse, PlainTextResponse
|
|
497
|
+
from starlette.routing import Route, Mount
|
|
498
|
+
|
|
499
|
+
# Create observability endpoints
|
|
500
|
+
async def health_check(request):
|
|
501
|
+
return JSONResponse({"status": "healthy", "server": self.name})
|
|
502
|
+
|
|
503
|
+
async def stats_endpoint(request):
|
|
504
|
+
stats = self._stats.get_stats()
|
|
505
|
+
return JSONResponse(stats)
|
|
506
|
+
|
|
507
|
+
async def metrics_endpoint(request):
|
|
508
|
+
if is_prometheus_available():
|
|
509
|
+
metrics_text = get_metrics()
|
|
510
|
+
return PlainTextResponse(metrics_text, media_type="text/plain; version=0.0.4; charset=utf-8")
|
|
511
|
+
else:
|
|
512
|
+
return PlainTextResponse("# Prometheus metrics not available\n", media_type="text/plain")
|
|
513
|
+
|
|
514
|
+
async def safety_mode_endpoint(request):
|
|
515
|
+
mode_info = get_mode_info()
|
|
516
|
+
return JSONResponse(mode_info)
|
|
361
517
|
|
|
362
518
|
# Create the SSE Starlette application
|
|
363
519
|
# message_path: POST endpoint for client messages
|
|
364
520
|
# sse_path: GET endpoint for SSE event stream
|
|
365
|
-
|
|
521
|
+
sse_app = create_sse_app(
|
|
366
522
|
self.server,
|
|
367
523
|
message_path="/messages/",
|
|
368
524
|
sse_path="/sse"
|
|
369
525
|
)
|
|
370
526
|
|
|
527
|
+
# Create combined app with SSE and observability endpoints
|
|
528
|
+
app = Starlette(
|
|
529
|
+
routes=[
|
|
530
|
+
Route("/health", health_check, methods=["GET"]),
|
|
531
|
+
Route("/stats", stats_endpoint, methods=["GET"]),
|
|
532
|
+
Route("/metrics", metrics_endpoint, methods=["GET"]),
|
|
533
|
+
Route("/safety", safety_mode_endpoint, methods=["GET"]),
|
|
534
|
+
Mount("/", app=sse_app), # Mount SSE app at root
|
|
535
|
+
]
|
|
536
|
+
)
|
|
537
|
+
|
|
371
538
|
logger.info(f"SSE endpoints: GET /sse (events), POST /messages/ (messages)")
|
|
539
|
+
logger.info(f"Observability endpoints: GET /health, /stats, /metrics, /safety")
|
|
372
540
|
|
|
373
541
|
# Run with uvicorn
|
|
374
542
|
config = uvicorn.Config(app, host=host, port=port, log_level="info")
|
|
@@ -498,11 +666,33 @@ class MCPServer:
|
|
|
498
666
|
"""Health check endpoint."""
|
|
499
667
|
return JSONResponse({"status": "healthy", "server": self.name})
|
|
500
668
|
|
|
669
|
+
async def stats_endpoint(request):
|
|
670
|
+
"""Return runtime statistics."""
|
|
671
|
+
stats = self._stats.get_stats()
|
|
672
|
+
return JSONResponse(stats)
|
|
673
|
+
|
|
674
|
+
async def metrics_endpoint(request):
|
|
675
|
+
"""Return Prometheus-format metrics."""
|
|
676
|
+
from starlette.responses import PlainTextResponse
|
|
677
|
+
if is_prometheus_available():
|
|
678
|
+
metrics_text = get_metrics()
|
|
679
|
+
return PlainTextResponse(metrics_text, media_type="text/plain; version=0.0.4; charset=utf-8")
|
|
680
|
+
else:
|
|
681
|
+
return PlainTextResponse("# Prometheus metrics not available\n", media_type="text/plain")
|
|
682
|
+
|
|
683
|
+
async def safety_mode_endpoint(request):
|
|
684
|
+
"""Return current safety mode information."""
|
|
685
|
+
mode_info = get_mode_info()
|
|
686
|
+
return JSONResponse(mode_info)
|
|
687
|
+
|
|
501
688
|
app = Starlette(
|
|
502
689
|
routes=[
|
|
503
690
|
Route("/", handle_mcp_request, methods=["POST"]),
|
|
504
691
|
Route("/mcp", handle_mcp_request, methods=["POST"]),
|
|
505
692
|
Route("/health", health_check, methods=["GET"]),
|
|
693
|
+
Route("/stats", stats_endpoint, methods=["GET"]),
|
|
694
|
+
Route("/metrics", metrics_endpoint, methods=["GET"]),
|
|
695
|
+
Route("/safety", safety_mode_endpoint, methods=["GET"]),
|
|
506
696
|
]
|
|
507
697
|
)
|
|
508
698
|
|
|
@@ -535,10 +725,31 @@ if __name__ == "__main__":
|
|
|
535
725
|
default="0.0.0.0",
|
|
536
726
|
help="Host to bind to for SSE/HTTP transport. Default: 0.0.0.0.",
|
|
537
727
|
)
|
|
728
|
+
parser.add_argument(
|
|
729
|
+
"--config",
|
|
730
|
+
type=str,
|
|
731
|
+
default=None,
|
|
732
|
+
help="Path to TOML configuration file.",
|
|
733
|
+
)
|
|
734
|
+
parser.add_argument(
|
|
735
|
+
"--read-only",
|
|
736
|
+
action="store_true",
|
|
737
|
+
help="Enable read-only mode (block all write operations).",
|
|
738
|
+
)
|
|
739
|
+
parser.add_argument(
|
|
740
|
+
"--disable-destructive",
|
|
741
|
+
action="store_true",
|
|
742
|
+
help="Disable destructive operations (allow create/update, block delete).",
|
|
743
|
+
)
|
|
538
744
|
args = parser.parse_args()
|
|
539
745
|
|
|
540
746
|
server_name = "kubectl_mcp_server"
|
|
541
|
-
mcp_server = MCPServer(
|
|
747
|
+
mcp_server = MCPServer(
|
|
748
|
+
name=server_name,
|
|
749
|
+
read_only=args.read_only,
|
|
750
|
+
disable_destructive=args.disable_destructive,
|
|
751
|
+
config_file=args.config,
|
|
752
|
+
)
|
|
542
753
|
|
|
543
754
|
# Handle signals gracefully with immediate exit
|
|
544
755
|
def signal_handler(sig, frame):
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Observability module for kubectl-mcp-server.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- StatsCollector: Runtime statistics and metrics collection
|
|
6
|
+
- Prometheus metrics: Standard Prometheus format metrics
|
|
7
|
+
- OpenTelemetry tracing: Distributed tracing with OTLP export
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
# Stats collection
|
|
11
|
+
from kubectl_mcp_tool.observability import get_stats_collector
|
|
12
|
+
stats = get_stats_collector()
|
|
13
|
+
stats.record_tool_call("get_pods", success=True, duration=0.5)
|
|
14
|
+
|
|
15
|
+
# Prometheus metrics
|
|
16
|
+
from kubectl_mcp_tool.observability import get_metrics
|
|
17
|
+
metrics_text = get_metrics()
|
|
18
|
+
|
|
19
|
+
# Tracing
|
|
20
|
+
from kubectl_mcp_tool.observability import init_tracing, traced_tool_call
|
|
21
|
+
init_tracing()
|
|
22
|
+
with traced_tool_call("get_pods") as span:
|
|
23
|
+
# execute tool
|
|
24
|
+
pass
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .stats import StatsCollector, get_stats_collector
|
|
28
|
+
from .metrics import (
|
|
29
|
+
get_metrics,
|
|
30
|
+
record_tool_call_metric,
|
|
31
|
+
record_tool_error_metric,
|
|
32
|
+
record_tool_duration_metric,
|
|
33
|
+
is_prometheus_available,
|
|
34
|
+
)
|
|
35
|
+
from .tracing import (
|
|
36
|
+
init_tracing,
|
|
37
|
+
traced_tool_call,
|
|
38
|
+
get_tracer,
|
|
39
|
+
is_tracing_available,
|
|
40
|
+
shutdown_tracing,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
# Stats
|
|
45
|
+
"StatsCollector",
|
|
46
|
+
"get_stats_collector",
|
|
47
|
+
# Metrics
|
|
48
|
+
"get_metrics",
|
|
49
|
+
"record_tool_call_metric",
|
|
50
|
+
"record_tool_error_metric",
|
|
51
|
+
"record_tool_duration_metric",
|
|
52
|
+
"is_prometheus_available",
|
|
53
|
+
# Tracing
|
|
54
|
+
"init_tracing",
|
|
55
|
+
"traced_tool_call",
|
|
56
|
+
"get_tracer",
|
|
57
|
+
"is_tracing_available",
|
|
58
|
+
"shutdown_tracing",
|
|
59
|
+
]
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prometheus metrics for kubectl-mcp-server.
|
|
3
|
+
|
|
4
|
+
Provides standard Prometheus format metrics for production monitoring.
|
|
5
|
+
|
|
6
|
+
Metrics exposed:
|
|
7
|
+
- mcp_tool_calls_total: Counter of tool invocations (labels: tool_name, status)
|
|
8
|
+
- mcp_tool_errors_total: Counter of tool errors (labels: tool_name, error_type)
|
|
9
|
+
- mcp_tool_duration_seconds: Histogram of tool call durations (labels: tool_name)
|
|
10
|
+
- mcp_http_requests_total: Counter of HTTP requests (labels: endpoint, method, status)
|
|
11
|
+
- mcp_server_info: Gauge with server metadata
|
|
12
|
+
|
|
13
|
+
Requires: prometheus-client>=0.19.0 (optional dependency)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Check if prometheus_client is available
|
|
22
|
+
_prometheus_available = False
|
|
23
|
+
_REGISTRY = None
|
|
24
|
+
_tool_calls_counter = None
|
|
25
|
+
_tool_errors_counter = None
|
|
26
|
+
_tool_duration_histogram = None
|
|
27
|
+
_http_requests_counter = None
|
|
28
|
+
_server_info_gauge = None
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from prometheus_client import (
|
|
32
|
+
Counter,
|
|
33
|
+
Histogram,
|
|
34
|
+
Gauge,
|
|
35
|
+
CollectorRegistry,
|
|
36
|
+
generate_latest,
|
|
37
|
+
CONTENT_TYPE_LATEST,
|
|
38
|
+
)
|
|
39
|
+
_prometheus_available = True
|
|
40
|
+
|
|
41
|
+
# Create a custom registry to avoid conflicts
|
|
42
|
+
_REGISTRY = CollectorRegistry()
|
|
43
|
+
|
|
44
|
+
# Tool call counter
|
|
45
|
+
_tool_calls_counter = Counter(
|
|
46
|
+
"mcp_tool_calls_total",
|
|
47
|
+
"Total number of MCP tool calls",
|
|
48
|
+
["tool_name", "status"],
|
|
49
|
+
registry=_REGISTRY,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Tool error counter
|
|
53
|
+
_tool_errors_counter = Counter(
|
|
54
|
+
"mcp_tool_errors_total",
|
|
55
|
+
"Total number of MCP tool errors",
|
|
56
|
+
["tool_name", "error_type"],
|
|
57
|
+
registry=_REGISTRY,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Tool duration histogram
|
|
61
|
+
# Buckets optimized for typical k8s API call durations
|
|
62
|
+
_tool_duration_histogram = Histogram(
|
|
63
|
+
"mcp_tool_duration_seconds",
|
|
64
|
+
"Duration of MCP tool calls in seconds",
|
|
65
|
+
["tool_name"],
|
|
66
|
+
buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0),
|
|
67
|
+
registry=_REGISTRY,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# HTTP requests counter
|
|
71
|
+
_http_requests_counter = Counter(
|
|
72
|
+
"mcp_http_requests_total",
|
|
73
|
+
"Total number of HTTP requests",
|
|
74
|
+
["endpoint", "method", "status"],
|
|
75
|
+
registry=_REGISTRY,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Server info gauge (version, features)
|
|
79
|
+
_server_info_gauge = Gauge(
|
|
80
|
+
"mcp_server_info",
|
|
81
|
+
"MCP server information",
|
|
82
|
+
["version", "transport"],
|
|
83
|
+
registry=_REGISTRY,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
logger.debug("Prometheus metrics initialized successfully")
|
|
87
|
+
|
|
88
|
+
except ImportError:
|
|
89
|
+
logger.debug(
|
|
90
|
+
"prometheus_client not installed. Prometheus metrics disabled. "
|
|
91
|
+
"Install with: pip install kubectl-mcp-server[observability]"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def is_prometheus_available() -> bool:
|
|
96
|
+
"""Check if Prometheus client is available."""
|
|
97
|
+
return _prometheus_available
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def record_tool_call_metric(
|
|
101
|
+
tool_name: str,
|
|
102
|
+
success: bool = True,
|
|
103
|
+
duration: float = 0.0
|
|
104
|
+
) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Record a tool call in Prometheus metrics.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
tool_name: Name of the tool called
|
|
110
|
+
success: Whether the call succeeded
|
|
111
|
+
duration: Call duration in seconds
|
|
112
|
+
"""
|
|
113
|
+
if not _prometheus_available:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
status = "success" if success else "error"
|
|
117
|
+
_tool_calls_counter.labels(tool_name=tool_name, status=status).inc()
|
|
118
|
+
|
|
119
|
+
if duration > 0:
|
|
120
|
+
_tool_duration_histogram.labels(tool_name=tool_name).observe(duration)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def record_tool_error_metric(
|
|
124
|
+
tool_name: str,
|
|
125
|
+
error_type: str = "unknown"
|
|
126
|
+
) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Record a tool error in Prometheus metrics.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
tool_name: Name of the tool that errored
|
|
132
|
+
error_type: Type/category of error (e.g., "timeout", "validation", "k8s_api")
|
|
133
|
+
"""
|
|
134
|
+
if not _prometheus_available:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
_tool_errors_counter.labels(
|
|
138
|
+
tool_name=tool_name,
|
|
139
|
+
error_type=error_type
|
|
140
|
+
).inc()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def record_tool_duration_metric(tool_name: str, duration: float) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Record tool duration in Prometheus histogram.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
tool_name: Name of the tool
|
|
149
|
+
duration: Duration in seconds
|
|
150
|
+
"""
|
|
151
|
+
if not _prometheus_available:
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
_tool_duration_histogram.labels(tool_name=tool_name).observe(duration)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def record_http_request_metric(
|
|
158
|
+
endpoint: str,
|
|
159
|
+
method: str,
|
|
160
|
+
status: int = 200
|
|
161
|
+
) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Record an HTTP request in Prometheus metrics.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
endpoint: Request endpoint path
|
|
167
|
+
method: HTTP method
|
|
168
|
+
status: HTTP status code
|
|
169
|
+
"""
|
|
170
|
+
if not _prometheus_available:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
_http_requests_counter.labels(
|
|
174
|
+
endpoint=endpoint,
|
|
175
|
+
method=method,
|
|
176
|
+
status=str(status)
|
|
177
|
+
).inc()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def set_server_info(version: str, transport: str) -> None:
|
|
181
|
+
"""
|
|
182
|
+
Set server info in Prometheus gauge.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
version: Server version
|
|
186
|
+
transport: Transport type (stdio, sse, http)
|
|
187
|
+
"""
|
|
188
|
+
if not _prometheus_available:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
_server_info_gauge.labels(version=version, transport=transport).set(1)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_metrics() -> str:
|
|
195
|
+
"""
|
|
196
|
+
Get metrics in Prometheus text format.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Prometheus metrics as text, or error message if unavailable
|
|
200
|
+
"""
|
|
201
|
+
if not _prometheus_available:
|
|
202
|
+
return (
|
|
203
|
+
"# Prometheus metrics not available.\n"
|
|
204
|
+
"# Install with: pip install kubectl-mcp-server[observability]\n"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
return generate_latest(_REGISTRY).decode("utf-8")
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Error generating Prometheus metrics: {e}")
|
|
211
|
+
return f"# Error generating metrics: {e}\n"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def get_metrics_content_type() -> str:
|
|
215
|
+
"""
|
|
216
|
+
Get the content type for Prometheus metrics.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Prometheus content type string
|
|
220
|
+
"""
|
|
221
|
+
if not _prometheus_available:
|
|
222
|
+
return "text/plain; charset=utf-8"
|
|
223
|
+
return CONTENT_TYPE_LATEST
|