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,525 @@
1
+ """
2
+ Domain exceptions for the MCP Registry.
3
+
4
+ All domain-specific exceptions should be defined here.
5
+ These exceptions carry context and can be serialized to structured error responses.
6
+ """
7
+
8
+ from typing import Any, Dict, Optional
9
+
10
+
11
+ class MCPError(Exception):
12
+ """Base exception for all MCP registry errors.
13
+
14
+ Provides structured error information with context for debugging and logging.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ message: str,
20
+ provider_id: str = "",
21
+ operation: str = "",
22
+ details: Optional[Dict[str, Any]] = None,
23
+ ):
24
+ super().__init__(message)
25
+ self.message = message
26
+ self.provider_id = provider_id
27
+ self.operation = operation
28
+ self.details = details or {}
29
+
30
+ def to_dict(self) -> Dict[str, Any]:
31
+ """Convert to structured error dictionary for API responses."""
32
+ return {
33
+ "error": self.message,
34
+ "provider_id": self.provider_id,
35
+ "operation": self.operation,
36
+ "details": self.details,
37
+ "type": self.__class__.__name__,
38
+ }
39
+
40
+ def __repr__(self) -> str:
41
+ return (
42
+ f"{self.__class__.__name__}("
43
+ f"message={self.message!r}, "
44
+ f"provider_id={self.provider_id!r}, "
45
+ f"operation={self.operation!r})"
46
+ )
47
+
48
+
49
+ # --- Provider Lifecycle Exceptions ---
50
+
51
+
52
+ class ProviderError(MCPError):
53
+ """Base exception for provider-related errors."""
54
+
55
+ pass
56
+
57
+
58
+ class ProviderNotFoundError(ProviderError):
59
+ """Raised when a provider is not found in the registry."""
60
+
61
+ def __init__(self, provider_id: str):
62
+ super().__init__(
63
+ message=f"Provider not found: {provider_id}",
64
+ provider_id=provider_id,
65
+ operation="lookup",
66
+ )
67
+
68
+
69
+ class ProviderStartError(ProviderError):
70
+ """Raised when a provider fails to start."""
71
+
72
+ def __init__(self, provider_id: str, reason: str, details: Optional[Dict[str, Any]] = None):
73
+ super().__init__(
74
+ message=f"Failed to start provider: {reason}",
75
+ provider_id=provider_id,
76
+ operation="start",
77
+ details=details or {},
78
+ )
79
+ self.reason = reason
80
+
81
+
82
+ class ProviderDegradedError(ProviderError):
83
+ """Raised when a provider is in degraded state and cannot accept requests."""
84
+
85
+ def __init__(
86
+ self,
87
+ provider_id: str,
88
+ backoff_remaining: float = 0,
89
+ consecutive_failures: int = 0,
90
+ ):
91
+ super().__init__(
92
+ message=f"Provider is degraded, retry in {backoff_remaining:.1f}s",
93
+ provider_id=provider_id,
94
+ operation="ensure_ready",
95
+ details={
96
+ "backoff_remaining_s": backoff_remaining,
97
+ "consecutive_failures": consecutive_failures,
98
+ },
99
+ )
100
+ self.backoff_remaining = backoff_remaining
101
+ self.consecutive_failures = consecutive_failures
102
+
103
+
104
+ class CannotStartProviderError(ProviderError):
105
+ """Raised when provider cannot be started due to backoff or other constraints."""
106
+
107
+ def __init__(self, provider_id: str, reason: str, time_until_retry: float = 0):
108
+ super().__init__(
109
+ message=f"Cannot start provider: {reason}",
110
+ provider_id=provider_id,
111
+ operation="start",
112
+ details={"time_until_retry_s": time_until_retry},
113
+ )
114
+ self.reason = reason
115
+ self.time_until_retry = time_until_retry
116
+
117
+
118
+ class ProviderNotReadyError(ProviderError):
119
+ """Raised when an operation requires READY state but provider is not ready."""
120
+
121
+ def __init__(self, provider_id: str, current_state: str):
122
+ super().__init__(
123
+ message=f"Provider is not ready (state={current_state})",
124
+ provider_id=provider_id,
125
+ operation="invoke",
126
+ details={"current_state": current_state},
127
+ )
128
+ self.current_state = current_state
129
+
130
+
131
+ class InvalidStateTransitionError(ProviderError):
132
+ """Raised when an invalid state transition is attempted."""
133
+
134
+ def __init__(self, provider_id: str, from_state: str, to_state: str):
135
+ super().__init__(
136
+ message=f"Invalid state transition: {from_state} -> {to_state}",
137
+ provider_id=provider_id,
138
+ operation="transition",
139
+ details={"from_state": from_state, "to_state": to_state},
140
+ )
141
+ self.from_state = from_state
142
+ self.to_state = to_state
143
+
144
+
145
+ # --- Tool Invocation Exceptions ---
146
+
147
+
148
+ class ToolError(MCPError):
149
+ """Base exception for tool-related errors."""
150
+
151
+ pass
152
+
153
+
154
+ class ToolNotFoundError(ToolError):
155
+ """Raised when a tool is not found in the provider's catalog."""
156
+
157
+ def __init__(self, provider_id: str, tool_name: str):
158
+ super().__init__(
159
+ message=f"Tool not found: {tool_name}",
160
+ provider_id=provider_id,
161
+ operation="invoke",
162
+ details={"tool_name": tool_name},
163
+ )
164
+ self.tool_name = tool_name
165
+
166
+
167
+ class ToolInvocationError(ToolError):
168
+ """Raised when a tool invocation fails."""
169
+
170
+ def __init__(self, provider_id: str, message: str, details: Optional[Dict[str, Any]] = None):
171
+ super().__init__(
172
+ message=message,
173
+ provider_id=provider_id,
174
+ operation="invoke",
175
+ details=details or {},
176
+ )
177
+
178
+
179
+ class ToolTimeoutError(ToolError):
180
+ """Raised when a tool invocation times out."""
181
+
182
+ def __init__(self, provider_id: str, tool_name: str, timeout: float):
183
+ super().__init__(
184
+ message=f"Tool invocation timed out after {timeout}s",
185
+ provider_id=provider_id,
186
+ operation="invoke",
187
+ details={"tool_name": tool_name, "timeout_s": timeout},
188
+ )
189
+ self.tool_name = tool_name
190
+ self.timeout = timeout
191
+
192
+
193
+ # --- Client/Communication Exceptions ---
194
+
195
+
196
+ class ClientError(MCPError):
197
+ """Raised when the stdio client encounters an error."""
198
+
199
+ def __init__(
200
+ self,
201
+ message: str,
202
+ provider_id: str = "",
203
+ details: Optional[Dict[str, Any]] = None,
204
+ ):
205
+ super().__init__(
206
+ message=message,
207
+ provider_id=provider_id,
208
+ operation="client",
209
+ details=details or {},
210
+ )
211
+
212
+
213
+ class ClientNotConnectedError(ClientError):
214
+ """Raised when attempting to use a client that is not connected."""
215
+
216
+ def __init__(self, provider_id: str = ""):
217
+ super().__init__(message="Client is not connected", provider_id=provider_id)
218
+
219
+
220
+ class ClientTimeoutError(ClientError):
221
+ """Raised when a client operation times out."""
222
+
223
+ def __init__(self, provider_id: str = "", timeout: float = 0, operation: str = "call"):
224
+ super().__init__(
225
+ message=f"Client operation timed out after {timeout}s",
226
+ provider_id=provider_id,
227
+ details={"timeout_s": timeout, "operation": operation},
228
+ )
229
+ self.timeout = timeout
230
+
231
+
232
+ # --- Validation Exceptions ---
233
+
234
+
235
+ class ValidationError(MCPError):
236
+ """Raised when input validation fails."""
237
+
238
+ def __init__(
239
+ self,
240
+ message: str,
241
+ field: str = "",
242
+ value: Any = None,
243
+ details: Optional[Dict[str, Any]] = None,
244
+ ):
245
+ base_details = {"field": field}
246
+ if value is not None:
247
+ # Sanitize value for logging (truncate if too long)
248
+ str_value = str(value)
249
+ if len(str_value) > 100:
250
+ str_value = str_value[:100] + "..."
251
+ base_details["value"] = str_value
252
+ if details:
253
+ base_details.update(details)
254
+
255
+ super().__init__(message=message, operation="validation", details=base_details)
256
+ self.field = field
257
+ self.value = value
258
+
259
+
260
+ class ConfigurationError(MCPError):
261
+ """Raised when configuration is invalid."""
262
+
263
+ def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
264
+ super().__init__(message=message, operation="configuration", details=details or {})
265
+
266
+
267
+ # --- Rate Limiting Exceptions ---
268
+
269
+
270
+ class RateLimitExceeded(MCPError):
271
+ """Raised when rate limit is exceeded."""
272
+
273
+ def __init__(self, provider_id: str = "", limit: int = 0, window_seconds: int = 0):
274
+ super().__init__(
275
+ message=f"Rate limit exceeded: {limit} requests per {window_seconds}s",
276
+ provider_id=provider_id,
277
+ operation="rate_limit",
278
+ details={"limit": limit, "window_seconds": window_seconds},
279
+ )
280
+ self.limit = limit
281
+ self.window_seconds = window_seconds
282
+
283
+
284
+ # --- Authentication Exceptions ---
285
+
286
+
287
+ class AuthenticationError(MCPError):
288
+ """Base class for authentication errors.
289
+
290
+ All authentication-related failures inherit from this class,
291
+ enabling unified handling of auth errors.
292
+ """
293
+
294
+ def __init__(
295
+ self,
296
+ message: str,
297
+ auth_method: str = "",
298
+ details: Optional[Dict[str, Any]] = None,
299
+ ):
300
+ super().__init__(
301
+ message=message,
302
+ operation="authentication",
303
+ details={"auth_method": auth_method, **(details or {})},
304
+ )
305
+ self.auth_method = auth_method
306
+
307
+
308
+ class InvalidCredentialsError(AuthenticationError):
309
+ """Credentials are invalid or malformed.
310
+
311
+ Raised when:
312
+ - API key format is invalid
313
+ - JWT signature verification fails
314
+ - Token is malformed
315
+ - Unknown API key
316
+ """
317
+
318
+ def __init__(
319
+ self,
320
+ message: str = "Invalid credentials",
321
+ auth_method: str = "",
322
+ details: Optional[Dict[str, Any]] = None,
323
+ ):
324
+ super().__init__(
325
+ message=message,
326
+ auth_method=auth_method,
327
+ details=details,
328
+ )
329
+
330
+
331
+ class ExpiredCredentialsError(AuthenticationError):
332
+ """Credentials have expired.
333
+
334
+ Raised when:
335
+ - JWT exp claim is in the past
336
+ - API key has passed its expiration date
337
+ """
338
+
339
+ def __init__(
340
+ self,
341
+ message: str = "Credentials have expired",
342
+ auth_method: str = "",
343
+ expired_at: Optional[float] = None,
344
+ ):
345
+ super().__init__(
346
+ message=message,
347
+ auth_method=auth_method,
348
+ details={"expired_at": expired_at} if expired_at else None,
349
+ )
350
+ self.expired_at = expired_at
351
+
352
+
353
+ class RevokedCredentialsError(AuthenticationError):
354
+ """Credentials have been revoked.
355
+
356
+ Raised when:
357
+ - API key has been explicitly revoked
358
+ - JWT is on a revocation list
359
+ """
360
+
361
+ def __init__(
362
+ self,
363
+ message: str = "Credentials have been revoked",
364
+ auth_method: str = "",
365
+ revoked_at: Optional[float] = None,
366
+ ):
367
+ super().__init__(
368
+ message=message,
369
+ auth_method=auth_method,
370
+ details={"revoked_at": revoked_at} if revoked_at else None,
371
+ )
372
+ self.revoked_at = revoked_at
373
+
374
+
375
+ class MissingCredentialsError(AuthenticationError):
376
+ """No credentials provided when authentication is required.
377
+
378
+ Raised when:
379
+ - No Authorization header present
380
+ - No API key header present
381
+ - Authentication is required but allow_anonymous is False
382
+ """
383
+
384
+ def __init__(
385
+ self,
386
+ message: str = "No credentials provided",
387
+ expected_methods: Optional[list[str]] = None,
388
+ ):
389
+ super().__init__(
390
+ message=message,
391
+ auth_method="none",
392
+ details={"expected_methods": expected_methods} if expected_methods else None,
393
+ )
394
+ self.expected_methods = expected_methods or []
395
+
396
+
397
+ class RateLimitExceededError(AuthenticationError):
398
+ """Rate limit exceeded for authentication attempts.
399
+
400
+ Raised when:
401
+ - Too many failed authentication attempts from an IP
402
+ - IP is temporarily locked out
403
+ """
404
+
405
+ def __init__(
406
+ self,
407
+ message: str = "Rate limit exceeded",
408
+ retry_after: Optional[float] = None,
409
+ ):
410
+ super().__init__(
411
+ message=message,
412
+ auth_method="rate_limit",
413
+ details={"retry_after": retry_after} if retry_after else None,
414
+ )
415
+ self.retry_after = retry_after
416
+
417
+
418
+ # --- Authorization Exceptions ---
419
+
420
+
421
+ class AuthorizationError(MCPError):
422
+ """Base class for authorization errors.
423
+
424
+ All authorization-related failures inherit from this class.
425
+ """
426
+
427
+ def __init__(
428
+ self,
429
+ message: str,
430
+ principal_id: str = "",
431
+ action: str = "",
432
+ resource: str = "",
433
+ details: Optional[Dict[str, Any]] = None,
434
+ ):
435
+ super().__init__(
436
+ message=message,
437
+ operation="authorization",
438
+ details={
439
+ "principal_id": principal_id,
440
+ "action": action,
441
+ "resource": resource,
442
+ **(details or {}),
443
+ },
444
+ )
445
+ self.principal_id = principal_id
446
+ self.action = action
447
+ self.resource = resource
448
+
449
+
450
+ class AccessDeniedError(AuthorizationError):
451
+ """Principal does not have permission for the requested action.
452
+
453
+ The most common authorization error - principal is authenticated
454
+ but lacks the necessary permissions.
455
+ """
456
+
457
+ def __init__(
458
+ self,
459
+ principal_id: str,
460
+ action: str,
461
+ resource: str,
462
+ reason: str = "",
463
+ ):
464
+ message = f"Access denied: {principal_id} cannot {action} on {resource}"
465
+ if reason:
466
+ message = f"{message} ({reason})"
467
+ super().__init__(
468
+ message=message,
469
+ principal_id=principal_id,
470
+ action=action,
471
+ resource=resource,
472
+ details={"reason": reason} if reason else None,
473
+ )
474
+ self.reason = reason
475
+
476
+
477
+ class InsufficientScopeError(AuthorizationError):
478
+ """Token does not have required scope.
479
+
480
+ Raised when JWT token scopes don't include the required scope
481
+ for the requested operation.
482
+ """
483
+
484
+ def __init__(
485
+ self,
486
+ principal_id: str,
487
+ required_scope: str,
488
+ available_scopes: list[str] | None = None,
489
+ ):
490
+ super().__init__(
491
+ message=f"Insufficient scope: required '{required_scope}'",
492
+ principal_id=principal_id,
493
+ action="scope_check",
494
+ resource=required_scope,
495
+ details={"available_scopes": available_scopes} if available_scopes else None,
496
+ )
497
+ self.required_scope = required_scope
498
+ self.available_scopes = available_scopes or []
499
+
500
+
501
+ class TenantAccessDeniedError(AuthorizationError):
502
+ """Principal cannot access resources in the specified tenant.
503
+
504
+ Raised when a principal attempts to access resources in a tenant
505
+ they don't belong to.
506
+ """
507
+
508
+ def __init__(
509
+ self,
510
+ principal_id: str,
511
+ principal_tenant: str | None,
512
+ resource_tenant: str,
513
+ ):
514
+ super().__init__(
515
+ message=f"Access denied: cannot access tenant '{resource_tenant}'",
516
+ principal_id=principal_id,
517
+ action="tenant_access",
518
+ resource=resource_tenant,
519
+ details={
520
+ "principal_tenant": principal_tenant,
521
+ "resource_tenant": resource_tenant,
522
+ },
523
+ )
524
+ self.principal_tenant = principal_tenant
525
+ self.resource_tenant = resource_tenant
@@ -0,0 +1,70 @@
1
+ """Domain model - Aggregates and entities."""
2
+
3
+ # Re-export ProviderState from value_objects for convenience
4
+ from ..value_objects import GroupState, LoadBalancerStrategy, MemberPriority, MemberWeight, ProviderState
5
+ from .aggregate import AggregateRoot
6
+ from .circuit_breaker import CircuitBreaker, CircuitBreakerConfig, CircuitState
7
+ from .event_sourced_provider import EventSourcedProvider, ProviderSnapshot
8
+ from .health_tracker import HealthTracker
9
+ from .load_balancer import (
10
+ BaseStrategy,
11
+ LeastConnectionsStrategy,
12
+ LoadBalancer,
13
+ PriorityStrategy,
14
+ RandomStrategy,
15
+ RoundRobinStrategy,
16
+ WeightedRoundRobinStrategy,
17
+ )
18
+ from .provider import Provider
19
+ from .provider_group import (
20
+ GroupCircuitClosed,
21
+ GroupCircuitOpened,
22
+ GroupCreated,
23
+ GroupMember,
24
+ GroupMemberAdded,
25
+ GroupMemberHealthChanged,
26
+ GroupMemberRemoved,
27
+ GroupStateChanged,
28
+ ProviderGroup,
29
+ )
30
+ from .tool_catalog import ToolCatalog, ToolSchema
31
+
32
+ __all__ = [
33
+ # Base
34
+ "AggregateRoot",
35
+ # Circuit Breaker
36
+ "CircuitBreaker",
37
+ "CircuitBreakerConfig",
38
+ "CircuitState",
39
+ # Provider
40
+ "HealthTracker",
41
+ "ToolCatalog",
42
+ "ToolSchema",
43
+ "Provider",
44
+ "ProviderState",
45
+ "EventSourcedProvider",
46
+ "ProviderSnapshot",
47
+ # Provider Group
48
+ "ProviderGroup",
49
+ "GroupMember",
50
+ "GroupState",
51
+ "LoadBalancerStrategy",
52
+ "MemberWeight",
53
+ "MemberPriority",
54
+ # Load Balancer
55
+ "LoadBalancer",
56
+ "BaseStrategy",
57
+ "RoundRobinStrategy",
58
+ "WeightedRoundRobinStrategy",
59
+ "LeastConnectionsStrategy",
60
+ "RandomStrategy",
61
+ "PriorityStrategy",
62
+ # Group Events
63
+ "GroupCreated",
64
+ "GroupMemberAdded",
65
+ "GroupMemberRemoved",
66
+ "GroupMemberHealthChanged",
67
+ "GroupStateChanged",
68
+ "GroupCircuitOpened",
69
+ "GroupCircuitClosed",
70
+ ]
@@ -0,0 +1,58 @@
1
+ """Base class for aggregate roots in the domain model."""
2
+
3
+ from abc import ABC
4
+ from typing import List
5
+
6
+ from ..events import DomainEvent
7
+
8
+
9
+ class AggregateRoot(ABC):
10
+ """
11
+ Base class for aggregate roots.
12
+
13
+ Aggregate roots are the entry points to aggregates and ensure consistency
14
+ within their boundaries. They collect domain events that can be published
15
+ after the aggregate is persisted.
16
+ """
17
+
18
+ def __init__(self):
19
+ self._uncommitted_events: List[DomainEvent] = []
20
+ self._version: int = 0
21
+
22
+ def _record_event(self, event: DomainEvent) -> None:
23
+ """
24
+ Record a domain event to be published after persistence.
25
+
26
+ Events are collected and published as a batch to ensure consistency.
27
+ The event bus should handle failures gracefully to avoid breaking
28
+ the aggregate's core functionality.
29
+ """
30
+ self._uncommitted_events.append(event)
31
+
32
+ def collect_events(self) -> List[DomainEvent]:
33
+ """
34
+ Collect and clear pending domain events.
35
+
36
+ This should be called after the aggregate is persisted to publish
37
+ events to the event bus. Returns a copy and clears internal list.
38
+ """
39
+ events = list(self._uncommitted_events)
40
+ self._uncommitted_events.clear()
41
+ return events
42
+
43
+ def has_uncommitted_events(self) -> bool:
44
+ """Check if there are uncommitted events."""
45
+ return len(self._uncommitted_events) > 0
46
+
47
+ @property
48
+ def version(self) -> int:
49
+ """
50
+ Get the aggregate version for optimistic concurrency control.
51
+
52
+ Version is incremented after each state change.
53
+ """
54
+ return self._version
55
+
56
+ def _increment_version(self) -> None:
57
+ """Increment version after state change."""
58
+ self._version += 1