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,217 @@
|
|
|
1
|
+
"""Persistence-backed audit store adapter.
|
|
2
|
+
|
|
3
|
+
Connects the existing AuditEventHandler with the new
|
|
4
|
+
persistent IAuditRepository implementation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from ...domain.contracts.persistence import AuditAction, AuditEntry, IAuditRepository
|
|
12
|
+
from ...infrastructure.async_executor import submit_async
|
|
13
|
+
from ...logging_config import get_logger
|
|
14
|
+
from .audit_handler import AuditRecord, AuditStore
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PersistentAuditStore(AuditStore):
|
|
20
|
+
"""Audit store backed by IAuditRepository.
|
|
21
|
+
|
|
22
|
+
Bridges the synchronous AuditEventHandler with the
|
|
23
|
+
async IAuditRepository for persistent storage.
|
|
24
|
+
|
|
25
|
+
Uses AsyncExecutor for efficient background persistence
|
|
26
|
+
without blocking the main thread.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, repository: IAuditRepository):
|
|
30
|
+
"""Initialize with audit repository.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
repository: Async audit repository for persistence
|
|
34
|
+
"""
|
|
35
|
+
self._repo = repository
|
|
36
|
+
|
|
37
|
+
def record(self, audit_record: AuditRecord) -> None:
|
|
38
|
+
"""Store an audit record asynchronously.
|
|
39
|
+
|
|
40
|
+
Converts AuditRecord to AuditEntry and persists using
|
|
41
|
+
background executor.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
audit_record: Record to store
|
|
45
|
+
"""
|
|
46
|
+
entry = self._record_to_entry(audit_record)
|
|
47
|
+
|
|
48
|
+
submit_async(self._async_record(entry), on_error=lambda e: logger.error(f"Failed to persist audit record: {e}"))
|
|
49
|
+
|
|
50
|
+
async def _async_record(self, entry: AuditEntry) -> None:
|
|
51
|
+
"""Async record method."""
|
|
52
|
+
try:
|
|
53
|
+
await self._repo.append(entry)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.error(f"Failed to persist audit entry: {e}")
|
|
56
|
+
|
|
57
|
+
def query(
|
|
58
|
+
self,
|
|
59
|
+
provider_id: Optional[str] = None,
|
|
60
|
+
event_type: Optional[str] = None,
|
|
61
|
+
since: Optional[datetime] = None,
|
|
62
|
+
limit: int = 100,
|
|
63
|
+
) -> List[AuditRecord]:
|
|
64
|
+
"""Query audit records.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
provider_id: Filter by provider ID
|
|
68
|
+
event_type: Filter by event type (entity_type in new model)
|
|
69
|
+
since: Filter records after this time
|
|
70
|
+
limit: Maximum records to return
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
List of audit records
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
if provider_id:
|
|
77
|
+
coro = self._repo.get_by_entity(
|
|
78
|
+
entity_id=provider_id,
|
|
79
|
+
entity_type="provider",
|
|
80
|
+
limit=limit,
|
|
81
|
+
)
|
|
82
|
+
elif since:
|
|
83
|
+
coro = self._repo.get_by_time_range(
|
|
84
|
+
start=since,
|
|
85
|
+
end=datetime.now(timezone.utc),
|
|
86
|
+
entity_type=event_type,
|
|
87
|
+
limit=limit,
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
# Get recent entries
|
|
91
|
+
coro = self._repo.get_by_time_range(
|
|
92
|
+
start=datetime(2000, 1, 1, tzinfo=timezone.utc),
|
|
93
|
+
end=datetime.now(timezone.utc),
|
|
94
|
+
entity_type=event_type,
|
|
95
|
+
limit=limit,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Check if we're in an async context
|
|
99
|
+
try:
|
|
100
|
+
asyncio.get_running_loop()
|
|
101
|
+
# We're in async context - can't run sync query
|
|
102
|
+
logger.warning("Cannot query audit in async context synchronously")
|
|
103
|
+
return []
|
|
104
|
+
except RuntimeError:
|
|
105
|
+
# Not in async context - safe to use asyncio.run()
|
|
106
|
+
entries = asyncio.run(coro)
|
|
107
|
+
return [self._entry_to_record(e) for e in entries]
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Failed to query audit records: {e}")
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
def _record_to_entry(self, record: AuditRecord) -> AuditEntry:
|
|
114
|
+
"""Convert AuditRecord to AuditEntry.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
record: Legacy audit record
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
New AuditEntry format
|
|
121
|
+
"""
|
|
122
|
+
# Map event type to action
|
|
123
|
+
action = self._map_event_to_action(record.event_type)
|
|
124
|
+
|
|
125
|
+
# Handle timestamp
|
|
126
|
+
if isinstance(record.occurred_at, datetime):
|
|
127
|
+
timestamp = record.occurred_at
|
|
128
|
+
elif isinstance(record.occurred_at, (int, float)):
|
|
129
|
+
timestamp = datetime.fromtimestamp(record.occurred_at, tz=timezone.utc)
|
|
130
|
+
else:
|
|
131
|
+
timestamp = datetime.now(timezone.utc)
|
|
132
|
+
|
|
133
|
+
return AuditEntry(
|
|
134
|
+
entity_id=record.provider_id or "_unknown",
|
|
135
|
+
entity_type="provider",
|
|
136
|
+
action=action,
|
|
137
|
+
timestamp=timestamp,
|
|
138
|
+
actor="system",
|
|
139
|
+
metadata={
|
|
140
|
+
"event_id": record.event_id,
|
|
141
|
+
"event_type": record.event_type,
|
|
142
|
+
"data": record.data,
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _entry_to_record(self, entry: AuditEntry) -> AuditRecord:
|
|
147
|
+
"""Convert AuditEntry to AuditRecord.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
entry: New audit entry
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Legacy AuditRecord format
|
|
154
|
+
"""
|
|
155
|
+
event_id = entry.metadata.get("event_id", str(entry.timestamp.timestamp()))
|
|
156
|
+
event_type = entry.metadata.get("event_type", entry.action.value)
|
|
157
|
+
data = entry.metadata.get("data", {})
|
|
158
|
+
|
|
159
|
+
return AuditRecord(
|
|
160
|
+
event_id=event_id,
|
|
161
|
+
event_type=event_type,
|
|
162
|
+
occurred_at=entry.timestamp,
|
|
163
|
+
provider_id=entry.entity_id if entry.entity_id != "_unknown" else None,
|
|
164
|
+
data=data,
|
|
165
|
+
recorded_at=entry.timestamp,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _map_event_to_action(self, event_type: str) -> AuditAction:
|
|
169
|
+
"""Map domain event type to audit action.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
event_type: Domain event class name
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Corresponding AuditAction
|
|
176
|
+
"""
|
|
177
|
+
mapping = {
|
|
178
|
+
"ProviderStarted": AuditAction.STARTED,
|
|
179
|
+
"ProviderStopped": AuditAction.STOPPED,
|
|
180
|
+
"ProviderDegraded": AuditAction.DEGRADED,
|
|
181
|
+
"ProviderStateChanged": AuditAction.STATE_CHANGED,
|
|
182
|
+
"ProviderRegistered": AuditAction.CREATED,
|
|
183
|
+
"ProviderUnregistered": AuditAction.DELETED,
|
|
184
|
+
"ToolInvocationCompleted": AuditAction.UPDATED,
|
|
185
|
+
"ToolInvocationFailed": AuditAction.STATE_CHANGED,
|
|
186
|
+
"HealthCheckPassed": AuditAction.RECOVERED,
|
|
187
|
+
"HealthCheckFailed": AuditAction.DEGRADED,
|
|
188
|
+
}
|
|
189
|
+
return mapping.get(event_type, AuditAction.UPDATED)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def create_persistent_audit_handler(
|
|
193
|
+
repository: IAuditRepository,
|
|
194
|
+
include_event_types: Optional[List[str]] = None,
|
|
195
|
+
exclude_event_types: Optional[List[str]] = None,
|
|
196
|
+
):
|
|
197
|
+
"""Create AuditEventHandler with persistent storage.
|
|
198
|
+
|
|
199
|
+
Factory function to create an AuditEventHandler backed
|
|
200
|
+
by an IAuditRepository for durable storage.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
repository: Audit repository for persistence
|
|
204
|
+
include_event_types: Only record these event types
|
|
205
|
+
exclude_event_types: Exclude these event types
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Configured AuditEventHandler
|
|
209
|
+
"""
|
|
210
|
+
from .audit_handler import AuditEventHandler
|
|
211
|
+
|
|
212
|
+
store = PersistentAuditStore(repository)
|
|
213
|
+
return AuditEventHandler(
|
|
214
|
+
store=store,
|
|
215
|
+
include_event_types=include_event_types,
|
|
216
|
+
exclude_event_types=exclude_event_types,
|
|
217
|
+
)
|