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,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
@@ -0,0 +1,255 @@
1
+ """
2
+ Runtime statistics collection for kubectl-mcp-server.
3
+
4
+ Provides a singleton StatsCollector that tracks:
5
+ - tool_calls_total: Total number of tool invocations
6
+ - tool_errors_total: Total number of tool errors
7
+ - tool_calls_by_name: Breakdown of calls by tool name
8
+ - http_requests_total: Total HTTP requests (for SSE/HTTP transports)
9
+ - uptime: Server uptime in seconds
10
+ """
11
+
12
+ import time
13
+ import threading
14
+ from collections import defaultdict
15
+ from dataclasses import dataclass, field
16
+ from typing import Dict, Any, Optional
17
+
18
+
19
+ @dataclass
20
+ class ToolStats:
21
+ """Statistics for a single tool."""
22
+ calls: int = 0
23
+ errors: int = 0
24
+ total_duration: float = 0.0
25
+ last_call_time: Optional[float] = None
26
+ last_error_time: Optional[float] = None
27
+
28
+
29
+ class StatsCollector:
30
+ """
31
+ Singleton class for collecting runtime statistics.
32
+
33
+ Thread-safe statistics collection for production observability.
34
+
35
+ Usage:
36
+ stats = get_stats_collector()
37
+ stats.record_tool_call("get_pods", success=True, duration=0.5)
38
+
39
+ # Get current stats
40
+ data = stats.get_stats()
41
+ """
42
+
43
+ _instance: Optional["StatsCollector"] = None
44
+ _lock = threading.Lock()
45
+
46
+ def __new__(cls) -> "StatsCollector":
47
+ """Ensure singleton pattern."""
48
+ if cls._instance is None:
49
+ with cls._lock:
50
+ if cls._instance is None:
51
+ cls._instance = super().__new__(cls)
52
+ cls._instance._initialized = False
53
+ return cls._instance
54
+
55
+ def __init__(self):
56
+ """Initialize the stats collector (only once)."""
57
+ if self._initialized:
58
+ return
59
+
60
+ self._stats_lock = threading.Lock()
61
+ self._start_time = time.time()
62
+
63
+ # Core counters
64
+ self._tool_calls_total = 0
65
+ self._tool_errors_total = 0
66
+ self._http_requests_total = 0
67
+
68
+ # Per-tool statistics
69
+ self._tool_stats: Dict[str, ToolStats] = defaultdict(ToolStats)
70
+
71
+ # HTTP request breakdown
72
+ self._http_requests_by_endpoint: Dict[str, int] = defaultdict(int)
73
+ self._http_requests_by_method: Dict[str, int] = defaultdict(int)
74
+
75
+ self._initialized = True
76
+
77
+ def record_tool_call(
78
+ self,
79
+ tool_name: str,
80
+ success: bool = True,
81
+ duration: float = 0.0
82
+ ) -> None:
83
+ """
84
+ Record a tool call.
85
+
86
+ Args:
87
+ tool_name: Name of the tool that was called
88
+ success: Whether the call succeeded
89
+ duration: Call duration in seconds
90
+ """
91
+ with self._stats_lock:
92
+ self._tool_calls_total += 1
93
+
94
+ stats = self._tool_stats[tool_name]
95
+ stats.calls += 1
96
+ stats.total_duration += duration
97
+ stats.last_call_time = time.time()
98
+
99
+ if not success:
100
+ self._tool_errors_total += 1
101
+ stats.errors += 1
102
+ stats.last_error_time = time.time()
103
+
104
+ def record_tool_error(self, tool_name: str) -> None:
105
+ """
106
+ Record a tool error (shorthand for failed call).
107
+
108
+ Args:
109
+ tool_name: Name of the tool that errored
110
+ """
111
+ self.record_tool_call(tool_name, success=False)
112
+
113
+ def record_http_request(
114
+ self,
115
+ endpoint: str = "/",
116
+ method: str = "POST"
117
+ ) -> None:
118
+ """
119
+ Record an HTTP request.
120
+
121
+ Args:
122
+ endpoint: Request endpoint path
123
+ method: HTTP method (GET, POST, etc.)
124
+ """
125
+ with self._stats_lock:
126
+ self._http_requests_total += 1
127
+ self._http_requests_by_endpoint[endpoint] += 1
128
+ self._http_requests_by_method[method] += 1
129
+
130
+ @property
131
+ def uptime(self) -> float:
132
+ """Get server uptime in seconds."""
133
+ return time.time() - self._start_time
134
+
135
+ @property
136
+ def tool_calls_total(self) -> int:
137
+ """Get total tool calls."""
138
+ with self._stats_lock:
139
+ return self._tool_calls_total
140
+
141
+ @property
142
+ def tool_errors_total(self) -> int:
143
+ """Get total tool errors."""
144
+ with self._stats_lock:
145
+ return self._tool_errors_total
146
+
147
+ @property
148
+ def http_requests_total(self) -> int:
149
+ """Get total HTTP requests."""
150
+ with self._stats_lock:
151
+ return self._http_requests_total
152
+
153
+ def get_tool_stats(self, tool_name: str) -> Optional[Dict[str, Any]]:
154
+ """
155
+ Get statistics for a specific tool.
156
+
157
+ Args:
158
+ tool_name: Name of the tool
159
+
160
+ Returns:
161
+ Dictionary with tool statistics or None if not found
162
+ """
163
+ with self._stats_lock:
164
+ if tool_name not in self._tool_stats:
165
+ return None
166
+
167
+ stats = self._tool_stats[tool_name]
168
+ avg_duration = (
169
+ stats.total_duration / stats.calls
170
+ if stats.calls > 0 else 0.0
171
+ )
172
+
173
+ return {
174
+ "calls": stats.calls,
175
+ "errors": stats.errors,
176
+ "error_rate": stats.errors / stats.calls if stats.calls > 0 else 0.0,
177
+ "total_duration_seconds": stats.total_duration,
178
+ "average_duration_seconds": avg_duration,
179
+ "last_call_time": stats.last_call_time,
180
+ "last_error_time": stats.last_error_time,
181
+ }
182
+
183
+ def get_stats(self) -> Dict[str, Any]:
184
+ """
185
+ Get all statistics as a JSON-serializable dictionary.
186
+
187
+ Returns:
188
+ Dictionary containing all collected statistics
189
+ """
190
+ with self._stats_lock:
191
+ # Calculate tool-level stats
192
+ tool_stats_dict = {}
193
+ for tool_name, stats in self._tool_stats.items():
194
+ avg_duration = (
195
+ stats.total_duration / stats.calls
196
+ if stats.calls > 0 else 0.0
197
+ )
198
+ tool_stats_dict[tool_name] = {
199
+ "calls": stats.calls,
200
+ "errors": stats.errors,
201
+ "error_rate": stats.errors / stats.calls if stats.calls > 0 else 0.0,
202
+ "average_duration_seconds": round(avg_duration, 4),
203
+ }
204
+
205
+ # Sort tools by call count (descending)
206
+ sorted_tools = dict(
207
+ sorted(
208
+ tool_stats_dict.items(),
209
+ key=lambda x: x[1]["calls"],
210
+ reverse=True
211
+ )
212
+ )
213
+
214
+ return {
215
+ "uptime_seconds": round(self.uptime, 2),
216
+ "tool_calls_total": self._tool_calls_total,
217
+ "tool_errors_total": self._tool_errors_total,
218
+ "tool_error_rate": (
219
+ self._tool_errors_total / self._tool_calls_total
220
+ if self._tool_calls_total > 0 else 0.0
221
+ ),
222
+ "http_requests_total": self._http_requests_total,
223
+ "http_requests_by_endpoint": dict(self._http_requests_by_endpoint),
224
+ "http_requests_by_method": dict(self._http_requests_by_method),
225
+ "tool_calls_by_name": sorted_tools,
226
+ "unique_tools_called": len(self._tool_stats),
227
+ }
228
+
229
+ def reset(self) -> None:
230
+ """Reset all statistics (useful for testing)."""
231
+ with self._stats_lock:
232
+ self._start_time = time.time()
233
+ self._tool_calls_total = 0
234
+ self._tool_errors_total = 0
235
+ self._http_requests_total = 0
236
+ self._tool_stats.clear()
237
+ self._http_requests_by_endpoint.clear()
238
+ self._http_requests_by_method.clear()
239
+
240
+
241
+ # Module-level singleton accessor
242
+ _stats_collector: Optional[StatsCollector] = None
243
+
244
+
245
+ def get_stats_collector() -> StatsCollector:
246
+ """
247
+ Get the singleton StatsCollector instance.
248
+
249
+ Returns:
250
+ The global StatsCollector instance
251
+ """
252
+ global _stats_collector
253
+ if _stats_collector is None:
254
+ _stats_collector = StatsCollector()
255
+ return _stats_collector