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,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
+ )