kubectl-mcp-server 1.16.0__py3-none-any.whl → 1.18.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 (30) hide show
  1. {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/METADATA +48 -1
  2. {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/RECORD +30 -15
  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/k8s_config.py +127 -1
  10. kubectl_mcp_tool/mcp_server.py +219 -8
  11. kubectl_mcp_tool/observability/__init__.py +59 -0
  12. kubectl_mcp_tool/observability/metrics.py +223 -0
  13. kubectl_mcp_tool/observability/stats.py +255 -0
  14. kubectl_mcp_tool/observability/tracing.py +335 -0
  15. kubectl_mcp_tool/prompts/__init__.py +43 -0
  16. kubectl_mcp_tool/prompts/builtin.py +695 -0
  17. kubectl_mcp_tool/prompts/custom.py +298 -0
  18. kubectl_mcp_tool/prompts/prompts.py +180 -4
  19. kubectl_mcp_tool/providers.py +347 -0
  20. kubectl_mcp_tool/safety.py +155 -0
  21. kubectl_mcp_tool/tools/cluster.py +384 -0
  22. tests/test_config.py +386 -0
  23. tests/test_mcp_integration.py +251 -0
  24. tests/test_observability.py +521 -0
  25. tests/test_prompts.py +716 -0
  26. tests/test_safety.py +218 -0
  27. {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/WHEEL +0 -0
  28. {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/entry_points.txt +0 -0
  29. {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/licenses/LICENSE +0 -0
  30. {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,335 @@
1
+ """
2
+ OpenTelemetry tracing for kubectl-mcp-server.
3
+
4
+ Provides distributed tracing with OTLP export for production observability.
5
+
6
+ Environment Variables:
7
+ OTEL_EXPORTER_OTLP_ENDPOINT: OTLP endpoint URL (e.g., http://localhost:4317)
8
+ OTEL_EXPORTER_OTLP_HEADERS: Optional headers for OTLP exporter
9
+ OTEL_TRACES_SAMPLER: Sampler type (always_on, always_off, traceidratio, parentbased_always_on)
10
+ OTEL_TRACES_SAMPLER_ARG: Sampler argument (e.g., 0.5 for 50% sampling)
11
+ OTEL_SERVICE_NAME: Service name (default: kubectl-mcp-server)
12
+ OTEL_RESOURCE_ATTRIBUTES: Additional resource attributes
13
+
14
+ Requires: opentelemetry-api, opentelemetry-sdk, opentelemetry-exporter-otlp (optional dependencies)
15
+ """
16
+
17
+ import os
18
+ import logging
19
+ from contextlib import contextmanager
20
+ from typing import Optional, Generator, Any, Dict
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Check if OpenTelemetry is available
25
+ _otel_available = False
26
+ _tracer = None
27
+ _tracer_provider = None
28
+
29
+ try:
30
+ from opentelemetry import trace
31
+ from opentelemetry.sdk.trace import TracerProvider, Span
32
+ from opentelemetry.sdk.trace.export import (
33
+ BatchSpanProcessor,
34
+ ConsoleSpanExporter,
35
+ )
36
+ from opentelemetry.sdk.resources import Resource, SERVICE_NAME
37
+ from opentelemetry.trace import Status, StatusCode
38
+ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
39
+
40
+ _otel_available = True
41
+ logger.debug("OpenTelemetry tracing modules available")
42
+
43
+ except ImportError:
44
+ logger.debug(
45
+ "OpenTelemetry not installed. Tracing disabled. "
46
+ "Install with: pip install kubectl-mcp-server[observability]"
47
+ )
48
+
49
+
50
+ def is_tracing_available() -> bool:
51
+ """Check if OpenTelemetry tracing is available."""
52
+ return _otel_available
53
+
54
+
55
+ def _get_sampler():
56
+ """
57
+ Get the configured sampler based on environment variables.
58
+
59
+ Supports:
60
+ - always_on: Always sample
61
+ - always_off: Never sample
62
+ - traceidratio: Sample based on ratio (OTEL_TRACES_SAMPLER_ARG)
63
+ - parentbased_always_on: Parent-based with always_on default
64
+ """
65
+ if not _otel_available:
66
+ return None
67
+
68
+ from opentelemetry.sdk.trace.sampling import (
69
+ ALWAYS_ON,
70
+ ALWAYS_OFF,
71
+ TraceIdRatioBased,
72
+ ParentBasedTraceIdRatio,
73
+ )
74
+
75
+ sampler_type = os.environ.get("OTEL_TRACES_SAMPLER", "parentbased_always_on").lower()
76
+ sampler_arg = os.environ.get("OTEL_TRACES_SAMPLER_ARG", "1.0")
77
+
78
+ try:
79
+ ratio = float(sampler_arg)
80
+ except ValueError:
81
+ ratio = 1.0
82
+ logger.warning(f"Invalid OTEL_TRACES_SAMPLER_ARG: {sampler_arg}, using 1.0")
83
+
84
+ if sampler_type == "always_on":
85
+ return ALWAYS_ON
86
+ elif sampler_type == "always_off":
87
+ return ALWAYS_OFF
88
+ elif sampler_type == "traceidratio":
89
+ return TraceIdRatioBased(ratio)
90
+ elif sampler_type in ("parentbased_always_on", "parentbased_traceidratio"):
91
+ return ParentBasedTraceIdRatio(ratio)
92
+ else:
93
+ logger.warning(f"Unknown sampler type: {sampler_type}, using parentbased_always_on")
94
+ return ParentBasedTraceIdRatio(ratio)
95
+
96
+
97
+ def init_tracing(
98
+ service_name: Optional[str] = None,
99
+ service_version: Optional[str] = None,
100
+ ) -> bool:
101
+ """
102
+ Initialize OpenTelemetry tracing.
103
+
104
+ Args:
105
+ service_name: Service name (default from OTEL_SERVICE_NAME or kubectl-mcp-server)
106
+ service_version: Service version (default from package version)
107
+
108
+ Returns:
109
+ True if tracing was initialized, False otherwise
110
+ """
111
+ global _tracer, _tracer_provider
112
+
113
+ if not _otel_available:
114
+ logger.debug("OpenTelemetry not available, skipping tracing init")
115
+ return False
116
+
117
+ # Already initialized
118
+ if _tracer is not None:
119
+ return True
120
+
121
+ try:
122
+ from opentelemetry import trace
123
+ from opentelemetry.sdk.trace import TracerProvider
124
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
125
+ from opentelemetry.sdk.resources import Resource, SERVICE_NAME
126
+
127
+ # Get service name
128
+ if service_name is None:
129
+ service_name = os.environ.get("OTEL_SERVICE_NAME", "kubectl-mcp-server")
130
+
131
+ # Get service version
132
+ if service_version is None:
133
+ try:
134
+ from kubectl_mcp_tool import __version__
135
+ service_version = __version__
136
+ except ImportError:
137
+ service_version = "unknown"
138
+
139
+ # Parse additional resource attributes
140
+ resource_attrs = {
141
+ SERVICE_NAME: service_name,
142
+ "service.version": service_version,
143
+ }
144
+
145
+ # Add custom attributes from environment
146
+ custom_attrs = os.environ.get("OTEL_RESOURCE_ATTRIBUTES", "")
147
+ if custom_attrs:
148
+ for attr in custom_attrs.split(","):
149
+ if "=" in attr:
150
+ key, value = attr.split("=", 1)
151
+ resource_attrs[key.strip()] = value.strip()
152
+
153
+ # Create resource
154
+ resource = Resource.create(resource_attrs)
155
+
156
+ # Create tracer provider with sampler
157
+ sampler = _get_sampler()
158
+ _tracer_provider = TracerProvider(resource=resource, sampler=sampler)
159
+
160
+ # Add exporter based on environment
161
+ otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
162
+
163
+ if otlp_endpoint:
164
+ # Use OTLP exporter
165
+ try:
166
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
167
+
168
+ otlp_headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
169
+ headers_dict = {}
170
+ if otlp_headers:
171
+ for header in otlp_headers.split(","):
172
+ if "=" in header:
173
+ key, value = header.split("=", 1)
174
+ headers_dict[key.strip()] = value.strip()
175
+
176
+ exporter = OTLPSpanExporter(
177
+ endpoint=otlp_endpoint,
178
+ headers=headers_dict if headers_dict else None,
179
+ )
180
+ _tracer_provider.add_span_processor(BatchSpanProcessor(exporter))
181
+ logger.info(f"OpenTelemetry OTLP exporter configured: {otlp_endpoint}")
182
+
183
+ except ImportError:
184
+ # Try HTTP exporter as fallback
185
+ try:
186
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as HTTPOTLPSpanExporter
187
+
188
+ exporter = HTTPOTLPSpanExporter(endpoint=f"{otlp_endpoint}/v1/traces")
189
+ _tracer_provider.add_span_processor(BatchSpanProcessor(exporter))
190
+ logger.info(f"OpenTelemetry HTTP OTLP exporter configured: {otlp_endpoint}")
191
+
192
+ except ImportError:
193
+ logger.warning(
194
+ "OTLP exporter not available. "
195
+ "Install with: pip install opentelemetry-exporter-otlp"
196
+ )
197
+ # Fall back to console exporter for debugging
198
+ from opentelemetry.sdk.trace.export import ConsoleSpanExporter
199
+ _tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
200
+ logger.info("Using console span exporter (OTLP exporter not available)")
201
+
202
+ elif os.environ.get("OTEL_TRACES_EXPORTER") == "console":
203
+ # Explicitly use console exporter
204
+ from opentelemetry.sdk.trace.export import ConsoleSpanExporter
205
+ _tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
206
+ logger.info("Using console span exporter")
207
+ else:
208
+ # No exporter configured, log a message
209
+ logger.debug(
210
+ "No OTEL_EXPORTER_OTLP_ENDPOINT set, tracing spans will not be exported. "
211
+ "Set OTEL_TRACES_EXPORTER=console for debug output."
212
+ )
213
+
214
+ # Set the global tracer provider
215
+ trace.set_tracer_provider(_tracer_provider)
216
+
217
+ # Create tracer
218
+ _tracer = trace.get_tracer(
219
+ "kubectl-mcp-server",
220
+ service_version,
221
+ )
222
+
223
+ logger.info(f"OpenTelemetry tracing initialized for {service_name} v{service_version}")
224
+ return True
225
+
226
+ except Exception as e:
227
+ logger.error(f"Failed to initialize OpenTelemetry tracing: {e}")
228
+ return False
229
+
230
+
231
+ def get_tracer():
232
+ """
233
+ Get the OpenTelemetry tracer.
234
+
235
+ Returns:
236
+ The tracer instance, or None if not initialized
237
+ """
238
+ return _tracer
239
+
240
+
241
+ def shutdown_tracing() -> None:
242
+ """Shutdown the tracer provider and flush any pending spans."""
243
+ global _tracer, _tracer_provider
244
+
245
+ if _tracer_provider is not None:
246
+ try:
247
+ _tracer_provider.shutdown()
248
+ logger.debug("OpenTelemetry tracing shut down")
249
+ except Exception as e:
250
+ logger.error(f"Error shutting down tracing: {e}")
251
+
252
+ _tracer = None
253
+ _tracer_provider = None
254
+
255
+
256
+ @contextmanager
257
+ def traced_tool_call(
258
+ tool_name: str,
259
+ attributes: Optional[Dict[str, Any]] = None,
260
+ ) -> Generator[Any, None, None]:
261
+ """
262
+ Context manager for tracing a tool call.
263
+
264
+ Creates a span for the tool call and records attributes and errors.
265
+
266
+ Args:
267
+ tool_name: Name of the tool being called
268
+ attributes: Optional additional span attributes
269
+
270
+ Yields:
271
+ The span object (or a no-op if tracing is disabled)
272
+
273
+ Example:
274
+ with traced_tool_call("get_pods", {"namespace": "default"}) as span:
275
+ result = await get_pods(namespace="default")
276
+ span.set_attribute("pod_count", len(result))
277
+ """
278
+ if not _otel_available or _tracer is None:
279
+ # Return a no-op context
280
+ yield None
281
+ return
282
+
283
+ from opentelemetry.trace import Status, StatusCode
284
+
285
+ with _tracer.start_as_current_span(
286
+ f"mcp.tool.{tool_name}",
287
+ kind=trace.SpanKind.INTERNAL,
288
+ ) as span:
289
+ # Set base attributes
290
+ span.set_attribute("mcp.tool.name", tool_name)
291
+
292
+ # Set additional attributes
293
+ if attributes:
294
+ for key, value in attributes.items():
295
+ if isinstance(value, (str, int, float, bool)):
296
+ span.set_attribute(f"mcp.tool.{key}", value)
297
+
298
+ try:
299
+ yield span
300
+ span.set_status(Status(StatusCode.OK))
301
+ except Exception as e:
302
+ span.set_status(Status(StatusCode.ERROR, str(e)))
303
+ span.record_exception(e)
304
+ raise
305
+
306
+
307
+ def add_span_attribute(key: str, value: Any) -> None:
308
+ """
309
+ Add an attribute to the current span.
310
+
311
+ Args:
312
+ key: Attribute key
313
+ value: Attribute value (must be str, int, float, or bool)
314
+ """
315
+ if not _otel_available:
316
+ return
317
+
318
+ span = trace.get_current_span()
319
+ if span is not None and isinstance(value, (str, int, float, bool)):
320
+ span.set_attribute(key, value)
321
+
322
+
323
+ def record_span_exception(exception: Exception) -> None:
324
+ """
325
+ Record an exception on the current span.
326
+
327
+ Args:
328
+ exception: The exception to record
329
+ """
330
+ if not _otel_available:
331
+ return
332
+
333
+ span = trace.get_current_span()
334
+ if span is not None:
335
+ span.record_exception(exception)
@@ -1,5 +1,48 @@
1
1
  from .prompts import register_prompts
2
+ from .custom import (
3
+ CustomPrompt,
4
+ PromptArgument,
5
+ PromptMessage,
6
+ render_prompt,
7
+ load_prompts_from_config,
8
+ load_prompts_from_toml_file,
9
+ validate_prompt_args,
10
+ apply_defaults,
11
+ get_prompt_schema,
12
+ )
13
+ from .builtin import (
14
+ BUILTIN_PROMPTS,
15
+ get_builtin_prompts,
16
+ get_builtin_prompt_by_name,
17
+ CLUSTER_HEALTH_CHECK,
18
+ DEBUG_WORKLOAD,
19
+ RESOURCE_USAGE,
20
+ SECURITY_POSTURE,
21
+ DEPLOYMENT_CHECKLIST,
22
+ INCIDENT_RESPONSE,
23
+ )
2
24
 
3
25
  __all__ = [
26
+ # Main registration function
4
27
  "register_prompts",
28
+ # Custom prompt types and functions
29
+ "CustomPrompt",
30
+ "PromptArgument",
31
+ "PromptMessage",
32
+ "render_prompt",
33
+ "load_prompts_from_config",
34
+ "load_prompts_from_toml_file",
35
+ "validate_prompt_args",
36
+ "apply_defaults",
37
+ "get_prompt_schema",
38
+ # Built-in prompts
39
+ "BUILTIN_PROMPTS",
40
+ "get_builtin_prompts",
41
+ "get_builtin_prompt_by_name",
42
+ "CLUSTER_HEALTH_CHECK",
43
+ "DEBUG_WORKLOAD",
44
+ "RESOURCE_USAGE",
45
+ "SECURITY_POSTURE",
46
+ "DEPLOYMENT_CHECKLIST",
47
+ "INCIDENT_RESPONSE",
5
48
  ]