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,383 @@
|
|
|
1
|
+
"""Domain contracts for persistence layer.
|
|
2
|
+
|
|
3
|
+
These protocols define the interfaces that infrastructure must implement,
|
|
4
|
+
following the Dependency Inversion Principle (DIP) from SOLID.
|
|
5
|
+
|
|
6
|
+
The domain layer owns these contracts - infrastructure depends on domain,
|
|
7
|
+
not the other way around.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Dict, List, Optional, Protocol
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuditAction(Enum):
|
|
17
|
+
"""Types of auditable actions on entities."""
|
|
18
|
+
|
|
19
|
+
CREATED = "created"
|
|
20
|
+
UPDATED = "updated"
|
|
21
|
+
DELETED = "deleted"
|
|
22
|
+
STATE_CHANGED = "state_changed"
|
|
23
|
+
STARTED = "started"
|
|
24
|
+
STOPPED = "stopped"
|
|
25
|
+
DEGRADED = "degraded"
|
|
26
|
+
RECOVERED = "recovered"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class AuditEntry:
|
|
31
|
+
"""Immutable record of an auditable action.
|
|
32
|
+
|
|
33
|
+
Value object representing a single audit log entry.
|
|
34
|
+
Immutability ensures audit trail integrity.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
entity_id: str
|
|
38
|
+
entity_type: str
|
|
39
|
+
action: AuditAction
|
|
40
|
+
timestamp: datetime
|
|
41
|
+
actor: str # who performed the action (system, user, etc.)
|
|
42
|
+
old_state: Optional[Dict[str, Any]] = None
|
|
43
|
+
new_state: Optional[Dict[str, Any]] = None
|
|
44
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
45
|
+
correlation_id: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
48
|
+
"""Serialize to dictionary for storage."""
|
|
49
|
+
return {
|
|
50
|
+
"entity_id": self.entity_id,
|
|
51
|
+
"entity_type": self.entity_type,
|
|
52
|
+
"action": self.action.value,
|
|
53
|
+
"timestamp": self.timestamp.isoformat(),
|
|
54
|
+
"actor": self.actor,
|
|
55
|
+
"old_state": self.old_state,
|
|
56
|
+
"new_state": self.new_state,
|
|
57
|
+
"metadata": self.metadata,
|
|
58
|
+
"correlation_id": self.correlation_id,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_dict(cls, data: Dict[str, Any]) -> "AuditEntry":
|
|
63
|
+
"""Deserialize from dictionary."""
|
|
64
|
+
return cls(
|
|
65
|
+
entity_id=data["entity_id"],
|
|
66
|
+
entity_type=data["entity_type"],
|
|
67
|
+
action=AuditAction(data["action"]),
|
|
68
|
+
timestamp=datetime.fromisoformat(data["timestamp"]),
|
|
69
|
+
actor=data["actor"],
|
|
70
|
+
old_state=data.get("old_state"),
|
|
71
|
+
new_state=data.get("new_state"),
|
|
72
|
+
metadata=data.get("metadata", {}),
|
|
73
|
+
correlation_id=data.get("correlation_id"),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class ProviderConfigSnapshot:
|
|
79
|
+
"""Immutable snapshot of provider configuration.
|
|
80
|
+
|
|
81
|
+
Captures the complete configuration state at a point in time,
|
|
82
|
+
used for persistence and recovery.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
provider_id: str
|
|
86
|
+
mode: str
|
|
87
|
+
command: Optional[List[str]] = None
|
|
88
|
+
image: Optional[str] = None
|
|
89
|
+
endpoint: Optional[str] = None
|
|
90
|
+
env: Dict[str, str] = field(default_factory=dict)
|
|
91
|
+
idle_ttl_s: int = 300
|
|
92
|
+
health_check_interval_s: int = 60
|
|
93
|
+
max_consecutive_failures: int = 3
|
|
94
|
+
description: Optional[str] = None
|
|
95
|
+
volumes: List[str] = field(default_factory=list)
|
|
96
|
+
build: Optional[Dict[str, str]] = None
|
|
97
|
+
resources: Dict[str, str] = field(default_factory=dict)
|
|
98
|
+
network: str = "none"
|
|
99
|
+
read_only: bool = True
|
|
100
|
+
user: Optional[str] = None
|
|
101
|
+
tools: Optional[List[Dict[str, Any]]] = None
|
|
102
|
+
enabled: bool = True
|
|
103
|
+
created_at: Optional[datetime] = None
|
|
104
|
+
updated_at: Optional[datetime] = None
|
|
105
|
+
|
|
106
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
107
|
+
"""Serialize to dictionary for storage."""
|
|
108
|
+
return {
|
|
109
|
+
"provider_id": self.provider_id,
|
|
110
|
+
"mode": self.mode,
|
|
111
|
+
"command": self.command,
|
|
112
|
+
"image": self.image,
|
|
113
|
+
"endpoint": self.endpoint,
|
|
114
|
+
"env": self.env,
|
|
115
|
+
"idle_ttl_s": self.idle_ttl_s,
|
|
116
|
+
"health_check_interval_s": self.health_check_interval_s,
|
|
117
|
+
"max_consecutive_failures": self.max_consecutive_failures,
|
|
118
|
+
"description": self.description,
|
|
119
|
+
"volumes": self.volumes,
|
|
120
|
+
"build": self.build,
|
|
121
|
+
"resources": self.resources,
|
|
122
|
+
"network": self.network,
|
|
123
|
+
"read_only": self.read_only,
|
|
124
|
+
"user": self.user,
|
|
125
|
+
"tools": self.tools,
|
|
126
|
+
"enabled": self.enabled,
|
|
127
|
+
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
128
|
+
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ProviderConfigSnapshot":
|
|
133
|
+
"""Deserialize from dictionary."""
|
|
134
|
+
created_at = data.get("created_at")
|
|
135
|
+
updated_at = data.get("updated_at")
|
|
136
|
+
|
|
137
|
+
return cls(
|
|
138
|
+
provider_id=data["provider_id"],
|
|
139
|
+
mode=data["mode"],
|
|
140
|
+
command=data.get("command"),
|
|
141
|
+
image=data.get("image"),
|
|
142
|
+
endpoint=data.get("endpoint"),
|
|
143
|
+
env=data.get("env", {}),
|
|
144
|
+
idle_ttl_s=data.get("idle_ttl_s", 300),
|
|
145
|
+
health_check_interval_s=data.get("health_check_interval_s", 60),
|
|
146
|
+
max_consecutive_failures=data.get("max_consecutive_failures", 3),
|
|
147
|
+
description=data.get("description"),
|
|
148
|
+
volumes=data.get("volumes", []),
|
|
149
|
+
build=data.get("build"),
|
|
150
|
+
resources=data.get("resources", {}),
|
|
151
|
+
network=data.get("network", "none"),
|
|
152
|
+
read_only=data.get("read_only", True),
|
|
153
|
+
user=data.get("user"),
|
|
154
|
+
tools=data.get("tools"),
|
|
155
|
+
enabled=data.get("enabled", True),
|
|
156
|
+
created_at=datetime.fromisoformat(created_at) if created_at else None,
|
|
157
|
+
updated_at=datetime.fromisoformat(updated_at) if updated_at else None,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class IProviderConfigRepository(Protocol):
|
|
162
|
+
"""Repository protocol for provider configuration persistence.
|
|
163
|
+
|
|
164
|
+
Follows Repository pattern from DDD - mediates between domain
|
|
165
|
+
and data mapping layers using a collection-like interface.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
async def save(self, config: ProviderConfigSnapshot) -> None:
|
|
169
|
+
"""Save provider configuration.
|
|
170
|
+
|
|
171
|
+
Creates or updates the configuration in persistent storage.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
config: Provider configuration snapshot to save
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
PersistenceError: If save operation fails
|
|
178
|
+
"""
|
|
179
|
+
...
|
|
180
|
+
|
|
181
|
+
async def get(self, provider_id: str) -> Optional[ProviderConfigSnapshot]:
|
|
182
|
+
"""Retrieve provider configuration by ID.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
provider_id: Unique provider identifier
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Configuration snapshot if found, None otherwise
|
|
189
|
+
"""
|
|
190
|
+
...
|
|
191
|
+
|
|
192
|
+
async def get_all(self) -> List[ProviderConfigSnapshot]:
|
|
193
|
+
"""Retrieve all provider configurations.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
List of all stored configurations
|
|
197
|
+
"""
|
|
198
|
+
...
|
|
199
|
+
|
|
200
|
+
async def delete(self, provider_id: str) -> bool:
|
|
201
|
+
"""Delete provider configuration.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
provider_id: Provider identifier to delete
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
True if deleted, False if not found
|
|
208
|
+
"""
|
|
209
|
+
...
|
|
210
|
+
|
|
211
|
+
async def exists(self, provider_id: str) -> bool:
|
|
212
|
+
"""Check if provider configuration exists.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
provider_id: Provider identifier to check
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
True if exists, False otherwise
|
|
219
|
+
"""
|
|
220
|
+
...
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class IAuditRepository(Protocol):
|
|
224
|
+
"""Repository protocol for audit log persistence.
|
|
225
|
+
|
|
226
|
+
Provides append-only storage for audit entries, ensuring
|
|
227
|
+
immutable audit trail for accountability.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
async def append(self, entry: AuditEntry) -> None:
|
|
231
|
+
"""Append an audit entry.
|
|
232
|
+
|
|
233
|
+
Audit entries are immutable once written.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
entry: Audit entry to append
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
PersistenceError: If append operation fails
|
|
240
|
+
"""
|
|
241
|
+
...
|
|
242
|
+
|
|
243
|
+
async def get_by_entity(
|
|
244
|
+
self,
|
|
245
|
+
entity_id: str,
|
|
246
|
+
entity_type: Optional[str] = None,
|
|
247
|
+
limit: int = 100,
|
|
248
|
+
offset: int = 0,
|
|
249
|
+
) -> List[AuditEntry]:
|
|
250
|
+
"""Get audit entries for an entity.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
entity_id: Entity identifier
|
|
254
|
+
entity_type: Optional entity type filter
|
|
255
|
+
limit: Maximum entries to return
|
|
256
|
+
offset: Number of entries to skip
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
List of audit entries, newest first
|
|
260
|
+
"""
|
|
261
|
+
...
|
|
262
|
+
|
|
263
|
+
async def get_by_time_range(
|
|
264
|
+
self,
|
|
265
|
+
start: datetime,
|
|
266
|
+
end: datetime,
|
|
267
|
+
entity_type: Optional[str] = None,
|
|
268
|
+
action: Optional[AuditAction] = None,
|
|
269
|
+
limit: int = 1000,
|
|
270
|
+
) -> List[AuditEntry]:
|
|
271
|
+
"""Get audit entries within a time range.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
start: Start of time range (inclusive)
|
|
275
|
+
end: End of time range (inclusive)
|
|
276
|
+
entity_type: Optional entity type filter
|
|
277
|
+
action: Optional action filter
|
|
278
|
+
limit: Maximum entries to return
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
List of audit entries, newest first
|
|
282
|
+
"""
|
|
283
|
+
...
|
|
284
|
+
|
|
285
|
+
async def get_by_correlation_id(self, correlation_id: str) -> List[AuditEntry]:
|
|
286
|
+
"""Get all audit entries for a correlation ID.
|
|
287
|
+
|
|
288
|
+
Useful for tracing distributed operations.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
correlation_id: Correlation identifier
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
List of related audit entries
|
|
295
|
+
"""
|
|
296
|
+
...
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class IUnitOfWork(Protocol):
|
|
300
|
+
"""Unit of Work pattern for transactional consistency.
|
|
301
|
+
|
|
302
|
+
Manages transactions across multiple repositories,
|
|
303
|
+
ensuring atomic commits or rollbacks.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
async def __aenter__(self) -> "IUnitOfWork":
|
|
307
|
+
"""Begin transaction."""
|
|
308
|
+
...
|
|
309
|
+
|
|
310
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
311
|
+
"""End transaction - commit on success, rollback on exception."""
|
|
312
|
+
...
|
|
313
|
+
|
|
314
|
+
async def commit(self) -> None:
|
|
315
|
+
"""Explicitly commit the transaction."""
|
|
316
|
+
...
|
|
317
|
+
|
|
318
|
+
async def rollback(self) -> None:
|
|
319
|
+
"""Explicitly rollback the transaction."""
|
|
320
|
+
...
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def providers(self) -> IProviderConfigRepository:
|
|
324
|
+
"""Access provider config repository within transaction."""
|
|
325
|
+
...
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def audit(self) -> IAuditRepository:
|
|
329
|
+
"""Access audit repository within transaction."""
|
|
330
|
+
...
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class IRecoveryService(Protocol):
|
|
334
|
+
"""Service protocol for system recovery on startup.
|
|
335
|
+
|
|
336
|
+
Responsible for restoring system state from persistent storage.
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
async def recover_providers(self) -> List[str]:
|
|
340
|
+
"""Recover all provider configurations from storage.
|
|
341
|
+
|
|
342
|
+
Loads saved configurations and registers them with
|
|
343
|
+
the provider repository.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
List of recovered provider IDs
|
|
347
|
+
"""
|
|
348
|
+
...
|
|
349
|
+
|
|
350
|
+
async def get_recovery_status(self) -> Dict[str, Any]:
|
|
351
|
+
"""Get status of last recovery operation.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Dictionary with recovery metrics and status
|
|
355
|
+
"""
|
|
356
|
+
...
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class PersistenceError(Exception):
|
|
360
|
+
"""Base exception for persistence operations."""
|
|
361
|
+
|
|
362
|
+
pass
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class ConfigurationNotFoundError(PersistenceError):
|
|
366
|
+
"""Raised when configuration is not found."""
|
|
367
|
+
|
|
368
|
+
def __init__(self, provider_id: str):
|
|
369
|
+
self.provider_id = provider_id
|
|
370
|
+
super().__init__(f"Configuration not found for provider: {provider_id}")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class ConcurrentModificationError(PersistenceError):
|
|
374
|
+
"""Raised when concurrent modification is detected."""
|
|
375
|
+
|
|
376
|
+
def __init__(self, provider_id: str, expected_version: int, actual_version: int):
|
|
377
|
+
self.provider_id = provider_id
|
|
378
|
+
self.expected_version = expected_version
|
|
379
|
+
self.actual_version = actual_version
|
|
380
|
+
super().__init__(
|
|
381
|
+
f"Concurrent modification on provider '{provider_id}': "
|
|
382
|
+
f"expected version {expected_version}, actual {actual_version}"
|
|
383
|
+
)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Protocol contracts for provider-like objects.
|
|
2
|
+
|
|
3
|
+
These contracts define the *minimum* surface area required by infrastructure
|
|
4
|
+
components (e.g. background workers and command handlers) without importing
|
|
5
|
+
concrete implementations.
|
|
6
|
+
|
|
7
|
+
Why:
|
|
8
|
+
- Avoids duck-typing via hasattr(...)
|
|
9
|
+
- Makes the expected interface explicit and type-checkable
|
|
10
|
+
- Supports the domain `Provider` aggregate and any compatible implementations
|
|
11
|
+
|
|
12
|
+
Notes:
|
|
13
|
+
- Protocols are for typing only; they don't enforce runtime inheritance.
|
|
14
|
+
- Keep these contracts small and stable.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Dict, Iterable, List, Protocol, runtime_checkable
|
|
20
|
+
|
|
21
|
+
from ..events import DomainEvent
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@runtime_checkable
|
|
25
|
+
class SupportsEventCollection(Protocol):
|
|
26
|
+
"""Something that buffers domain events and can expose them for publishing."""
|
|
27
|
+
|
|
28
|
+
def collect_events(self) -> Iterable[DomainEvent]:
|
|
29
|
+
"""Return all currently buffered domain events and clear the buffer."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@runtime_checkable
|
|
34
|
+
class SupportsHealthCheck(Protocol):
|
|
35
|
+
"""Something that can perform an active health check."""
|
|
36
|
+
|
|
37
|
+
def health_check(self) -> bool:
|
|
38
|
+
"""Return True if healthy, False otherwise."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@runtime_checkable
|
|
43
|
+
class SupportsIdleShutdown(Protocol):
|
|
44
|
+
"""Something that can shut itself down when idle."""
|
|
45
|
+
|
|
46
|
+
def maybe_shutdown_idle(self) -> bool:
|
|
47
|
+
"""Shutdown when idle past TTL. Returns True if shutdown happened."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@runtime_checkable
|
|
52
|
+
class SupportsState(Protocol):
|
|
53
|
+
"""Something that exposes a state-like object.
|
|
54
|
+
|
|
55
|
+
We intentionally keep this loose: state can be an enum with a `.value`
|
|
56
|
+
or a string. Background worker may normalize this.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def state(self) -> Any: # enum-like or str
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@runtime_checkable
|
|
65
|
+
class SupportsHealthStats(Protocol):
|
|
66
|
+
"""Something that exposes health stats for metrics."""
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def health(self) -> Any:
|
|
70
|
+
"""Health tracker-like object (must expose `consecutive_failures`)."""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@runtime_checkable
|
|
75
|
+
class SupportsProviderLifecycle(Protocol):
|
|
76
|
+
"""Commands-side lifecycle surface required by command handlers."""
|
|
77
|
+
|
|
78
|
+
def ensure_ready(self) -> None:
|
|
79
|
+
"""Ensure provider is started and ready to accept requests."""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
def shutdown(self) -> None:
|
|
83
|
+
"""Stop provider and release resources."""
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@runtime_checkable
|
|
88
|
+
class SupportsToolInvocation(Protocol):
|
|
89
|
+
"""Commands-side tool invocation surface required by command handlers."""
|
|
90
|
+
|
|
91
|
+
def invoke_tool(self, tool_name: str, arguments: Dict[str, Any], timeout: float = 30.0) -> Dict[str, Any]:
|
|
92
|
+
"""Invoke a tool on the provider."""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
def get_tool_names(self) -> List[str]:
|
|
96
|
+
"""Get list of available tool names."""
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@runtime_checkable
|
|
101
|
+
class ProviderRuntime(
|
|
102
|
+
SupportsEventCollection,
|
|
103
|
+
SupportsHealthCheck,
|
|
104
|
+
SupportsIdleShutdown,
|
|
105
|
+
SupportsState,
|
|
106
|
+
SupportsHealthStats,
|
|
107
|
+
SupportsProviderLifecycle,
|
|
108
|
+
SupportsToolInvocation,
|
|
109
|
+
Protocol,
|
|
110
|
+
):
|
|
111
|
+
"""Provider-like runtime contract required by background worker and command handlers.
|
|
112
|
+
|
|
113
|
+
Any object satisfying this protocol can be managed by:
|
|
114
|
+
- GC/health workers
|
|
115
|
+
- CQRS command handlers
|
|
116
|
+
|
|
117
|
+
Primary implementation:
|
|
118
|
+
- domain aggregate: `mcp_hangar.domain.model.Provider`
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
# No additional members beyond the composed protocols.
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@runtime_checkable
|
|
126
|
+
class ProviderMapping(Protocol):
|
|
127
|
+
"""Dict-like view of providers consumed by BackgroundWorker.
|
|
128
|
+
|
|
129
|
+
BackgroundWorker only needs `.items()` for snapshot iteration.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def items(self) -> Iterable[tuple[str, ProviderRuntime]]: ...
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def normalize_state_to_str(state: Any) -> str:
|
|
136
|
+
"""Best-effort normalization of a state-like value to a lower-case string.
|
|
137
|
+
|
|
138
|
+
This exists to centralize normalization logic instead of scattering
|
|
139
|
+
`hasattr(state, "value")` checks around infrastructure code.
|
|
140
|
+
"""
|
|
141
|
+
if state is None:
|
|
142
|
+
return "unknown"
|
|
143
|
+
value = getattr(state, "value", None)
|
|
144
|
+
if value is not None:
|
|
145
|
+
return str(value).lower()
|
|
146
|
+
return str(state).lower()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Discovery domain module.
|
|
2
|
+
|
|
3
|
+
This module contains the domain model for provider discovery,
|
|
4
|
+
including value objects, ports, and domain services.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .conflict_resolver import ConflictResolution, ConflictResolver, ConflictResult
|
|
8
|
+
from .discovered_provider import DiscoveredProvider
|
|
9
|
+
from .discovery_service import DiscoveryService
|
|
10
|
+
from .discovery_source import DiscoveryMode, DiscoverySource
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"DiscoveredProvider",
|
|
14
|
+
"DiscoveryMode",
|
|
15
|
+
"DiscoverySource",
|
|
16
|
+
"ConflictResolution",
|
|
17
|
+
"ConflictResult",
|
|
18
|
+
"ConflictResolver",
|
|
19
|
+
"DiscoveryService",
|
|
20
|
+
]
|