mcp-hangar 0.2.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 (160) hide show
  1. mcp_hangar/__init__.py +139 -0
  2. mcp_hangar/application/__init__.py +1 -0
  3. mcp_hangar/application/commands/__init__.py +67 -0
  4. mcp_hangar/application/commands/auth_commands.py +118 -0
  5. mcp_hangar/application/commands/auth_handlers.py +296 -0
  6. mcp_hangar/application/commands/commands.py +59 -0
  7. mcp_hangar/application/commands/handlers.py +189 -0
  8. mcp_hangar/application/discovery/__init__.py +21 -0
  9. mcp_hangar/application/discovery/discovery_metrics.py +283 -0
  10. mcp_hangar/application/discovery/discovery_orchestrator.py +497 -0
  11. mcp_hangar/application/discovery/lifecycle_manager.py +315 -0
  12. mcp_hangar/application/discovery/security_validator.py +414 -0
  13. mcp_hangar/application/event_handlers/__init__.py +50 -0
  14. mcp_hangar/application/event_handlers/alert_handler.py +191 -0
  15. mcp_hangar/application/event_handlers/audit_handler.py +203 -0
  16. mcp_hangar/application/event_handlers/knowledge_base_handler.py +120 -0
  17. mcp_hangar/application/event_handlers/logging_handler.py +69 -0
  18. mcp_hangar/application/event_handlers/metrics_handler.py +152 -0
  19. mcp_hangar/application/event_handlers/persistent_audit_store.py +217 -0
  20. mcp_hangar/application/event_handlers/security_handler.py +604 -0
  21. mcp_hangar/application/mcp/tooling.py +158 -0
  22. mcp_hangar/application/ports/__init__.py +9 -0
  23. mcp_hangar/application/ports/observability.py +237 -0
  24. mcp_hangar/application/queries/__init__.py +52 -0
  25. mcp_hangar/application/queries/auth_handlers.py +237 -0
  26. mcp_hangar/application/queries/auth_queries.py +118 -0
  27. mcp_hangar/application/queries/handlers.py +227 -0
  28. mcp_hangar/application/read_models/__init__.py +11 -0
  29. mcp_hangar/application/read_models/provider_views.py +139 -0
  30. mcp_hangar/application/sagas/__init__.py +11 -0
  31. mcp_hangar/application/sagas/group_rebalance_saga.py +137 -0
  32. mcp_hangar/application/sagas/provider_failover_saga.py +266 -0
  33. mcp_hangar/application/sagas/provider_recovery_saga.py +172 -0
  34. mcp_hangar/application/services/__init__.py +9 -0
  35. mcp_hangar/application/services/provider_service.py +208 -0
  36. mcp_hangar/application/services/traced_provider_service.py +211 -0
  37. mcp_hangar/bootstrap/runtime.py +328 -0
  38. mcp_hangar/context.py +178 -0
  39. mcp_hangar/domain/__init__.py +117 -0
  40. mcp_hangar/domain/contracts/__init__.py +57 -0
  41. mcp_hangar/domain/contracts/authentication.py +225 -0
  42. mcp_hangar/domain/contracts/authorization.py +229 -0
  43. mcp_hangar/domain/contracts/event_store.py +178 -0
  44. mcp_hangar/domain/contracts/metrics_publisher.py +59 -0
  45. mcp_hangar/domain/contracts/persistence.py +383 -0
  46. mcp_hangar/domain/contracts/provider_runtime.py +146 -0
  47. mcp_hangar/domain/discovery/__init__.py +20 -0
  48. mcp_hangar/domain/discovery/conflict_resolver.py +267 -0
  49. mcp_hangar/domain/discovery/discovered_provider.py +185 -0
  50. mcp_hangar/domain/discovery/discovery_service.py +412 -0
  51. mcp_hangar/domain/discovery/discovery_source.py +192 -0
  52. mcp_hangar/domain/events.py +433 -0
  53. mcp_hangar/domain/exceptions.py +525 -0
  54. mcp_hangar/domain/model/__init__.py +70 -0
  55. mcp_hangar/domain/model/aggregate.py +58 -0
  56. mcp_hangar/domain/model/circuit_breaker.py +152 -0
  57. mcp_hangar/domain/model/event_sourced_api_key.py +413 -0
  58. mcp_hangar/domain/model/event_sourced_provider.py +423 -0
  59. mcp_hangar/domain/model/event_sourced_role_assignment.py +268 -0
  60. mcp_hangar/domain/model/health_tracker.py +183 -0
  61. mcp_hangar/domain/model/load_balancer.py +185 -0
  62. mcp_hangar/domain/model/provider.py +810 -0
  63. mcp_hangar/domain/model/provider_group.py +656 -0
  64. mcp_hangar/domain/model/tool_catalog.py +105 -0
  65. mcp_hangar/domain/policies/__init__.py +19 -0
  66. mcp_hangar/domain/policies/provider_health.py +187 -0
  67. mcp_hangar/domain/repository.py +249 -0
  68. mcp_hangar/domain/security/__init__.py +85 -0
  69. mcp_hangar/domain/security/input_validator.py +710 -0
  70. mcp_hangar/domain/security/rate_limiter.py +387 -0
  71. mcp_hangar/domain/security/roles.py +237 -0
  72. mcp_hangar/domain/security/sanitizer.py +387 -0
  73. mcp_hangar/domain/security/secrets.py +501 -0
  74. mcp_hangar/domain/services/__init__.py +20 -0
  75. mcp_hangar/domain/services/audit_service.py +376 -0
  76. mcp_hangar/domain/services/image_builder.py +328 -0
  77. mcp_hangar/domain/services/provider_launcher.py +1046 -0
  78. mcp_hangar/domain/value_objects.py +1138 -0
  79. mcp_hangar/errors.py +818 -0
  80. mcp_hangar/fastmcp_server.py +1105 -0
  81. mcp_hangar/gc.py +134 -0
  82. mcp_hangar/infrastructure/__init__.py +79 -0
  83. mcp_hangar/infrastructure/async_executor.py +133 -0
  84. mcp_hangar/infrastructure/auth/__init__.py +37 -0
  85. mcp_hangar/infrastructure/auth/api_key_authenticator.py +388 -0
  86. mcp_hangar/infrastructure/auth/event_sourced_store.py +567 -0
  87. mcp_hangar/infrastructure/auth/jwt_authenticator.py +360 -0
  88. mcp_hangar/infrastructure/auth/middleware.py +340 -0
  89. mcp_hangar/infrastructure/auth/opa_authorizer.py +243 -0
  90. mcp_hangar/infrastructure/auth/postgres_store.py +659 -0
  91. mcp_hangar/infrastructure/auth/projections.py +366 -0
  92. mcp_hangar/infrastructure/auth/rate_limiter.py +311 -0
  93. mcp_hangar/infrastructure/auth/rbac_authorizer.py +323 -0
  94. mcp_hangar/infrastructure/auth/sqlite_store.py +624 -0
  95. mcp_hangar/infrastructure/command_bus.py +112 -0
  96. mcp_hangar/infrastructure/discovery/__init__.py +110 -0
  97. mcp_hangar/infrastructure/discovery/docker_source.py +289 -0
  98. mcp_hangar/infrastructure/discovery/entrypoint_source.py +249 -0
  99. mcp_hangar/infrastructure/discovery/filesystem_source.py +383 -0
  100. mcp_hangar/infrastructure/discovery/kubernetes_source.py +247 -0
  101. mcp_hangar/infrastructure/event_bus.py +260 -0
  102. mcp_hangar/infrastructure/event_sourced_repository.py +443 -0
  103. mcp_hangar/infrastructure/event_store.py +396 -0
  104. mcp_hangar/infrastructure/knowledge_base/__init__.py +259 -0
  105. mcp_hangar/infrastructure/knowledge_base/contracts.py +202 -0
  106. mcp_hangar/infrastructure/knowledge_base/memory.py +177 -0
  107. mcp_hangar/infrastructure/knowledge_base/postgres.py +545 -0
  108. mcp_hangar/infrastructure/knowledge_base/sqlite.py +513 -0
  109. mcp_hangar/infrastructure/metrics_publisher.py +36 -0
  110. mcp_hangar/infrastructure/observability/__init__.py +10 -0
  111. mcp_hangar/infrastructure/observability/langfuse_adapter.py +534 -0
  112. mcp_hangar/infrastructure/persistence/__init__.py +33 -0
  113. mcp_hangar/infrastructure/persistence/audit_repository.py +371 -0
  114. mcp_hangar/infrastructure/persistence/config_repository.py +398 -0
  115. mcp_hangar/infrastructure/persistence/database.py +333 -0
  116. mcp_hangar/infrastructure/persistence/database_common.py +330 -0
  117. mcp_hangar/infrastructure/persistence/event_serializer.py +280 -0
  118. mcp_hangar/infrastructure/persistence/event_upcaster.py +166 -0
  119. mcp_hangar/infrastructure/persistence/in_memory_event_store.py +150 -0
  120. mcp_hangar/infrastructure/persistence/recovery_service.py +312 -0
  121. mcp_hangar/infrastructure/persistence/sqlite_event_store.py +386 -0
  122. mcp_hangar/infrastructure/persistence/unit_of_work.py +409 -0
  123. mcp_hangar/infrastructure/persistence/upcasters/README.md +13 -0
  124. mcp_hangar/infrastructure/persistence/upcasters/__init__.py +7 -0
  125. mcp_hangar/infrastructure/query_bus.py +153 -0
  126. mcp_hangar/infrastructure/saga_manager.py +401 -0
  127. mcp_hangar/logging_config.py +209 -0
  128. mcp_hangar/metrics.py +1007 -0
  129. mcp_hangar/models.py +31 -0
  130. mcp_hangar/observability/__init__.py +54 -0
  131. mcp_hangar/observability/health.py +487 -0
  132. mcp_hangar/observability/metrics.py +319 -0
  133. mcp_hangar/observability/tracing.py +433 -0
  134. mcp_hangar/progress.py +542 -0
  135. mcp_hangar/retry.py +613 -0
  136. mcp_hangar/server/__init__.py +120 -0
  137. mcp_hangar/server/__main__.py +6 -0
  138. mcp_hangar/server/auth_bootstrap.py +340 -0
  139. mcp_hangar/server/auth_cli.py +335 -0
  140. mcp_hangar/server/auth_config.py +305 -0
  141. mcp_hangar/server/bootstrap.py +735 -0
  142. mcp_hangar/server/cli.py +161 -0
  143. mcp_hangar/server/config.py +224 -0
  144. mcp_hangar/server/context.py +215 -0
  145. mcp_hangar/server/http_auth_middleware.py +165 -0
  146. mcp_hangar/server/lifecycle.py +467 -0
  147. mcp_hangar/server/state.py +117 -0
  148. mcp_hangar/server/tools/__init__.py +16 -0
  149. mcp_hangar/server/tools/discovery.py +186 -0
  150. mcp_hangar/server/tools/groups.py +75 -0
  151. mcp_hangar/server/tools/health.py +301 -0
  152. mcp_hangar/server/tools/provider.py +939 -0
  153. mcp_hangar/server/tools/registry.py +320 -0
  154. mcp_hangar/server/validation.py +113 -0
  155. mcp_hangar/stdio_client.py +229 -0
  156. mcp_hangar-0.2.0.dist-info/METADATA +347 -0
  157. mcp_hangar-0.2.0.dist-info/RECORD +160 -0
  158. mcp_hangar-0.2.0.dist-info/WHEEL +4 -0
  159. mcp_hangar-0.2.0.dist-info/entry_points.txt +2 -0
  160. mcp_hangar-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,433 @@
