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,105 @@
1
+ """Tool catalog value object for providers."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Dict, List, Optional
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class ToolSchema:
9
+ """
10
+ Schema for a tool provided by a provider.
11
+
12
+ Immutable value object containing tool metadata.
13
+ """
14
+
15
+ name: str
16
+ description: str
17
+ input_schema: Dict[str, Any]
18
+ output_schema: Optional[Dict[str, Any]] = None
19
+
20
+ def to_dict(self) -> dict:
21
+ """Convert to dictionary representation."""
22
+ result = {
23
+ "name": self.name,
24
+ "description": self.description,
25
+ "inputSchema": self.input_schema,
26
+ }
27
+ if self.output_schema is not None:
28
+ result["outputSchema"] = self.output_schema
29
+ return result
30
+
31
+
32
+ class ToolCatalog:
33
+ """
34
+ Catalog of tools provided by a provider.
35
+
36
+ This is a mutable collection that can be updated when tools are
37
+ discovered or refreshed. Thread safety is handled by the aggregate.
38
+ """
39
+
40
+ def __init__(self, tools: Optional[Dict[str, ToolSchema]] = None):
41
+ self._tools: Dict[str, ToolSchema] = dict(tools or {})
42
+
43
+ def has(self, tool_name: str) -> bool:
44
+ """Check if a tool exists in the catalog."""
45
+ return tool_name in self._tools
46
+
47
+ def get(self, tool_name: str) -> Optional[ToolSchema]:
48
+ """Get a tool schema by name."""
49
+ return self._tools.get(tool_name)
50
+
51
+ def list_names(self) -> List[str]:
52
+ """Get list of all tool names."""
53
+ return list(self._tools.keys())
54
+
55
+ def list_tools(self) -> List[ToolSchema]:
56
+ """Get list of all tool schemas."""
57
+ return list(self._tools.values())
58
+
59
+ def count(self) -> int:
60
+ """Get number of tools in catalog."""
61
+ return len(self._tools)
62
+
63
+ def add(self, tool: ToolSchema) -> None:
64
+ """Add or update a tool in the catalog."""
65
+ self._tools[tool.name] = tool
66
+
67
+ def remove(self, tool_name: str) -> bool:
68
+ """Remove a tool from the catalog. Returns True if removed."""
69
+ if tool_name in self._tools:
70
+ del self._tools[tool_name]
71
+ return True
72
+ return False
73
+
74
+ def clear(self) -> None:
75
+ """Remove all tools from the catalog."""
76
+ self._tools.clear()
77
+
78
+ def update_from_list(self, tool_list: List[dict]) -> None:
79
+ """
80
+ Update catalog from a list of tool dictionaries.
81
+
82
+ This is typically used when refreshing tools from a provider response.
83
+ """
84
+ self._tools.clear()
85
+ for t in tool_list:
86
+ tool = ToolSchema(
87
+ name=t["name"],
88
+ description=t.get("description", ""),
89
+ input_schema=t.get("inputSchema", {}),
90
+ output_schema=t.get("outputSchema"),
91
+ )
92
+ self._tools[tool.name] = tool
93
+
94
+ def to_dict(self) -> Dict[str, ToolSchema]:
95
+ """Get a copy of the internal tools dictionary."""
96
+ return dict(self._tools)
97
+
98
+ def __contains__(self, tool_name: str) -> bool:
99
+ return tool_name in self._tools
100
+
101
+ def __len__(self) -> int:
102
+ return len(self._tools)
103
+
104
+ def __iter__(self):
105
+ return iter(self._tools.values())
@@ -0,0 +1,19 @@
1
+ """Domain policies for MCP Hangar.
2
+
3
+ Policies encapsulate domain rules and classification logic that can be
4
+ applied across different contexts without coupling to specific aggregates.
5
+ """
6
+
7
+ from .provider_health import (
8
+ classify_provider_health,
9
+ classify_provider_health_from_provider,
10
+ ProviderHealthClassification,
11
+ to_health_status_string,
12
+ )
13
+
14
+ __all__ = [
15
+ "ProviderHealthClassification",
16
+ "classify_provider_health",
17
+ "classify_provider_health_from_provider",
18
+ "to_health_status_string",
19
+ ]
@@ -0,0 +1,187 @@
1
+ """Provider health classification policy.
2
+
3
+ This module centralizes the logic that maps a Provider's state + health tracker
4
+ signals into a user-facing health classification.
5
+
6
+ Why this exists:
7
+ - Avoids duplicating "health status" mapping logic across query handlers / APIs.
8
+ - Keeps interpretation of state and failures as a domain-level policy.
9
+ - Allows the policy to evolve without touching CQRS read mapping.
10
+
11
+ This policy is intentionally small and pure (no I/O, no imports from infrastructure).
12
+
13
+ Usage (typical):
14
+ from mcp_hangar.domain.policies.provider_health import classify_provider_health
15
+
16
+ health_status = classify_provider_health(
17
+ state=provider.state,
18
+ consecutive_failures=provider.health.consecutive_failures,
19
+ )
20
+
21
+ Or, if you already have a HealthTracker-like object:
22
+ health_status = classify_provider_health_from_provider(provider)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+ from typing import Any, Protocol
29
+
30
+ from ..value_objects import HealthStatus, ProviderState
31
+
32
+
33
+ class _HealthView(Protocol):
34
+ """Minimal health-tracker view required by the policy.
35
+
36
+ Defines the interface for accessing health metrics from any
37
+ health-tracker-like object.
38
+ """
39
+
40
+ @property
41
+ def consecutive_failures(self) -> int:
42
+ """Get the count of consecutive failures."""
43
+ ...
44
+
45
+
46
+ class _ProviderView(Protocol):
47
+ """Minimal provider view required by the policy.
48
+
49
+ Defines the interface for accessing provider state and health
50
+ from any provider-like object.
51
+ """
52
+
53
+ @property
54
+ def state(self) -> ProviderState:
55
+ """Get the current provider state."""
56
+ ...
57
+
58
+ @property
59
+ def health(self) -> _HealthView:
60
+ """Get the health tracker view."""
61
+ ...
62
+
63
+
64
+ def _normalize_state(state: Any) -> ProviderState:
65
+ """Convert a loose/legacy state representation to ProviderState."""
66
+ if isinstance(state, ProviderState):
67
+ return state
68
+
69
+ # Some call sites may pass enum-like objects with `.value`
70
+ value = getattr(state, "value", None)
71
+ if value is not None:
72
+ state_str = str(value).lower()
73
+ else:
74
+ state_str = str(state).lower()
75
+
76
+ for s in ProviderState:
77
+ if s.value == state_str:
78
+ return s
79
+
80
+ # If unknown, treat as DEAD from a health classification standpoint
81
+ # (conservative default).
82
+ return ProviderState.DEAD
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class ProviderHealthClassification:
87
+ """Result of applying the classification policy."""
88
+
89
+ status: HealthStatus
90
+ reason: str
91
+ consecutive_failures: int
92
+
93
+ def to_dict(self) -> dict:
94
+ return {
95
+ "status": str(self.status),
96
+ "reason": self.reason,
97
+ "consecutive_failures": self.consecutive_failures,
98
+ }
99
+
100
+
101
+ def classify_provider_health(
102
+ *,
103
+ state: Any,
104
+ consecutive_failures: int = 0,
105
+ ) -> ProviderHealthClassification:
106
+ """Classify provider health from state and failure count.
107
+
108
+ Rules (current):
109
+ - READY + 0 failures -> HEALTHY
110
+ - READY + >0 failures -> DEGRADED
111
+ - DEGRADED -> DEGRADED
112
+ - DEAD -> UNHEALTHY
113
+ - COLD / INITIALIZING -> UNKNOWN
114
+
115
+ Notes:
116
+ - This is a *classification*, not the same as "can accept requests".
117
+ That rule is handled by ProviderState.can_accept_requests and other domain logic.
118
+ """
119
+ st = _normalize_state(state)
120
+ failures = int(consecutive_failures or 0)
121
+
122
+ if st == ProviderState.READY:
123
+ if failures <= 0:
124
+ return ProviderHealthClassification(
125
+ status=HealthStatus.HEALTHY,
126
+ reason="ready_no_failures",
127
+ consecutive_failures=failures,
128
+ )
129
+ return ProviderHealthClassification(
130
+ status=HealthStatus.DEGRADED,
131
+ reason="ready_with_failures",
132
+ consecutive_failures=failures,
133
+ )
134
+
135
+ if st == ProviderState.DEGRADED:
136
+ return ProviderHealthClassification(
137
+ status=HealthStatus.DEGRADED,
138
+ reason="provider_state_degraded",
139
+ consecutive_failures=failures,
140
+ )
141
+
142
+ if st == ProviderState.DEAD:
143
+ return ProviderHealthClassification(
144
+ status=HealthStatus.UNHEALTHY,
145
+ reason="provider_state_dead",
146
+ consecutive_failures=failures,
147
+ )
148
+
149
+ if st in (ProviderState.COLD, ProviderState.INITIALIZING):
150
+ return ProviderHealthClassification(
151
+ status=HealthStatus.UNKNOWN,
152
+ reason=f"provider_state_{st.value}",
153
+ consecutive_failures=failures,
154
+ )
155
+
156
+ # Fallback (shouldn't happen due to normalization)
157
+ return ProviderHealthClassification(
158
+ status=HealthStatus.UNKNOWN,
159
+ reason="unknown_state",
160
+ consecutive_failures=failures,
161
+ )
162
+
163
+
164
+ def classify_provider_health_from_provider(
165
+ provider: _ProviderView,
166
+ ) -> ProviderHealthClassification:
167
+ """Convenience wrapper to classify health from a provider-like object."""
168
+ return classify_provider_health(
169
+ state=provider.state,
170
+ consecutive_failures=provider.health.consecutive_failures,
171
+ )
172
+
173
+
174
+ def to_health_status_string(
175
+ *,
176
+ state: Any,
177
+ consecutive_failures: int = 0,
178
+ ) -> str:
179
+ """Legacy helper: return the `HealthStatus.value` string.
180
+
181
+ This exists to minimize changes in read model mapping code while still routing
182
+ logic through a single policy.
183
+ """
184
+ return classify_provider_health(
185
+ state=state,
186
+ consecutive_failures=consecutive_failures,
187
+ ).status.value
@@ -0,0 +1,249 @@
1
+ """
2
+ Repository interfaces for provider storage abstraction.
3
+
4
+ The Repository pattern separates domain logic from data access logic,
5
+ allowing the persistence mechanism to change without affecting business code.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ import threading
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ # Type alias for provider-like objects (Provider aggregate)
13
+ ProviderLike = Any
14
+
15
+
16
+ class IProviderRepository(ABC):
17
+ """Abstract interface for provider storage.
18
+
19
+ This interface defines the contract for storing and retrieving providers,
20
+ allowing different implementations (in-memory, database, etc.) without
21
+ changing business logic.
22
+
23
+ Stores Provider aggregates.
24
+
25
+ Thread-safety is guaranteed by implementations.
26
+ """
27
+
28
+ @abstractmethod
29
+ def add(self, provider_id: str, provider: ProviderLike) -> None:
30
+ """Add or update a provider in the repository.
31
+
32
+ Args:
33
+ provider_id: Unique provider identifier
34
+ provider: Provider aggregate instance to store
35
+
36
+ Raises:
37
+ ValueError: If provider_id is empty or invalid
38
+ """
39
+ pass
40
+
41
+ @abstractmethod
42
+ def get(self, provider_id: str) -> Optional[ProviderLike]:
43
+ """Retrieve a provider by ID.
44
+
45
+ Args:
46
+ provider_id: Provider identifier to look up
47
+
48
+ Returns:
49
+ Provider if found, None otherwise
50
+ """
51
+ pass
52
+
53
+ @abstractmethod
54
+ def exists(self, provider_id: str) -> bool:
55
+ """Check if a provider exists in the repository.
56
+
57
+ Args:
58
+ provider_id: Provider identifier to check
59
+
60
+ Returns:
61
+ True if provider exists, False otherwise
62
+ """
63
+ pass
64
+
65
+ @abstractmethod
66
+ def remove(self, provider_id: str) -> bool:
67
+ """Remove a provider from the repository.
68
+
69
+ Args:
70
+ provider_id: Provider identifier to remove
71
+
72
+ Returns:
73
+ True if provider was removed, False if not found
74
+ """
75
+ pass
76
+
77
+ @abstractmethod
78
+ def get_all(self) -> Dict[str, ProviderLike]:
79
+ """Get all providers as a dictionary.
80
+
81
+ Returns:
82
+ Dictionary mapping provider_id -> Provider
83
+ Returns a copy to prevent external modifications
84
+ """
85
+ pass
86
+
87
+ @abstractmethod
88
+ def get_all_ids(self) -> List[str]:
89
+ """Get all provider IDs.
90
+
91
+ Returns:
92
+ List of provider identifiers
93
+ """
94
+ pass
95
+
96
+ @abstractmethod
97
+ def count(self) -> int:
98
+ """Get the total number of providers.
99
+
100
+ Returns:
101
+ Number of providers in the repository
102
+ """
103
+ pass
104
+
105
+ @abstractmethod
106
+ def clear(self) -> None:
107
+ """Remove all providers from the repository.
108
+
109
+ This is primarily for testing and cleanup.
110
+ """
111
+ pass
112
+
113
+
114
+ class InMemoryProviderRepository(IProviderRepository):
115
+ """In-memory implementation of provider repository.
116
+
117
+ This implementation stores providers in a dictionary with thread-safe
118
+ access using a read-write lock pattern.
119
+
120
+ Stores Provider aggregates.
121
+
122
+ Thread Safety:
123
+ - All operations are protected by a lock
124
+ - get_all() returns a snapshot copy
125
+ - Safe for concurrent access from multiple threads
126
+ """
127
+
128
+ def __init__(self):
129
+ """Initialize empty in-memory repository."""
130
+ self._providers: Dict[str, ProviderLike] = {}
131
+ self._lock = threading.RLock()
132
+
133
+ def add(self, provider_id: str, provider: ProviderLike) -> None:
134
+ """Add or update a provider in the repository.
135
+
136
+ Args:
137
+ provider_id: Unique provider identifier
138
+ provider: Provider aggregate instance to store
139
+
140
+ Raises:
141
+ ValueError: If provider_id is empty
142
+ """
143
+ if not provider_id:
144
+ raise ValueError("Provider ID cannot be empty")
145
+
146
+ with self._lock:
147
+ self._providers[provider_id] = provider
148
+
149
+ def get(self, provider_id: str) -> Optional[ProviderLike]:
150
+ """Retrieve a provider by ID.
151
+
152
+ Args:
153
+ provider_id: Provider identifier to look up
154
+
155
+ Returns:
156
+ Provider if found, None otherwise
157
+ """
158
+ with self._lock:
159
+ return self._providers.get(provider_id)
160
+
161
+ def exists(self, provider_id: str) -> bool:
162
+ """Check if a provider exists in the repository.
163
+
164
+ Args:
165
+ provider_id: Provider identifier to check
166
+
167
+ Returns:
168
+ True if provider exists, False otherwise
169
+ """
170
+ with self._lock:
171
+ return provider_id in self._providers
172
+
173
+ def remove(self, provider_id: str) -> bool:
174
+ """Remove a provider from the repository.
175
+
176
+ Args:
177
+ provider_id: Provider identifier to remove
178
+
179
+ Returns:
180
+ True if provider was removed, False if not found
181
+ """
182
+ with self._lock:
183
+ if provider_id in self._providers:
184
+ del self._providers[provider_id]
185
+ return True
186
+ return False
187
+
188
+ def get_all(self) -> Dict[str, ProviderLike]:
189
+ """Get all providers as a dictionary.
190
+
191
+ Returns:
192
+ Dictionary mapping provider_id -> Provider
193
+ Returns a copy to prevent external modifications
194
+ """
195
+ with self._lock:
196
+ return dict(self._providers)
197
+
198
+ def get_all_ids(self) -> List[str]:
199
+ """Get all provider IDs.
200
+
201
+ Returns:
202
+ List of provider identifiers
203
+ """
204
+ with self._lock:
205
+ return list(self._providers.keys())
206
+
207
+ def count(self) -> int:
208
+ """Get the total number of providers.
209
+
210
+ Returns:
211
+ Number of providers in the repository
212
+ """
213
+ with self._lock:
214
+ return len(self._providers)
215
+
216
+ def clear(self) -> None:
217
+ """Remove all providers from the repository.
218
+
219
+ This is primarily for testing and cleanup.
220
+ """
221
+ with self._lock:
222
+ self._providers.clear()
223
+
224
+ def __contains__(self, provider_id: str) -> bool:
225
+ """Support 'in' operator for checking existence.
226
+
227
+ Args:
228
+ provider_id: Provider identifier to check
229
+
230
+ Returns:
231
+ True if provider exists, False otherwise
232
+ """
233
+ return self.exists(provider_id)
234
+
235
+ def __len__(self) -> int:
236
+ """Support len() function.
237
+
238
+ Returns:
239
+ Number of providers in the repository
240
+ """
241
+ return self.count()
242
+
243
+ def __repr__(self) -> str:
244
+ """String representation for debugging.
245
+
246
+ Returns:
247
+ String showing repository type and provider count
248
+ """
249
+ return f"InMemoryProviderRepository(providers={self.count()})"
@@ -0,0 +1,85 @@
1
+ """
2
+ Security module for the MCP Registry.
3
+
4
+ Provides security primitives including:
5
+ - Input validation and sanitization
6
+ - Command injection prevention
7
+ - Rate limiting
8
+ - Secrets management
9
+ - Security audit logging utilities
10
+ """
11
+
12
+ from .input_validator import (
13
+ InputValidator,
14
+ validate_arguments,
15
+ validate_command,
16
+ validate_docker_image,
17
+ validate_environment_variables,
18
+ validate_provider_id,
19
+ validate_timeout,
20
+ validate_tool_name,
21
+ ValidationResult,
22
+ )
23
+ from .rate_limiter import InMemoryRateLimiter, RateLimitConfig, RateLimiter, RateLimitResult
24
+ from .roles import (
25
+ BUILTIN_ROLES,
26
+ get_builtin_role,
27
+ get_permission,
28
+ list_builtin_roles,
29
+ list_permissions,
30
+ PERMISSIONS,
31
+ ROLE_ADMIN,
32
+ ROLE_AUDITOR,
33
+ ROLE_DEVELOPER,
34
+ ROLE_PROVIDER_ADMIN,
35
+ ROLE_VIEWER,
36
+ )
37
+ from .sanitizer import (
38
+ sanitize_command_argument,
39
+ sanitize_environment_value,
40
+ sanitize_log_message,
41
+ sanitize_path,
42
+ Sanitizer,
43
+ )
44
+ from .secrets import is_sensitive_key, mask_sensitive_value, SecretsMask, SecureEnvironment
45
+
46
+ __all__ = [
47
+ # Input Validation
48
+ "InputValidator",
49
+ "ValidationResult",
50
+ "validate_provider_id",
51
+ "validate_tool_name",
52
+ "validate_arguments",
53
+ "validate_timeout",
54
+ "validate_command",
55
+ "validate_docker_image",
56
+ "validate_environment_variables",
57
+ # Sanitization
58
+ "Sanitizer",
59
+ "sanitize_command_argument",
60
+ "sanitize_environment_value",
61
+ "sanitize_log_message",
62
+ "sanitize_path",
63
+ # Rate Limiting
64
+ "RateLimiter",
65
+ "RateLimitConfig",
66
+ "InMemoryRateLimiter",
67
+ "RateLimitResult",
68
+ # Secrets
69
+ "SecretsMask",
70
+ "SecureEnvironment",
71
+ "is_sensitive_key",
72
+ "mask_sensitive_value",
73
+ # Roles & Permissions
74
+ "BUILTIN_ROLES",
75
+ "PERMISSIONS",
76
+ "ROLE_ADMIN",
77
+ "ROLE_DEVELOPER",
78
+ "ROLE_PROVIDER_ADMIN",
79
+ "ROLE_VIEWER",
80
+ "ROLE_AUDITOR",
81
+ "get_builtin_role",
82
+ "get_permission",
83
+ "list_builtin_roles",
84
+ "list_permissions",
85
+ ]