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,433 @@
1
+ """Domain events for the MCP Registry.
2
+
3
+ Events capture important business occurrences and allow decoupled reactions.
4
+ """
5
+
6
+ from abc import ABC
7
+ from dataclasses import dataclass, field
8
+ import time
9
+ from typing import Any, Dict
10
+ import uuid
11
+
12
+
13
+ class DomainEvent(ABC):
14
+ """
15
+ Base class for all domain events.
16
+
17
+ Note: Not a dataclass to avoid inheritance issues.
18
+ Subclasses should be dataclasses.
19
+ """
20
+
21
+ def __init__(self):
22
+ self.event_id: str = str(uuid.uuid4())
23
+ self.occurred_at: float = time.time()
24
+
25
+ def to_dict(self) -> Dict[str, Any]:
26
+ """Convert event to dictionary for serialization."""
27
+ return {"event_type": self.__class__.__name__, **self.__dict__}
28
+
29
+
30
+ # Provider Lifecycle Events
31
+
32
+
33
+ @dataclass
34
+ class ProviderStarted(DomainEvent):
35
+ """Published when a provider successfully starts."""
36
+
37
+ provider_id: str
38
+ mode: str # subprocess, docker, remote
39
+ tools_count: int
40
+ startup_duration_ms: float
41
+
42
+ def __post_init__(self):
43
+ super().__init__()
44
+
45
+
46
+ @dataclass
47
+ class ProviderStopped(DomainEvent):
48
+ """Published when a provider stops."""
49
+
50
+ provider_id: str
51
+ reason: str # "shutdown", "idle", "error", "degraded"
52
+
53
+ def __post_init__(self):
54
+ super().__init__()
55
+
56
+
57
+ @dataclass
58
+ class ProviderDegraded(DomainEvent):
59
+ """Published when a provider enters degraded state."""
60
+
61
+ provider_id: str
62
+ consecutive_failures: int
63
+ total_failures: int
64
+ reason: str
65
+
66
+ def __post_init__(self):
67
+ super().__init__()
68
+
69
+
70
+ @dataclass
71
+ class ProviderStateChanged(DomainEvent):
72
+ """Published when provider state transitions."""
73
+
74
+ provider_id: str
75
+ old_state: str
76
+ new_state: str
77
+
78
+ def __post_init__(self):
79
+ super().__init__()
80
+
81
+
82
+ # Tool Invocation Events
83
+
84
+
85
+ @dataclass
86
+ class ToolInvocationRequested(DomainEvent):
87
+ """Published when a tool invocation is requested."""
88
+
89
+ provider_id: str
90
+ tool_name: str
91
+ correlation_id: str
92
+ arguments: Dict[str, Any] = field(default_factory=dict)
93
+
94
+ def __post_init__(self):
95
+ super().__init__()
96
+
97
+
98
+ @dataclass
99
+ class ToolInvocationCompleted(DomainEvent):
100
+ """Published when a tool invocation completes successfully."""
101
+
102
+ provider_id: str
103
+ tool_name: str
104
+ correlation_id: str
105
+ duration_ms: float
106
+ result_size_bytes: int = 0
107
+
108
+ def __post_init__(self):
109
+ super().__init__()
110
+
111
+
112
+ @dataclass
113
+ class ToolInvocationFailed(DomainEvent):
114
+ """Published when a tool invocation fails."""
115
+
116
+ provider_id: str
117
+ tool_name: str
118
+ correlation_id: str
119
+ error_message: str
120
+ error_type: str
121
+
122
+ def __post_init__(self):
123
+ super().__init__()
124
+
125
+
126
+ # Health Check Events
127
+
128
+
129
+ @dataclass
130
+ class HealthCheckPassed(DomainEvent):
131
+ """Published when a health check succeeds."""
132
+
133
+ provider_id: str
134
+ duration_ms: float
135
+
136
+ def __post_init__(self):
137
+ super().__init__()
138
+
139
+
140
+ @dataclass
141
+ class HealthCheckFailed(DomainEvent):
142
+ """Published when a health check fails."""
143
+
144
+ provider_id: str
145
+ consecutive_failures: int
146
+ error_message: str
147
+
148
+ def __post_init__(self):
149
+ super().__init__()
150
+
151
+
152
+ # Resource Management Events
153
+
154
+
155
+ @dataclass
156
+ class ProviderIdleDetected(DomainEvent):
157
+ """Published when a provider is detected as idle."""
158
+
159
+ provider_id: str
160
+ idle_duration_s: float
161
+ last_used_at: float
162
+
163
+ def __post_init__(self):
164
+ super().__init__()
165
+
166
+
167
+ # Provider Group Events are defined in mcp_hangar.domain.model.provider_group
168
+ # to avoid circular imports. Re-export them here for convenience.
169
+ # Import at runtime only when needed.
170
+
171
+
172
+ # Discovery Events
173
+
174
+
175
+ @dataclass
176
+ class ProviderDiscovered(DomainEvent):
177
+ """Published when a new provider is discovered."""
178
+
179
+ provider_name: str
180
+ source_type: str
181
+ mode: str
182
+ fingerprint: str
183
+
184
+ def __post_init__(self):
185
+ super().__init__()
186
+
187
+
188
+ @dataclass
189
+ class ProviderDiscoveryLost(DomainEvent):
190
+ """Published when a previously discovered provider is no longer found."""
191
+
192
+ provider_name: str
193
+ source_type: str
194
+ reason: str # "ttl_expired", "source_removed", etc.
195
+
196
+ def __post_init__(self):
197
+ super().__init__()
198
+
199
+
200
+ @dataclass
201
+ class ProviderDiscoveryConfigChanged(DomainEvent):
202
+ """Published when discovered provider configuration changes."""
203
+
204
+ provider_name: str
205
+ source_type: str
206
+ old_fingerprint: str
207
+ new_fingerprint: str
208
+
209
+ def __post_init__(self):
210
+ super().__init__()
211
+
212
+
213
+ @dataclass
214
+ class ProviderQuarantined(DomainEvent):
215
+ """Published when a discovered provider is quarantined."""
216
+
217
+ provider_name: str
218
+ source_type: str
219
+ reason: str
220
+ validation_result: str
221
+
222
+ def __post_init__(self):
223
+ super().__init__()
224
+
225
+
226
+ @dataclass
227
+ class ProviderApproved(DomainEvent):
228
+ """Published when a quarantined provider is approved."""
229
+
230
+ provider_name: str
231
+ source_type: str
232
+ approved_by: str # "manual" or "auto"
233
+
234
+ def __post_init__(self):
235
+ super().__init__()
236
+
237
+
238
+ @dataclass
239
+ class DiscoveryCycleCompleted(DomainEvent):
240
+ """Published when a discovery cycle completes."""
241
+
242
+ discovered_count: int
243
+ registered_count: int
244
+ deregistered_count: int
245
+ quarantined_count: int
246
+ error_count: int
247
+ duration_ms: float
248
+
249
+ def __post_init__(self):
250
+ super().__init__()
251
+
252
+
253
+ @dataclass
254
+ class DiscoverySourceHealthChanged(DomainEvent):
255
+ """Published when a discovery source health status changes."""
256
+
257
+ source_type: str
258
+ is_healthy: bool
259
+ error_message: str | None = None
260
+
261
+ def __post_init__(self):
262
+ super().__init__()
263
+
264
+
265
+ # Authentication & Authorization Events
266
+
267
+
268
+ @dataclass
269
+ class AuthenticationSucceeded(DomainEvent):
270
+ """Published when a principal successfully authenticates.
271
+
272
+ Attributes:
273
+ principal_id: The authenticated principal's identifier.
274
+ principal_type: Type of principal (user, service_account, system).
275
+ auth_method: Authentication method used (api_key, jwt, mtls).
276
+ source_ip: IP address of the request origin.
277
+ tenant_id: Optional tenant identifier if multi-tenancy is enabled.
278
+ """
279
+
280
+ principal_id: str
281
+ principal_type: str
282
+ auth_method: str
283
+ source_ip: str
284
+ tenant_id: str | None = None
285
+
286
+ def __post_init__(self):
287
+ super().__init__()
288
+
289
+
290
+ @dataclass
291
+ class AuthenticationFailed(DomainEvent):
292
+ """Published when authentication fails.
293
+
294
+ Attributes:
295
+ auth_method: Authentication method that was attempted.
296
+ source_ip: IP address of the request origin.
297
+ reason: Reason for failure (invalid_token, expired, revoked, unknown_key).
298
+ attempted_principal_id: Optional principal ID if it could be extracted.
299
+ """
300
+
301
+ auth_method: str
302
+ source_ip: str
303
+ reason: str
304
+ attempted_principal_id: str | None = None
305
+
306
+ def __post_init__(self):
307
+ super().__init__()
308
+
309
+
310
+ @dataclass
311
+ class AuthorizationDenied(DomainEvent):
312
+ """Published when an authorized principal is denied access.
313
+
314
+ Attributes:
315
+ principal_id: The principal who was denied.
316
+ action: The action that was attempted.
317
+ resource_type: Type of resource being accessed.
318
+ resource_id: Specific resource identifier.
319
+ reason: Why access was denied.
320
+ """
321
+
322
+ principal_id: str
323
+ action: str
324
+ resource_type: str
325
+ resource_id: str
326
+ reason: str
327
+
328
+ def __post_init__(self):
329
+ super().__init__()
330
+
331
+
332
+ @dataclass
333
+ class AuthorizationGranted(DomainEvent):
334
+ """Published when authorization is granted (for audit trail).
335
+
336
+ Attributes:
337
+ principal_id: The principal who was granted access.
338
+ action: The action that was authorized.
339
+ resource_type: Type of resource being accessed.
340
+ resource_id: Specific resource identifier.
341
+ granted_by_role: Role that granted the permission.
342
+ """
343
+
344
+ principal_id: str
345
+ action: str
346
+ resource_type: str
347
+ resource_id: str
348
+ granted_by_role: str
349
+
350
+ def __post_init__(self):
351
+ super().__init__()
352
+
353
+
354
+ @dataclass
355
+ class RoleAssigned(DomainEvent):
356
+ """Published when a role is assigned to a principal.
357
+
358
+ Attributes:
359
+ principal_id: Principal receiving the role.
360
+ role_name: Name of the role being assigned.
361
+ scope: Scope of the assignment (global, tenant:X, namespace:Y).
362
+ assigned_by: Principal who made the assignment.
363
+ """
364
+
365
+ principal_id: str
366
+ role_name: str
367
+ scope: str
368
+ assigned_by: str
369
+
370
+ def __post_init__(self):
371
+ super().__init__()
372
+
373
+
374
+ @dataclass
375
+ class RoleRevoked(DomainEvent):
376
+ """Published when a role is revoked from a principal.
377
+
378
+ Attributes:
379
+ principal_id: Principal losing the role.
380
+ role_name: Name of the role being revoked.
381
+ scope: Scope from which the role is being revoked.
382
+ revoked_by: Principal who made the revocation.
383
+ """
384
+
385
+ principal_id: str
386
+ role_name: str
387
+ scope: str
388
+ revoked_by: str
389
+
390
+ def __post_init__(self):
391
+ super().__init__()
392
+
393
+
394
+ @dataclass
395
+ class ApiKeyCreated(DomainEvent):
396
+ """Published when a new API key is created.
397
+
398
+ Attributes:
399
+ key_id: Unique identifier of the key (not the key itself).
400
+ principal_id: Principal the key authenticates as.
401
+ key_name: Human-readable name for the key.
402
+ expires_at: Optional expiration timestamp.
403
+ created_by: Principal who created the key.
404
+ """
405
+
406
+ key_id: str
407
+ principal_id: str
408
+ key_name: str
409
+ expires_at: float | None
410
+ created_by: str
411
+
412
+ def __post_init__(self):
413
+ super().__init__()
414
+
415
+
416
+ @dataclass
417
+ class ApiKeyRevoked(DomainEvent):
418
+ """Published when an API key is revoked.
419
+
420
+ Attributes:
421
+ key_id: Unique identifier of the revoked key.
422
+ principal_id: Principal the key belonged to.
423
+ revoked_by: Principal who revoked the key.
424
+ reason: Optional reason for revocation.
425
+ """
426
+
427
+ key_id: str
428
+ principal_id: str
429
+ revoked_by: str
430
+ reason: str = ""
431
+
432
+ def __post_init__(self):
433
+ super().__init__()