1
+ """OpenTelemetry tracing for MCP Hangar.
2
+
3
+ Provides distributed tracing with automatic context propagation
4
+ through tool invocations and provider calls.
5
+
6
+ Configuration via environment variables:
7
+ OTEL_EXPORTER_OTLP_ENDPOINT: OTLP endpoint (default: http://localhost:4317)
8
+ OTEL_SERVICE_NAME: Service name (default: mcp-hangar)
9
+ OTEL_TRACES_SAMPLER: Sampler type (default: always_on)
10
+ MCP_TRACING_ENABLED: Enable/disable tracing (default: true)
11
+
12
+ Example:
13
+ from mcp_hangar.observability.tracing import init_tracing, get_tracer
14
+
15
+ # Initialize once at startup
16
+ init_tracing()
17
+
18
+ # Get tracer for module
19
+ tracer = get_tracer(__name__)
20
+
21
+ # Create spans
22
+ with tracer.start_as_current_span("my_operation") as span:
23
+ span.set_attribute("key", "value")
24
+ do_work()
25
+ """
26
+
27
+ from contextlib import contextmanager
28
+ from functools import wraps
29
+ import os
30
+ from typing import Any, Callable, Dict, Optional, TypeVar
31
+
32
+ from mcp_hangar.logging_config import get_logger
33
+
34
+ logger = get_logger(__name__)
35
+
36
+ # Type variable for generic decorator
37
+ F = TypeVar("F", bound=Callable[..., Any])
38
+
39
+ # Global state
40
+ _tracer_provider = None
41
+ _initialized = False
42
+
43
+ # Check if OpenTelemetry is available
44
+ try:
45
+ from opentelemetry.sdk.resources import Resource, SERVICE_NAME
46
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
47
+ from opentelemetry.sdk.trace import TracerProvider
48
+ from opentelemetry import trace
49
+ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
50
+ from opentelemetry.trace import Span, Status, StatusCode, Tracer
51
+
52
+ OTEL_AVAILABLE = True
53
+ except ImportError:
54
+ OTEL_AVAILABLE = False
55
+ trace = None
56
+ Tracer = None
57
+ Span = None
58
+
59
+ # Try to import OTLP exporter
60
+ try:
61
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
62
+
63
+ OTLP_AVAILABLE = True
64
+ except ImportError:
65
+ OTLP_AVAILABLE = False
66
+ OTLPSpanExporter = None
67
+
68
+ # Try to import Jaeger exporter
69
+ try:
70
+ from opentelemetry.exporter.jaeger.thrift import JaegerExporter
71
+
72
+ JAEGER_AVAILABLE = True
73
+ except ImportError:
74
+ JAEGER_AVAILABLE = False
75
+ JaegerExporter = None
76
+
77
+
78
+ class NoOpSpan:
79
+ """No-op span for when tracing is disabled."""
80
+
81
+ def set_attribute(self, key: str, value: Any) -> None:
82
+ pass
83
+
84
+ def set_status(self, status: Any) -> None:
85
+ pass
86
+
87
+ def record_exception(self, exception: Exception) -> None:
88
+ pass
89
+
90
+ def add_event(self, name: str, attributes: Optional[Dict] = None) -> None:
91
+ pass
92
+
93
+ def __enter__(self) -> "NoOpSpan":
94
+ return self
95
+
96
+ def __exit__(self, *args) -> None:
97
+ pass
98
+
99
+
100
+ class NoOpTracer:
101
+ """No-op tracer for when tracing is disabled."""
102
+
103
+ def start_as_current_span(self, name: str, **kwargs) -> NoOpSpan:
104
+ return NoOpSpan()
105
+
106
+ @contextmanager
107
+ def start_span(self, name: str, **kwargs):
108
+ yield NoOpSpan()
109
+
110
+
111
+ _noop_tracer = NoOpTracer()
112
+
113
+
114
+ def is_tracing_enabled() -> bool:
115
+ """Check if tracing is enabled."""
116
+ enabled = os.getenv("MCP_TRACING_ENABLED", "true").lower()
117
+ return enabled in ("true", "1", "yes") and OTEL_AVAILABLE
118
+
119
+
120
+ def init_tracing(
121
+ service_name: str = "mcp-hangar",
122
+ otlp_endpoint: Optional[str] = None,
123
+ jaeger_host: Optional[str] = None,
124
+ jaeger_port: int = 6831,
125
+ console_export: bool = False,
126
+ ) -> bool:
127
+ """Initialize OpenTelemetry tracing.
128
+
129
+ Args:
130
+ service_name: Service name for traces.
131
+ otlp_endpoint: OTLP collector endpoint (gRPC).
132
+ jaeger_host: Jaeger agent host for UDP export.
133
+ jaeger_port: Jaeger agent port.
134
+ console_export: Enable console span export (for debugging).
135
+
136
+ Returns:
137
+ True if tracing was initialized, False otherwise.
138
+ """
139
+ global _tracer_provider, _initialized
140
+
141
+ if _initialized:
142
+ logger.debug("tracing_already_initialized")
143
+ return True
144
+
145
+ if not OTEL_AVAILABLE:
146
+ logger.info(
147
+ "tracing_disabled_otel_not_available",
148
+ hint="Install opentelemetry-api and opentelemetry-sdk",
149
+ )
150
+ return False
151
+
152
+ if not is_tracing_enabled():
153
+ logger.info("tracing_disabled_by_config")
154
+ return False
155
+
156
+ try:
157
+ # Get endpoint from env or parameter
158
+ otlp_endpoint = otlp_endpoint or os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317")
159
+
160
+ # Create resource with service info
161
+ resource = Resource.create(
162
+ {
163
+ SERVICE_NAME: service_name,
164
+ "service.version": _get_version(),
165
+ "deployment.environment": os.getenv("MCP_ENVIRONMENT", "development"),
166
+ }
167
+ )
168
+
169
+ # Create tracer provider
170
+ _tracer_provider = TracerProvider(resource=resource)
171
+
172
+ # Add exporters
173
+ exporters_added = 0
174
+
175
+ # OTLP exporter (preferred)
176
+ if OTLP_AVAILABLE and otlp_endpoint:
177
+ try:
178
+ otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint, insecure=True)
179
+ _tracer_provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
180
+ exporters_added += 1
181
+ logger.info("tracing_otlp_exporter_added", endpoint=otlp_endpoint)
182
+ except Exception as e:
183
+ logger.warning("tracing_otlp_exporter_failed", error=str(e))
184
+
185
+ # Jaeger exporter (fallback)
186
+ if JAEGER_AVAILABLE and jaeger_host:
187
+ try:
188
+ jaeger_exporter = JaegerExporter(
189
+ agent_host_name=jaeger_host,
190
+ agent_port=jaeger_port,
191
+ )
192
+ _tracer_provider.add_span_processor(BatchSpanProcessor(jaeger_exporter))
193
+ exporters_added += 1
194
+ logger.info(
195
+ "tracing_jaeger_exporter_added",
196
+ host=jaeger_host,
197
+ port=jaeger_port,
198
+ )
199
+ except Exception as e:
200
+ logger.warning("tracing_jaeger_exporter_failed", error=str(e))
201
+
202
+ # Console exporter (debugging)
203
+ if console_export:
204
+ console_exporter = ConsoleSpanExporter()
205
+ _tracer_provider.add_span_processor(BatchSpanProcessor(console_exporter))
206
+ exporters_added += 1
207
+ logger.info("tracing_console_exporter_added")
208
+
209
+ if exporters_added == 0:
210
+ logger.warning("tracing_no_exporters_configured")
211
+ return False
212
+
213
+ # Set global tracer provider
214
+ trace.set_tracer_provider(_tracer_provider)
215
+ _initialized = True
216
+
217
+ logger.info(
218
+ "tracing_initialized",
219
+ service_name=service_name,
220
+ exporters=exporters_added,
221
+ )
222
+ return True
223
+
224
+ except Exception as e:
225
+ logger.error("tracing_initialization_failed", error=str(e))
226
+ return False
227
+
228
+
229
+ def shutdown_tracing() -> None:
230
+ """Shutdown tracing and flush pending spans."""
231
+ global _tracer_provider, _initialized
232
+
233
+ if _tracer_provider is not None:
234
+ try:
235
+ _tracer_provider.shutdown()
236
+ logger.info("tracing_shutdown_complete")
237
+ except Exception as e:
238
+ logger.warning("tracing_shutdown_error", error=str(e))
239
+ finally:
240
+ _tracer_provider = None
241
+ _initialized = False
242
+
243
+
244
+ def get_tracer(name: str = __name__) -> Any:
245
+ """Get a tracer instance.
246
+
247
+ Args:
248
+ name: Tracer name (usually __name__).
249
+
250
+ Returns:
251
+ OpenTelemetry tracer or NoOpTracer if disabled.
252
+ """
253
+ if not _initialized or not OTEL_AVAILABLE:
254
+ return _noop_tracer
255
+
256
+ return trace.get_tracer(name)
257
+
258
+
259
+ def trace_tool_invocation(
260
+ provider_id: str,
261
+ tool_name: str,
262
+ timeout: float,
263
+ ) -> Callable[[F], F]:
264
+ """Decorator to trace tool invocations.
265
+
266
+ Args:
267
+ provider_id: Provider ID.
268
+ tool_name: Tool name.
269
+ timeout: Timeout in seconds.
270
+
271
+ Example:
272
+ @trace_tool_invocation("sqlite", "query", 30.0)
273
+ def invoke_tool(...):
274
+ ...
275
+ """
276
+
277
+ def decorator(func: F) -> F:
278
+ @wraps(func)
279
+ def wrapper(*args, **kwargs):
280
+ tracer = get_tracer(__name__)
281
+
282
+ with tracer.start_as_current_span(
283
+ f"tool.invoke.{tool_name}",
284
+ kind=trace.SpanKind.CLIENT if OTEL_AVAILABLE else None,
285
+ ) as span:
286
+ # Set standard attributes
287
+ span.set_attribute("mcp.provider.id", provider_id)
288
+ span.set_attribute("mcp.tool.name", tool_name)
289
+ span.set_attribute("mcp.timeout_seconds", timeout)
290
+
291
+ try:
292
+ result = func(*args, **kwargs)
293
+ span.set_attribute("mcp.result.success", True)
294
+ return result
295
+ except Exception as e:
296
+ span.set_attribute("mcp.result.success", False)
297
+ span.set_attribute("mcp.error.type", type(e).__name__)
298
+ span.set_attribute("mcp.error.message", str(e)[:500])
299
+ span.record_exception(e)
300
+ if OTEL_AVAILABLE:
301
+ span.set_status(Status(StatusCode.ERROR, str(e)))
302
+ raise
303
+
304
+ return wrapper
305
+
306
+ return decorator
307
+
308
+
309
+ @contextmanager
310
+ def trace_span(
311
+ name: str,
312
+ attributes: Optional[Dict[str, Any]] = None,
313
+ kind: Optional[str] = None,
314
+ ):
315
+ """Context manager for creating trace spans.
316
+
317
+ Args:
318
+ name: Span name.
319
+ attributes: Initial span attributes.
320
+ kind: Span kind (client, server, producer, consumer, internal).
321
+
322
+ Example:
323
+ with trace_span("my_operation", {"key": "value"}) as span:
324
+ span.add_event("checkpoint_reached")
325
+ do_work()
326
+ """
327
+ tracer = get_tracer(__name__)
328
+
329
+ span_kind = None
330
+ if OTEL_AVAILABLE and kind:
331
+ kind_map = {
332
+ "client": trace.SpanKind.CLIENT,
333
+ "server": trace.SpanKind.SERVER,
334
+ "producer": trace.SpanKind.PRODUCER,
335
+ "consumer": trace.SpanKind.CONSUMER,
336
+ "internal": trace.SpanKind.INTERNAL,
337
+ }
338
+ span_kind = kind_map.get(kind.lower())
339
+
340
+ with tracer.start_as_current_span(name, kind=span_kind) as span:
341
+ if attributes:
342
+ for key, value in attributes.items():
343
+ span.set_attribute(key, value)
344
+ yield span
345
+
346
+
347
+ def inject_trace_context(carrier: Dict[str, str]) -> None:
348
+ """Inject trace context into carrier dict for propagation.
349
+
350
+ Args:
351
+ carrier: Dict to inject trace context into.
352
+
353
+ Example:
354
+ headers = {}
355
+ inject_trace_context(headers)
356
+ # headers now contains traceparent, tracestate
357
+ """
358
+ if not OTEL_AVAILABLE or not _initialized:
359
+ return
360
+
361
+ propagator = TraceContextTextMapPropagator()
362
+ propagator.inject(carrier)
363
+
364
+
365
+ def extract_trace_context(carrier: Dict[str, str]) -> Any:
366
+ """Extract trace context from carrier dict.
367
+
368
+ Args:
369
+ carrier: Dict containing trace context.
370
+
371
+ Returns:
372
+ OpenTelemetry context or None.
373
+
374
+ Example:
375
+ context = extract_trace_context(request.headers)
376
+ with tracer.start_as_current_span("handle", context=context):
377
+ ...
378
+ """
379
+ if not OTEL_AVAILABLE or not _initialized:
380
+ return None
381
+
382
+ propagator = TraceContextTextMapPropagator()
383
+ return propagator.extract(carrier)
384
+
385
+
386
+ def get_current_trace_id() -> Optional[str]:
387
+ """Get current trace ID as hex string.
388
+
389
+ Returns:
390
+ Trace ID or None if not in a trace.
391
+ """
392
+ if not OTEL_AVAILABLE or not _initialized:
393
+ return None
394
+
395
+ span = trace.get_current_span()
396
+ if span is None:
397
+ return None
398
+
399
+ ctx = span.get_span_context()
400
+ if ctx is None or not ctx.is_valid:
401
+ return None
402
+
403
+ return format(ctx.trace_id, "032x")
404
+
405
+
406
+ def get_current_span_id() -> Optional[str]:
407
+ """Get current span ID as hex string.
408
+
409
+ Returns:
410
+ Span ID or None if not in a span.
411
+ """
412
+ if not OTEL_AVAILABLE or not _initialized:
413
+ return None
414
+
415
+ span = trace.get_current_span()
416
+ if span is None:
417
+ return None
418
+
419
+ ctx = span.get_span_context()
420
+ if ctx is None or not ctx.is_valid:
421
+ return None
422
+
423
+ return format(ctx.span_id, "016x")
424
+
425
+
426
+ def _get_version() -> str:
427
+ """Get MCP Hangar version."""
428
+ try:
429
+ from mcp_hangar import __version__
430
+
431
+ return __version__
432
+ except (ImportError, AttributeError):
433
+ return "unknown"