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.
- mcp_hangar/__init__.py +139 -0
- mcp_hangar/application/__init__.py +1 -0
- mcp_hangar/application/commands/__init__.py +67 -0
- mcp_hangar/application/commands/auth_commands.py +118 -0
- mcp_hangar/application/commands/auth_handlers.py +296 -0
- mcp_hangar/application/commands/commands.py +59 -0
- mcp_hangar/application/commands/handlers.py +189 -0
- mcp_hangar/application/discovery/__init__.py +21 -0
- mcp_hangar/application/discovery/discovery_metrics.py +283 -0
- mcp_hangar/application/discovery/discovery_orchestrator.py +497 -0
- mcp_hangar/application/discovery/lifecycle_manager.py +315 -0
- mcp_hangar/application/discovery/security_validator.py +414 -0
- mcp_hangar/application/event_handlers/__init__.py +50 -0
- mcp_hangar/application/event_handlers/alert_handler.py +191 -0
- mcp_hangar/application/event_handlers/audit_handler.py +203 -0
- mcp_hangar/application/event_handlers/knowledge_base_handler.py +120 -0
- mcp_hangar/application/event_handlers/logging_handler.py +69 -0
- mcp_hangar/application/event_handlers/metrics_handler.py +152 -0
- mcp_hangar/application/event_handlers/persistent_audit_store.py +217 -0
- mcp_hangar/application/event_handlers/security_handler.py +604 -0
- mcp_hangar/application/mcp/tooling.py +158 -0
- mcp_hangar/application/ports/__init__.py +9 -0
- mcp_hangar/application/ports/observability.py +237 -0
- mcp_hangar/application/queries/__init__.py +52 -0
- mcp_hangar/application/queries/auth_handlers.py +237 -0
- mcp_hangar/application/queries/auth_queries.py +118 -0
- mcp_hangar/application/queries/handlers.py +227 -0
- mcp_hangar/application/read_models/__init__.py +11 -0
- mcp_hangar/application/read_models/provider_views.py +139 -0
- mcp_hangar/application/sagas/__init__.py +11 -0
- mcp_hangar/application/sagas/group_rebalance_saga.py +137 -0
- mcp_hangar/application/sagas/provider_failover_saga.py +266 -0
- mcp_hangar/application/sagas/provider_recovery_saga.py +172 -0
- mcp_hangar/application/services/__init__.py +9 -0
- mcp_hangar/application/services/provider_service.py +208 -0
- mcp_hangar/application/services/traced_provider_service.py +211 -0
- mcp_hangar/bootstrap/runtime.py +328 -0
- mcp_hangar/context.py +178 -0
- mcp_hangar/domain/__init__.py +117 -0
- mcp_hangar/domain/contracts/__init__.py +57 -0
- mcp_hangar/domain/contracts/authentication.py +225 -0
- mcp_hangar/domain/contracts/authorization.py +229 -0
- mcp_hangar/domain/contracts/event_store.py +178 -0
- mcp_hangar/domain/contracts/metrics_publisher.py +59 -0
- mcp_hangar/domain/contracts/persistence.py +383 -0
- mcp_hangar/domain/contracts/provider_runtime.py +146 -0
- mcp_hangar/domain/discovery/__init__.py +20 -0
- mcp_hangar/domain/discovery/conflict_resolver.py +267 -0
- mcp_hangar/domain/discovery/discovered_provider.py +185 -0
- mcp_hangar/domain/discovery/discovery_service.py +412 -0
- mcp_hangar/domain/discovery/discovery_source.py +192 -0
- mcp_hangar/domain/events.py +433 -0
- mcp_hangar/domain/exceptions.py +525 -0
- mcp_hangar/domain/model/__init__.py +70 -0
- mcp_hangar/domain/model/aggregate.py +58 -0
- mcp_hangar/domain/model/circuit_breaker.py +152 -0
- mcp_hangar/domain/model/event_sourced_api_key.py +413 -0
- mcp_hangar/domain/model/event_sourced_provider.py +423 -0
- mcp_hangar/domain/model/event_sourced_role_assignment.py +268 -0
- mcp_hangar/domain/model/health_tracker.py +183 -0
- mcp_hangar/domain/model/load_balancer.py +185 -0
- mcp_hangar/domain/model/provider.py +810 -0
- mcp_hangar/domain/model/provider_group.py +656 -0
- mcp_hangar/domain/model/tool_catalog.py +105 -0
- mcp_hangar/domain/policies/__init__.py +19 -0
- mcp_hangar/domain/policies/provider_health.py +187 -0
- mcp_hangar/domain/repository.py +249 -0
- mcp_hangar/domain/security/__init__.py +85 -0
- mcp_hangar/domain/security/input_validator.py +710 -0
- mcp_hangar/domain/security/rate_limiter.py +387 -0
- mcp_hangar/domain/security/roles.py +237 -0
- mcp_hangar/domain/security/sanitizer.py +387 -0
- mcp_hangar/domain/security/secrets.py +501 -0
- mcp_hangar/domain/services/__init__.py +20 -0
- mcp_hangar/domain/services/audit_service.py +376 -0
- mcp_hangar/domain/services/image_builder.py +328 -0
- mcp_hangar/domain/services/provider_launcher.py +1046 -0
- mcp_hangar/domain/value_objects.py +1138 -0
- mcp_hangar/errors.py +818 -0
- mcp_hangar/fastmcp_server.py +1105 -0
- mcp_hangar/gc.py +134 -0
- mcp_hangar/infrastructure/__init__.py +79 -0
- mcp_hangar/infrastructure/async_executor.py +133 -0
- mcp_hangar/infrastructure/auth/__init__.py +37 -0
- mcp_hangar/infrastructure/auth/api_key_authenticator.py +388 -0
- mcp_hangar/infrastructure/auth/event_sourced_store.py +567 -0
- mcp_hangar/infrastructure/auth/jwt_authenticator.py +360 -0
- mcp_hangar/infrastructure/auth/middleware.py +340 -0
- mcp_hangar/infrastructure/auth/opa_authorizer.py +243 -0
- mcp_hangar/infrastructure/auth/postgres_store.py +659 -0
- mcp_hangar/infrastructure/auth/projections.py +366 -0
- mcp_hangar/infrastructure/auth/rate_limiter.py +311 -0
- mcp_hangar/infrastructure/auth/rbac_authorizer.py +323 -0
- mcp_hangar/infrastructure/auth/sqlite_store.py +624 -0
- mcp_hangar/infrastructure/command_bus.py +112 -0
- mcp_hangar/infrastructure/discovery/__init__.py +110 -0
- mcp_hangar/infrastructure/discovery/docker_source.py +289 -0
- mcp_hangar/infrastructure/discovery/entrypoint_source.py +249 -0
- mcp_hangar/infrastructure/discovery/filesystem_source.py +383 -0
- mcp_hangar/infrastructure/discovery/kubernetes_source.py +247 -0
- mcp_hangar/infrastructure/event_bus.py +260 -0
- mcp_hangar/infrastructure/event_sourced_repository.py +443 -0
- mcp_hangar/infrastructure/event_store.py +396 -0
- mcp_hangar/infrastructure/knowledge_base/__init__.py +259 -0
- mcp_hangar/infrastructure/knowledge_base/contracts.py +202 -0
- mcp_hangar/infrastructure/knowledge_base/memory.py +177 -0
- mcp_hangar/infrastructure/knowledge_base/postgres.py +545 -0
- mcp_hangar/infrastructure/knowledge_base/sqlite.py +513 -0
- mcp_hangar/infrastructure/metrics_publisher.py +36 -0
- mcp_hangar/infrastructure/observability/__init__.py +10 -0
- mcp_hangar/infrastructure/observability/langfuse_adapter.py +534 -0
- mcp_hangar/infrastructure/persistence/__init__.py +33 -0
- mcp_hangar/infrastructure/persistence/audit_repository.py +371 -0
- mcp_hangar/infrastructure/persistence/config_repository.py +398 -0
- mcp_hangar/infrastructure/persistence/database.py +333 -0
- mcp_hangar/infrastructure/persistence/database_common.py +330 -0
- mcp_hangar/infrastructure/persistence/event_serializer.py +280 -0
- mcp_hangar/infrastructure/persistence/event_upcaster.py +166 -0
- mcp_hangar/infrastructure/persistence/in_memory_event_store.py +150 -0
- mcp_hangar/infrastructure/persistence/recovery_service.py +312 -0
- mcp_hangar/infrastructure/persistence/sqlite_event_store.py +386 -0
- mcp_hangar/infrastructure/persistence/unit_of_work.py +409 -0
- mcp_hangar/infrastructure/persistence/upcasters/README.md +13 -0
- mcp_hangar/infrastructure/persistence/upcasters/__init__.py +7 -0
- mcp_hangar/infrastructure/query_bus.py +153 -0
- mcp_hangar/infrastructure/saga_manager.py +401 -0
- mcp_hangar/logging_config.py +209 -0
- mcp_hangar/metrics.py +1007 -0
- mcp_hangar/models.py +31 -0
- mcp_hangar/observability/__init__.py +54 -0
- mcp_hangar/observability/health.py +487 -0
- mcp_hangar/observability/metrics.py +319 -0
- mcp_hangar/observability/tracing.py +433 -0
- mcp_hangar/progress.py +542 -0
- mcp_hangar/retry.py +613 -0
- mcp_hangar/server/__init__.py +120 -0
- mcp_hangar/server/__main__.py +6 -0
- mcp_hangar/server/auth_bootstrap.py +340 -0
- mcp_hangar/server/auth_cli.py +335 -0
- mcp_hangar/server/auth_config.py +305 -0
- mcp_hangar/server/bootstrap.py +735 -0
- mcp_hangar/server/cli.py +161 -0
- mcp_hangar/server/config.py +224 -0
- mcp_hangar/server/context.py +215 -0
- mcp_hangar/server/http_auth_middleware.py +165 -0
- mcp_hangar/server/lifecycle.py +467 -0
- mcp_hangar/server/state.py +117 -0
- mcp_hangar/server/tools/__init__.py +16 -0
- mcp_hangar/server/tools/discovery.py +186 -0
- mcp_hangar/server/tools/groups.py +75 -0
- mcp_hangar/server/tools/health.py +301 -0
- mcp_hangar/server/tools/provider.py +939 -0
- mcp_hangar/server/tools/registry.py +320 -0
- mcp_hangar/server/validation.py +113 -0
- mcp_hangar/stdio_client.py +229 -0
- mcp_hangar-0.2.0.dist-info/METADATA +347 -0
- mcp_hangar-0.2.0.dist-info/RECORD +160 -0
- mcp_hangar-0.2.0.dist-info/WHEEL +4 -0
- mcp_hangar-0.2.0.dist-info/entry_points.txt +2 -0
- 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()
|