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,243 @@
1
+ """OPA (Open Policy Agent) authorization implementation.
2
+
3
+ Provides integration with OPA for complex policy-based authorization.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, TYPE_CHECKING
9
+
10
+ import structlog
11
+
12
+ from ...domain.contracts.authorization import AuthorizationRequest, AuthorizationResult, IPolicyEngine
13
+
14
+ if TYPE_CHECKING:
15
+ from .rbac_authorizer import RBACAuthorizer
16
+
17
+ logger = structlog.get_logger(__name__)
18
+
19
+
20
+ class OPAAuthorizer(IPolicyEngine):
21
+ """Open Policy Agent integration for complex authorization policies.
22
+
23
+ Evaluates authorization requests against OPA policies via HTTP API.
24
+ Fails closed (denies access) on any errors to ensure security.
25
+
26
+ Example OPA policy (policies/mcp_authz.rego):
27
+
28
+ package mcp.authz
29
+
30
+ default allow := false
31
+
32
+ # Admins can do anything
33
+ allow if {
34
+ "admin" in input.principal.groups
35
+ }
36
+
37
+ # Developers can invoke tools
38
+ allow if {
39
+ "developer" in input.principal.groups
40
+ input.resource.type == "tool"
41
+ input.action == "invoke"
42
+ }
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ opa_url: str,
48
+ policy_path: str = "v1/data/mcp/authz/allow",
49
+ timeout: float = 5.0,
50
+ ):
51
+ """Initialize the OPA authorizer.
52
+
53
+ Args:
54
+ opa_url: Base URL of the OPA server (e.g., http://localhost:8181).
55
+ policy_path: Path to the policy decision endpoint.
56
+ timeout: HTTP request timeout in seconds.
57
+ """
58
+ self._opa_url = opa_url.rstrip("/")
59
+ self._policy_path = policy_path.lstrip("/")
60
+ self._timeout = timeout
61
+ self._client = None # Lazy initialization
62
+
63
+ def evaluate(self, input_data: dict[str, Any]) -> AuthorizationResult:
64
+ """Evaluate OPA policy with given input.
65
+
66
+ Args:
67
+ input_data: Policy input including principal, action, resource, context.
68
+
69
+ Returns:
70
+ AuthorizationResult from policy evaluation.
71
+ Returns deny on any errors (fail closed).
72
+ """
73
+ try:
74
+ import httpx
75
+ except ImportError:
76
+ logger.error("opa_httpx_not_installed")
77
+ return AuthorizationResult.deny(reason="opa_error:httpx_not_installed")
78
+
79
+ url = f"{self._opa_url}/{self._policy_path}"
80
+
81
+ try:
82
+ # Lazy initialize client
83
+ if self._client is None:
84
+ self._client = httpx.Client(timeout=self._timeout)
85
+
86
+ response = self._client.post(url, json={"input": input_data})
87
+ response.raise_for_status()
88
+
89
+ result = response.json()
90
+ allowed = result.get("result", False)
91
+
92
+ logger.debug(
93
+ "opa_evaluation_complete",
94
+ url=url,
95
+ allowed=allowed,
96
+ principal_id=input_data.get("principal", {}).get("id"),
97
+ )
98
+
99
+ if allowed:
100
+ return AuthorizationResult.allow(reason="opa_policy")
101
+ return AuthorizationResult.deny(reason="opa_denied")
102
+
103
+ except httpx.ConnectError as e:
104
+ logger.error("opa_connection_failed", url=url, error=str(e))
105
+ return AuthorizationResult.deny(reason="opa_error:connection_failed")
106
+
107
+ except httpx.TimeoutException as e:
108
+ logger.error("opa_timeout", url=url, error=str(e))
109
+ return AuthorizationResult.deny(reason="opa_error:timeout")
110
+
111
+ except httpx.HTTPStatusError as e:
112
+ logger.error(
113
+ "opa_http_error",
114
+ url=url,
115
+ status_code=e.response.status_code,
116
+ error=str(e),
117
+ )
118
+ return AuthorizationResult.deny(reason=f"opa_error:http_{e.response.status_code}")
119
+
120
+ except Exception as e:
121
+ logger.error("opa_evaluation_failed", url=url, error=str(e))
122
+ return AuthorizationResult.deny(reason=f"opa_error:{type(e).__name__}")
123
+
124
+ @staticmethod
125
+ def build_input(request: AuthorizationRequest) -> dict[str, Any]:
126
+ """Build OPA input from authorization request.
127
+
128
+ Args:
129
+ request: The authorization request.
130
+
131
+ Returns:
132
+ Dictionary formatted for OPA policy input.
133
+ """
134
+ return {
135
+ "principal": {
136
+ "id": request.principal.id.value,
137
+ "type": request.principal.type.value,
138
+ "tenant_id": request.principal.tenant_id,
139
+ "groups": list(request.principal.groups),
140
+ "metadata": request.principal.metadata or {},
141
+ },
142
+ "action": request.action,
143
+ "resource": {
144
+ "type": request.resource_type,
145
+ "id": request.resource_id,
146
+ },
147
+ "context": request.context or {},
148
+ }
149
+
150
+ def authorize(self, request: AuthorizationRequest) -> AuthorizationResult:
151
+ """Authorize using OPA policy.
152
+
153
+ Convenience method that builds input and evaluates policy.
154
+
155
+ Args:
156
+ request: The authorization request.
157
+
158
+ Returns:
159
+ AuthorizationResult from policy evaluation.
160
+ """
161
+ input_data = self.build_input(request)
162
+ return self.evaluate(input_data)
163
+
164
+ def close(self) -> None:
165
+ """Close the HTTP client."""
166
+ if self._client is not None:
167
+ self._client.close()
168
+ self._client = None
169
+
170
+ def __enter__(self) -> "OPAAuthorizer":
171
+ return self
172
+
173
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
174
+ self.close()
175
+
176
+
177
+ class CombinedAuthorizer:
178
+ """Combines RBAC with optional OPA for hybrid authorization.
179
+
180
+ Checks RBAC first for performance, then OPA for additional policies.
181
+ Useful when you want fast RBAC for most cases with OPA for complex rules.
182
+ """
183
+
184
+ def __init__(
185
+ self,
186
+ rbac_authorizer: "RBACAuthorizer",
187
+ opa_authorizer: OPAAuthorizer | None = None,
188
+ require_both: bool = False,
189
+ ):
190
+ """Initialize combined authorizer.
191
+
192
+ Args:
193
+ rbac_authorizer: Primary RBAC authorizer.
194
+ opa_authorizer: Optional OPA authorizer for additional checks.
195
+ require_both: If True, both RBAC and OPA must allow.
196
+ If False, RBAC is checked first and OPA only if RBAC denies.
197
+ """
198
+
199
+ self._rbac = rbac_authorizer
200
+ self._opa = opa_authorizer
201
+ self._require_both = require_both
202
+
203
+ def authorize(self, request: AuthorizationRequest) -> AuthorizationResult:
204
+ """Check authorization with combined strategy.
205
+
206
+ Args:
207
+ request: The authorization request.
208
+
209
+ Returns:
210
+ AuthorizationResult based on combined evaluation.
211
+ """
212
+ rbac_result = self._rbac.authorize(request)
213
+
214
+ if self._opa is None:
215
+ return rbac_result
216
+
217
+ if self._require_both:
218
+ # Both must allow
219
+ if not rbac_result.allowed:
220
+ return rbac_result
221
+
222
+ opa_result = self._opa.authorize(request)
223
+ if not opa_result.allowed:
224
+ return AuthorizationResult.deny(reason=f"rbac_allowed_but_{opa_result.reason}")
225
+
226
+ return AuthorizationResult.allow(
227
+ reason=f"rbac_and_opa_allowed:{rbac_result.matched_role}",
228
+ permission=rbac_result.matched_permission,
229
+ role=rbac_result.matched_role,
230
+ )
231
+ else:
232
+ # RBAC first, OPA as fallback
233
+ if rbac_result.allowed:
234
+ return rbac_result
235
+
236
+ # RBAC denied, check OPA
237
+ opa_result = self._opa.authorize(request)
238
+ if opa_result.allowed:
239
+ return AuthorizationResult.allow(
240
+ reason="opa_override",
241
+ )
242
+
243
+ return rbac_result # Return original RBAC denial