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,211 @@
1
+ """Traced provider service - adds observability to provider operations.
2
+
3
+ This decorator wraps ProviderService to automatically trace all tool
4
+ invocations and health checks with the configured observability backend.
5
+
6
+ Example:
7
+ service = TracedProviderService(
8
+ provider_service=ProviderService(...),
9
+ observability=LangfuseObservabilityAdapter(config),
10
+ )
11
+
12
+ # Tool invocations are automatically traced
13
+ result = service.invoke_tool("math", "add", {"a": 1, "b": 2})
14
+ """
15
+
16
+ import logging
17
+ import time
18
+ from typing import Any
19
+
20
+ from ..ports.observability import ObservabilityPort, TraceContext
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class TracedProviderService:
26
+ """Decorator that adds observability tracing to ProviderService.
27
+
28
+ Wraps an existing ProviderService instance and automatically traces:
29
+ - Tool invocations with input/output and timing
30
+ - Health checks with results and latency
31
+ - Provider state transitions
32
+
33
+ All tracing is transparent to callers and adds minimal overhead
34
+ when observability is disabled.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ provider_service: "ProviderService", # noqa: F821
40
+ observability: ObservabilityPort,
41
+ ) -> None:
42
+ """Initialize traced service.
43
+
44
+ Args:
45
+ provider_service: The underlying provider service to wrap.
46
+ observability: Observability adapter for tracing.
47
+ """
48
+ self._service = provider_service
49
+ self._observability = observability
50
+
51
+ # --- Delegated methods (no tracing needed) ---
52
+
53
+ def list_providers(self) -> list[dict[str, Any]]:
54
+ """List all providers with their status."""
55
+ return self._service.list_providers()
56
+
57
+ def start_provider(self, provider_id: str) -> dict[str, Any]:
58
+ """Start a provider."""
59
+ return self._service.start_provider(provider_id)
60
+
61
+ def stop_provider(self, provider_id: str) -> dict[str, Any]:
62
+ """Stop a provider."""
63
+ return self._service.stop_provider(provider_id)
64
+
65
+ def get_provider_tools(self, provider_id: str) -> dict[str, Any]:
66
+ """Get provider tools."""
67
+ return self._service.get_provider_tools(provider_id)
68
+
69
+ # --- Traced methods ---
70
+
71
+ def invoke_tool(
72
+ self,
73
+ provider_id: str,
74
+ tool_name: str,
75
+ arguments: dict[str, Any],
76
+ timeout: float = 30.0,
77
+ trace_id: str | None = None,
78
+ user_id: str | None = None,
79
+ session_id: str | None = None,
80
+ ) -> dict[str, Any]:
81
+ """Invoke a tool with full tracing.
82
+
83
+ Args:
84
+ provider_id: Provider identifier.
85
+ tool_name: Tool name.
86
+ arguments: Tool arguments.
87
+ timeout: Timeout in seconds.
88
+ trace_id: Optional trace ID for correlation.
89
+ user_id: Optional user ID for attribution.
90
+ session_id: Optional session ID for grouping.
91
+
92
+ Returns:
93
+ Tool result dictionary.
94
+
95
+ Raises:
96
+ ProviderNotFoundError: If provider doesn't exist.
97
+ ToolNotFoundError: If tool doesn't exist.
98
+ ToolInvocationError: If invocation fails.
99
+ """
100
+ trace_context = None
101
+ if trace_id or user_id or session_id:
102
+ trace_context = TraceContext(
103
+ trace_id=trace_id or "",
104
+ user_id=user_id,
105
+ session_id=session_id,
106
+ )
107
+
108
+ span = self._observability.start_tool_span(
109
+ provider_name=provider_id,
110
+ tool_name=tool_name,
111
+ input_params=arguments,
112
+ trace_context=trace_context,
113
+ )
114
+
115
+ try:
116
+ result = self._service.invoke_tool(
117
+ provider_id=provider_id,
118
+ tool_name=tool_name,
119
+ arguments=arguments,
120
+ timeout=timeout,
121
+ )
122
+ span.end_success(output=result)
123
+ return result
124
+
125
+ except Exception as e:
126
+ span.end_error(error=e)
127
+ raise
128
+
129
+ def health_check(
130
+ self,
131
+ provider_id: str,
132
+ trace_id: str | None = None,
133
+ ) -> bool:
134
+ """Perform health check with tracing.
135
+
136
+ Args:
137
+ provider_id: Provider identifier.
138
+ trace_id: Optional trace ID to attach result to.
139
+
140
+ Returns:
141
+ True if healthy, False otherwise.
142
+ """
143
+ start_time = time.perf_counter()
144
+
145
+ try:
146
+ healthy = self._service.health_check(provider_id)
147
+ latency_ms = (time.perf_counter() - start_time) * 1000
148
+
149
+ self._observability.record_health_check(
150
+ provider_name=provider_id,
151
+ healthy=healthy,
152
+ latency_ms=latency_ms,
153
+ trace_id=trace_id,
154
+ )
155
+
156
+ return healthy
157
+
158
+ except Exception as e:
159
+ latency_ms = (time.perf_counter() - start_time) * 1000
160
+
161
+ self._observability.record_health_check(
162
+ provider_name=provider_id,
163
+ healthy=False,
164
+ latency_ms=latency_ms,
165
+ trace_id=trace_id,
166
+ )
167
+
168
+ logger.error(
169
+ "Health check failed for provider %s: %s",
170
+ provider_id,
171
+ e,
172
+ )
173
+ raise
174
+
175
+ def check_all_health(
176
+ self,
177
+ trace_id: str | None = None,
178
+ ) -> dict[str, bool]:
179
+ """Check health of all providers with tracing.
180
+
181
+ Args:
182
+ trace_id: Optional trace ID to attach results to.
183
+
184
+ Returns:
185
+ Dictionary mapping provider_id to health status.
186
+ """
187
+ results = {}
188
+
189
+ for provider_status in self._service.list_providers():
190
+ provider_id = provider_status.get("name") or provider_status.get("provider_id")
191
+ if provider_id:
192
+ try:
193
+ results[provider_id] = self.health_check(provider_id, trace_id)
194
+ except Exception:
195
+ results[provider_id] = False
196
+
197
+ return results
198
+
199
+ def shutdown_idle_providers(self) -> list[str]:
200
+ """Shutdown idle providers."""
201
+ return self._service.shutdown_idle_providers()
202
+
203
+ # --- Observability control ---
204
+
205
+ def flush_traces(self) -> None:
206
+ """Flush pending traces to backend."""
207
+ self._observability.flush()
208
+
209
+ def shutdown_tracing(self) -> None:
210
+ """Shutdown tracing with final flush."""
211
+ self._observability.shutdown()
@@ -0,0 +1,328 @@
1
+ """Bootstrap helpers for wiring runtime dependencies.
2
+
3
+ This module centralizes object graph creation (composition root helpers) so that
4
+ the rest of the codebase can avoid module-level singletons and implicit globals.
5
+
6
+ It intentionally returns plain objects (repository, buses, security plumbing)
7
+ without starting any background threads.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ import os
14
+ from typing import Any, Optional, Protocol, runtime_checkable
15
+
16
+ from ..application.event_handlers import get_security_handler
17
+ from ..application.ports.observability import NullObservabilityAdapter, ObservabilityPort
18
+ from ..domain.repository import InMemoryProviderRepository, IProviderRepository
19
+ from ..domain.security.input_validator import InputValidator
20
+ from ..domain.security.rate_limiter import get_rate_limiter, RateLimitConfig
21
+ from ..infrastructure.command_bus import CommandBus, get_command_bus
22
+ from ..infrastructure.event_bus import EventBus, get_event_bus
23
+ from ..infrastructure.persistence import (
24
+ Database,
25
+ DatabaseConfig,
26
+ InMemoryAuditRepository,
27
+ InMemoryProviderConfigRepository,
28
+ RecoveryService,
29
+ SQLiteAuditRepository,
30
+ SQLiteProviderConfigRepository,
31
+ )
32
+ from ..infrastructure.query_bus import get_query_bus, QueryBus
33
+
34
+ # =============================================================================
35
+ # Protocol Interfaces for Runtime Dependencies
36
+ # =============================================================================
37
+
38
+
39
+ @runtime_checkable
40
+ class IRateLimiter(Protocol):
41
+ """Interface for rate limiter."""
42
+
43
+ def consume(self, key: str) -> Any:
44
+ """Check rate limit for a key."""
45
+ ...
46
+
47
+ def get_stats(self) -> dict[str, Any]:
48
+ """Get rate limiter statistics."""
49
+ ...
50
+
51
+
52
+ @runtime_checkable
53
+ class ISecurityHandler(Protocol):
54
+ """Interface for security event handler."""
55
+
56
+ def handle(self, event: Any) -> None:
57
+ """Handle a security event."""
58
+ ...
59
+
60
+ def log_rate_limit_exceeded(self, limit: int, window_seconds: int) -> None:
61
+ """Log rate limit exceeded."""
62
+ ...
63
+
64
+ def log_validation_failed(
65
+ self,
66
+ field: str,
67
+ message: str,
68
+ provider_id: Optional[str] = None,
69
+ value: Optional[str] = None,
70
+ ) -> None:
71
+ """Log validation failure."""
72
+ ...
73
+
74
+
75
+ @runtime_checkable
76
+ class IConfigRepository(Protocol):
77
+ """Interface for provider config repository."""
78
+
79
+ async def save(self, config: Any) -> None:
80
+ """Save a configuration."""
81
+ ...
82
+
83
+ async def get(self, provider_id: str) -> Optional[Any]:
84
+ """Get configuration by provider ID."""
85
+ ...
86
+
87
+ async def get_all(self) -> list[Any]:
88
+ """Get all configurations."""
89
+ ...
90
+
91
+
92
+ @runtime_checkable
93
+ class IAuditRepository(Protocol):
94
+ """Interface for audit repository."""
95
+
96
+ async def append(self, entry: Any) -> None:
97
+ """Append an audit entry."""
98
+ ...
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class PersistenceConfig:
103
+ """Configuration for persistence layer."""
104
+
105
+ enabled: bool = False
106
+ database_path: str = "data/mcp_hangar.db"
107
+ enable_wal: bool = True
108
+ auto_recover: bool = True
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class ObservabilityConfig:
113
+ """Configuration for observability integrations.
114
+
115
+ Supports Langfuse for LLM observability and tracing.
116
+
117
+ Attributes:
118
+ langfuse_enabled: Whether Langfuse integration is active.
119
+ langfuse_public_key: Langfuse public API key.
120
+ langfuse_secret_key: Langfuse secret API key.
121
+ langfuse_host: Langfuse host URL.
122
+ langfuse_sample_rate: Fraction of traces to sample (0.0 to 1.0).
123
+ langfuse_scrub_inputs: Whether to redact sensitive inputs.
124
+ langfuse_scrub_outputs: Whether to redact sensitive outputs.
125
+ """
126
+
127
+ langfuse_enabled: bool = False
128
+ langfuse_public_key: str = ""
129
+ langfuse_secret_key: str = ""
130
+ langfuse_host: str = "https://cloud.langfuse.com"
131
+ langfuse_sample_rate: float = 1.0
132
+ langfuse_scrub_inputs: bool = False
133
+ langfuse_scrub_outputs: bool = False
134
+
135
+
136
+ @dataclass(frozen=True)
137
+ class Runtime:
138
+ """Container for runtime dependencies.
139
+
140
+ Uses Protocol interfaces for type safety while maintaining flexibility.
141
+ """
142
+
143
+ repository: IProviderRepository
144
+ event_bus: EventBus
145
+ command_bus: CommandBus
146
+ query_bus: QueryBus
147
+
148
+ rate_limit_config: RateLimitConfig
149
+ rate_limiter: IRateLimiter
150
+
151
+ input_validator: InputValidator
152
+ security_handler: ISecurityHandler
153
+
154
+ # Persistence components (optional)
155
+ persistence_config: Optional[PersistenceConfig] = None
156
+ database: Optional[Database] = None
157
+ config_repository: Optional[IConfigRepository] = None
158
+ audit_repository: Optional[IAuditRepository] = None
159
+ recovery_service: Optional[RecoveryService] = None
160
+
161
+ # Observability components (optional)
162
+ observability_config: Optional[ObservabilityConfig] = None
163
+ observability: Optional[ObservabilityPort] = None
164
+
165
+
166
+ def create_runtime(
167
+ *,
168
+ repository: Optional[IProviderRepository] = None,
169
+ event_bus: Optional[EventBus] = None,
170
+ command_bus: Optional[CommandBus] = None,
171
+ query_bus: Optional[QueryBus] = None,
172
+ persistence_config: Optional[PersistenceConfig] = None,
173
+ observability_config: Optional[ObservabilityConfig] = None,
174
+ env: Optional[dict[str, str]] = None,
175
+ ) -> Runtime:
176
+ """Create runtime dependencies explicitly.
177
+
178
+ Args:
179
+ repository: Optional repository override (useful for tests).
180
+ event_bus: Optional event bus override.
181
+ command_bus: Optional command bus override.
182
+ query_bus: Optional query bus override.
183
+ persistence_config: Optional persistence configuration.
184
+ observability_config: Optional observability configuration.
185
+ env: Optional environment mapping (defaults to os.environ).
186
+
187
+ Returns:
188
+ Runtime container.
189
+ """
190
+ env = env or os.environ
191
+
192
+ repo = repository or InMemoryProviderRepository()
193
+ eb = event_bus or get_event_bus()
194
+ cb = command_bus or get_command_bus()
195
+ qb = query_bus or get_query_bus()
196
+
197
+ rate_limit_config = RateLimitConfig(
198
+ requests_per_second=float(env.get("MCP_RATE_LIMIT_RPS", "10")),
199
+ burst_size=int(env.get("MCP_RATE_LIMIT_BURST", "20")),
200
+ )
201
+ rate_limiter = get_rate_limiter(rate_limit_config)
202
+
203
+ input_validator = InputValidator(
204
+ allow_absolute_paths=env.get("MCP_ALLOW_ABSOLUTE_PATHS", "false").lower() == "true",
205
+ )
206
+
207
+ security_handler = get_security_handler()
208
+
209
+ # Configure persistence if enabled
210
+ persistence_enabled = env.get("MCP_PERSISTENCE_ENABLED", "false").lower() == "true"
211
+
212
+ if persistence_config is None and persistence_enabled:
213
+ persistence_config = PersistenceConfig(
214
+ enabled=True,
215
+ database_path=env.get("MCP_DATABASE_PATH", "data/mcp_hangar.db"),
216
+ enable_wal=env.get("MCP_DATABASE_WAL", "true").lower() == "true",
217
+ auto_recover=env.get("MCP_AUTO_RECOVER", "true").lower() == "true",
218
+ )
219
+
220
+ database = None
221
+ config_repository = None
222
+ audit_repository = None
223
+ recovery_service = None
224
+
225
+ if persistence_config and persistence_config.enabled:
226
+ db_config = DatabaseConfig(
227
+ path=persistence_config.database_path,
228
+ enable_wal=persistence_config.enable_wal,
229
+ )
230
+ database = Database(db_config)
231
+ config_repository = SQLiteProviderConfigRepository(database)
232
+ audit_repository = SQLiteAuditRepository(database)
233
+ recovery_service = RecoveryService(
234
+ database=database,
235
+ provider_repository=repo,
236
+ config_repository=config_repository,
237
+ audit_repository=audit_repository,
238
+ )
239
+ else:
240
+ # Use in-memory repositories for non-persistent mode
241
+ config_repository = InMemoryProviderConfigRepository()
242
+ audit_repository = InMemoryAuditRepository()
243
+
244
+ # Configure observability if enabled
245
+ langfuse_enabled = env.get("HANGAR_LANGFUSE_ENABLED", "false").lower() == "true"
246
+
247
+ if observability_config is None and langfuse_enabled:
248
+ observability_config = ObservabilityConfig(
249
+ langfuse_enabled=True,
250
+ langfuse_public_key=env.get("LANGFUSE_PUBLIC_KEY", ""),
251
+ langfuse_secret_key=env.get("LANGFUSE_SECRET_KEY", ""),
252
+ langfuse_host=env.get("LANGFUSE_HOST", "https://cloud.langfuse.com"),
253
+ langfuse_sample_rate=float(env.get("HANGAR_LANGFUSE_SAMPLE_RATE", "1.0")),
254
+ langfuse_scrub_inputs=env.get("HANGAR_LANGFUSE_SCRUB_INPUTS", "false").lower() == "true",
255
+ langfuse_scrub_outputs=env.get("HANGAR_LANGFUSE_SCRUB_OUTPUTS", "false").lower() == "true",
256
+ )
257
+
258
+ observability: ObservabilityPort = NullObservabilityAdapter()
259
+
260
+ if observability_config and observability_config.langfuse_enabled:
261
+ try:
262
+ from ..infrastructure.observability import LangfuseConfig, LangfuseObservabilityAdapter
263
+
264
+ langfuse_config = LangfuseConfig(
265
+ enabled=True,
266
+ public_key=observability_config.langfuse_public_key,
267
+ secret_key=observability_config.langfuse_secret_key,
268
+ host=observability_config.langfuse_host,
269
+ sample_rate=observability_config.langfuse_sample_rate,
270
+ scrub_inputs=observability_config.langfuse_scrub_inputs,
271
+ scrub_outputs=observability_config.langfuse_scrub_outputs,
272
+ )
273
+ observability = LangfuseObservabilityAdapter(langfuse_config)
274
+ except ImportError:
275
+ import logging
276
+
277
+ logging.getLogger(__name__).warning(
278
+ "Langfuse enabled but package not installed. Install with: pip install mcp-hangar[observability]"
279
+ )
280
+
281
+ return Runtime(
282
+ repository=repo,
283
+ event_bus=eb,
284
+ command_bus=cb,
285
+ query_bus=qb,
286
+ rate_limit_config=rate_limit_config,
287
+ rate_limiter=rate_limiter,
288
+ input_validator=input_validator,
289
+ security_handler=security_handler,
290
+ persistence_config=persistence_config,
291
+ database=database,
292
+ config_repository=config_repository,
293
+ audit_repository=audit_repository,
294
+ recovery_service=recovery_service,
295
+ observability_config=observability_config,
296
+ observability=observability,
297
+ )
298
+
299
+
300
+ async def initialize_runtime(runtime: Runtime) -> None:
301
+ """Initialize runtime async components.
302
+
303
+ Should be called during application startup.
304
+
305
+ Args:
306
+ runtime: Runtime container to initialize
307
+ """
308
+ if runtime.database:
309
+ await runtime.database.initialize()
310
+
311
+ if runtime.recovery_service and runtime.persistence_config:
312
+ if runtime.persistence_config.auto_recover:
313
+ await runtime.recovery_service.recover_providers()
314
+
315
+
316
+ async def shutdown_runtime(runtime: Runtime) -> None:
317
+ """Shutdown runtime async components.
318
+
319
+ Should be called during application shutdown.
320
+
321
+ Args:
322
+ runtime: Runtime container to shutdown
323
+ """
324
+ if runtime.observability:
325
+ runtime.observability.shutdown()
326
+
327
+ if runtime.database:
328
+ await runtime.database.close()