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.
- mcp_hangar/__init__.py +139 -0
- mcp_hangar/application/__init__.py +1 -0
- mcp_hangar/application/commands/__init__.py +67 -0
- mcp_hangar/application/commands/auth_commands.py +118 -0
- mcp_hangar/application/commands/auth_handlers.py +296 -0
- mcp_hangar/application/commands/commands.py +59 -0
- mcp_hangar/application/commands/handlers.py +189 -0
- mcp_hangar/application/discovery/__init__.py +21 -0
- mcp_hangar/application/discovery/discovery_metrics.py +283 -0
- mcp_hangar/application/discovery/discovery_orchestrator.py +497 -0
- mcp_hangar/application/discovery/lifecycle_manager.py +315 -0
- mcp_hangar/application/discovery/security_validator.py +414 -0
- mcp_hangar/application/event_handlers/__init__.py +50 -0
- mcp_hangar/application/event_handlers/alert_handler.py +191 -0
- mcp_hangar/application/event_handlers/audit_handler.py +203 -0
- mcp_hangar/application/event_handlers/knowledge_base_handler.py +120 -0
- mcp_hangar/application/event_handlers/logging_handler.py +69 -0
- mcp_hangar/application/event_handlers/metrics_handler.py +152 -0
- mcp_hangar/application/event_handlers/persistent_audit_store.py +217 -0
- mcp_hangar/application/event_handlers/security_handler.py +604 -0
- mcp_hangar/application/mcp/tooling.py +158 -0
- mcp_hangar/application/ports/__init__.py +9 -0
- mcp_hangar/application/ports/observability.py +237 -0
- mcp_hangar/application/queries/__init__.py +52 -0
- mcp_hangar/application/queries/auth_handlers.py +237 -0
- mcp_hangar/application/queries/auth_queries.py +118 -0
- mcp_hangar/application/queries/handlers.py +227 -0
- mcp_hangar/application/read_models/__init__.py +11 -0
- mcp_hangar/application/read_models/provider_views.py +139 -0
- mcp_hangar/application/sagas/__init__.py +11 -0
- mcp_hangar/application/sagas/group_rebalance_saga.py +137 -0
- mcp_hangar/application/sagas/provider_failover_saga.py +266 -0
- mcp_hangar/application/sagas/provider_recovery_saga.py +172 -0
- mcp_hangar/application/services/__init__.py +9 -0
- mcp_hangar/application/services/provider_service.py +208 -0
- mcp_hangar/application/services/traced_provider_service.py +211 -0
- mcp_hangar/bootstrap/runtime.py +328 -0
- mcp_hangar/context.py +178 -0
- mcp_hangar/domain/__init__.py +117 -0
- mcp_hangar/domain/contracts/__init__.py +57 -0
- mcp_hangar/domain/contracts/authentication.py +225 -0
- mcp_hangar/domain/contracts/authorization.py +229 -0
- mcp_hangar/domain/contracts/event_store.py +178 -0
- mcp_hangar/domain/contracts/metrics_publisher.py +59 -0
- mcp_hangar/domain/contracts/persistence.py +383 -0
- mcp_hangar/domain/contracts/provider_runtime.py +146 -0
- mcp_hangar/domain/discovery/__init__.py +20 -0
- mcp_hangar/domain/discovery/conflict_resolver.py +267 -0
- mcp_hangar/domain/discovery/discovered_provider.py +185 -0
- mcp_hangar/domain/discovery/discovery_service.py +412 -0
- mcp_hangar/domain/discovery/discovery_source.py +192 -0
- mcp_hangar/domain/events.py +433 -0
- mcp_hangar/domain/exceptions.py +525 -0
- mcp_hangar/domain/model/__init__.py +70 -0
- mcp_hangar/domain/model/aggregate.py +58 -0
- mcp_hangar/domain/model/circuit_breaker.py +152 -0
- mcp_hangar/domain/model/event_sourced_api_key.py +413 -0
- mcp_hangar/domain/model/event_sourced_provider.py +423 -0
- mcp_hangar/domain/model/event_sourced_role_assignment.py +268 -0
- mcp_hangar/domain/model/health_tracker.py +183 -0
- mcp_hangar/domain/model/load_balancer.py +185 -0
- mcp_hangar/domain/model/provider.py +810 -0
- mcp_hangar/domain/model/provider_group.py +656 -0
- mcp_hangar/domain/model/tool_catalog.py +105 -0
- mcp_hangar/domain/policies/__init__.py +19 -0
- mcp_hangar/domain/policies/provider_health.py +187 -0
- mcp_hangar/domain/repository.py +249 -0
- mcp_hangar/domain/security/__init__.py +85 -0
- mcp_hangar/domain/security/input_validator.py +710 -0
- mcp_hangar/domain/security/rate_limiter.py +387 -0
- mcp_hangar/domain/security/roles.py +237 -0
- mcp_hangar/domain/security/sanitizer.py +387 -0
- mcp_hangar/domain/security/secrets.py +501 -0
- mcp_hangar/domain/services/__init__.py +20 -0
- mcp_hangar/domain/services/audit_service.py +376 -0
- mcp_hangar/domain/services/image_builder.py +328 -0
- mcp_hangar/domain/services/provider_launcher.py +1046 -0
- mcp_hangar/domain/value_objects.py +1138 -0
- mcp_hangar/errors.py +818 -0
- mcp_hangar/fastmcp_server.py +1105 -0
- mcp_hangar/gc.py +134 -0
- mcp_hangar/infrastructure/__init__.py +79 -0
- mcp_hangar/infrastructure/async_executor.py +133 -0
- mcp_hangar/infrastructure/auth/__init__.py +37 -0
- mcp_hangar/infrastructure/auth/api_key_authenticator.py +388 -0
- mcp_hangar/infrastructure/auth/event_sourced_store.py +567 -0
- mcp_hangar/infrastructure/auth/jwt_authenticator.py +360 -0
- mcp_hangar/infrastructure/auth/middleware.py +340 -0
- mcp_hangar/infrastructure/auth/opa_authorizer.py +243 -0
- mcp_hangar/infrastructure/auth/postgres_store.py +659 -0
- mcp_hangar/infrastructure/auth/projections.py +366 -0
- mcp_hangar/infrastructure/auth/rate_limiter.py +311 -0
- mcp_hangar/infrastructure/auth/rbac_authorizer.py +323 -0
- mcp_hangar/infrastructure/auth/sqlite_store.py +624 -0
- mcp_hangar/infrastructure/command_bus.py +112 -0
- mcp_hangar/infrastructure/discovery/__init__.py +110 -0
- mcp_hangar/infrastructure/discovery/docker_source.py +289 -0
- mcp_hangar/infrastructure/discovery/entrypoint_source.py +249 -0
- mcp_hangar/infrastructure/discovery/filesystem_source.py +383 -0
- mcp_hangar/infrastructure/discovery/kubernetes_source.py +247 -0
- mcp_hangar/infrastructure/event_bus.py +260 -0
- mcp_hangar/infrastructure/event_sourced_repository.py +443 -0
- mcp_hangar/infrastructure/event_store.py +396 -0
- mcp_hangar/infrastructure/knowledge_base/__init__.py +259 -0
- mcp_hangar/infrastructure/knowledge_base/contracts.py +202 -0
- mcp_hangar/infrastructure/knowledge_base/memory.py +177 -0
- mcp_hangar/infrastructure/knowledge_base/postgres.py +545 -0
- mcp_hangar/infrastructure/knowledge_base/sqlite.py +513 -0
- mcp_hangar/infrastructure/metrics_publisher.py +36 -0
- mcp_hangar/infrastructure/observability/__init__.py +10 -0
- mcp_hangar/infrastructure/observability/langfuse_adapter.py +534 -0
- mcp_hangar/infrastructure/persistence/__init__.py +33 -0
- mcp_hangar/infrastructure/persistence/audit_repository.py +371 -0
- mcp_hangar/infrastructure/persistence/config_repository.py +398 -0
- mcp_hangar/infrastructure/persistence/database.py +333 -0
- mcp_hangar/infrastructure/persistence/database_common.py +330 -0
- mcp_hangar/infrastructure/persistence/event_serializer.py +280 -0
- mcp_hangar/infrastructure/persistence/event_upcaster.py +166 -0
- mcp_hangar/infrastructure/persistence/in_memory_event_store.py +150 -0
- mcp_hangar/infrastructure/persistence/recovery_service.py +312 -0
- mcp_hangar/infrastructure/persistence/sqlite_event_store.py +386 -0
- mcp_hangar/infrastructure/persistence/unit_of_work.py +409 -0
- mcp_hangar/infrastructure/persistence/upcasters/README.md +13 -0
- mcp_hangar/infrastructure/persistence/upcasters/__init__.py +7 -0
- mcp_hangar/infrastructure/query_bus.py +153 -0
- mcp_hangar/infrastructure/saga_manager.py +401 -0
- mcp_hangar/logging_config.py +209 -0
- mcp_hangar/metrics.py +1007 -0
- mcp_hangar/models.py +31 -0
- mcp_hangar/observability/__init__.py +54 -0
- mcp_hangar/observability/health.py +487 -0
- mcp_hangar/observability/metrics.py +319 -0
- mcp_hangar/observability/tracing.py +433 -0
- mcp_hangar/progress.py +542 -0
- mcp_hangar/retry.py +613 -0
- mcp_hangar/server/__init__.py +120 -0
- mcp_hangar/server/__main__.py +6 -0
- mcp_hangar/server/auth_bootstrap.py +340 -0
- mcp_hangar/server/auth_cli.py +335 -0
- mcp_hangar/server/auth_config.py +305 -0
- mcp_hangar/server/bootstrap.py +735 -0
- mcp_hangar/server/cli.py +161 -0
- mcp_hangar/server/config.py +224 -0
- mcp_hangar/server/context.py +215 -0
- mcp_hangar/server/http_auth_middleware.py +165 -0
- mcp_hangar/server/lifecycle.py +467 -0
- mcp_hangar/server/state.py +117 -0
- mcp_hangar/server/tools/__init__.py +16 -0
- mcp_hangar/server/tools/discovery.py +186 -0
- mcp_hangar/server/tools/groups.py +75 -0
- mcp_hangar/server/tools/health.py +301 -0
- mcp_hangar/server/tools/provider.py +939 -0
- mcp_hangar/server/tools/registry.py +320 -0
- mcp_hangar/server/validation.py +113 -0
- mcp_hangar/stdio_client.py +229 -0
- mcp_hangar-0.2.0.dist-info/METADATA +347 -0
- mcp_hangar-0.2.0.dist-info/RECORD +160 -0
- mcp_hangar-0.2.0.dist-info/WHEEL +4 -0
- mcp_hangar-0.2.0.dist-info/entry_points.txt +2 -0
- mcp_hangar-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
"""Application Bootstrap - Composition Root.
|
|
2
|
+
|
|
3
|
+
This module is responsible for wiring up all dependencies and initializing
|
|
4
|
+
application components. It is the composition root of the application.
|
|
5
|
+
|
|
6
|
+
The bootstrap process:
|
|
7
|
+
1. Load configuration
|
|
8
|
+
2. Initialize runtime (event bus, command bus, query bus)
|
|
9
|
+
3. Initialize event store (for event sourcing)
|
|
10
|
+
4. Register event handlers
|
|
11
|
+
5. Register CQRS handlers
|
|
12
|
+
6. Initialize sagas
|
|
13
|
+
7. Load providers from config
|
|
14
|
+
8. Initialize discovery (if enabled)
|
|
15
|
+
9. Create MCP server with tools
|
|
16
|
+
10. Create background workers (DO NOT START)
|
|
17
|
+
|
|
18
|
+
Key principle: Bootstrap returns a fully configured but NOT running application.
|
|
19
|
+
Starting is handled by the lifecycle module.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
from mcp.server.fastmcp import FastMCP
|
|
28
|
+
|
|
29
|
+
from ..application.commands import register_all_handlers as register_command_handlers
|
|
30
|
+
from ..application.discovery import DiscoveryConfig, DiscoveryOrchestrator
|
|
31
|
+
from ..application.event_handlers import AlertEventHandler, AuditEventHandler, LoggingEventHandler, MetricsEventHandler
|
|
32
|
+
from ..application.queries import register_all_handlers as register_query_handlers
|
|
33
|
+
from ..application.sagas import GroupRebalanceSaga
|
|
34
|
+
from ..domain.contracts.event_store import NullEventStore
|
|
35
|
+
from ..domain.discovery import DiscoveryMode
|
|
36
|
+
from ..domain.model import Provider
|
|
37
|
+
from ..gc import BackgroundWorker
|
|
38
|
+
from ..infrastructure.persistence import SQLiteEventStore
|
|
39
|
+
from ..infrastructure.saga_manager import get_saga_manager
|
|
40
|
+
from ..logging_config import get_logger
|
|
41
|
+
from ..retry import get_retry_store
|
|
42
|
+
from .auth_bootstrap import AuthComponents, bootstrap_auth
|
|
43
|
+
from .auth_config import parse_auth_config
|
|
44
|
+
from .config import load_configuration
|
|
45
|
+
from .context import get_context, init_context
|
|
46
|
+
from .state import (
|
|
47
|
+
get_runtime,
|
|
48
|
+
GROUPS,
|
|
49
|
+
PROVIDER_REPOSITORY,
|
|
50
|
+
PROVIDERS,
|
|
51
|
+
set_discovery_orchestrator,
|
|
52
|
+
set_group_rebalance_saga,
|
|
53
|
+
)
|
|
54
|
+
from .tools import (
|
|
55
|
+
register_discovery_tools,
|
|
56
|
+
register_group_tools,
|
|
57
|
+
register_health_tools,
|
|
58
|
+
register_provider_tools,
|
|
59
|
+
register_registry_tools,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if TYPE_CHECKING:
|
|
63
|
+
from ..bootstrap.runtime import Runtime
|
|
64
|
+
|
|
65
|
+
logger = get_logger(__name__)
|
|
66
|
+
|
|
67
|
+
# =============================================================================
|
|
68
|
+
# Constants
|
|
69
|
+
# =============================================================================
|
|
70
|
+
|
|
71
|
+
GC_WORKER_INTERVAL_SECONDS = 30
|
|
72
|
+
"""Interval for garbage collection worker."""
|
|
73
|
+
|
|
74
|
+
HEALTH_CHECK_INTERVAL_SECONDS = 60
|
|
75
|
+
"""Interval for health check worker."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# =============================================================================
|
|
79
|
+
# Application Context
|
|
80
|
+
# =============================================================================
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class ApplicationContext:
|
|
85
|
+
"""Fully initialized application context.
|
|
86
|
+
|
|
87
|
+
Contains all components needed to run the server.
|
|
88
|
+
Components are initialized but not started.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
runtime: "Runtime"
|
|
92
|
+
"""Runtime instance with buses and repository."""
|
|
93
|
+
|
|
94
|
+
mcp_server: FastMCP
|
|
95
|
+
"""FastMCP server instance with registered tools."""
|
|
96
|
+
|
|
97
|
+
background_workers: List[BackgroundWorker] = field(default_factory=list)
|
|
98
|
+
"""Background workers (GC, health check) - not started."""
|
|
99
|
+
|
|
100
|
+
discovery_orchestrator: Optional[DiscoveryOrchestrator] = None
|
|
101
|
+
"""Discovery orchestrator if enabled - not started."""
|
|
102
|
+
|
|
103
|
+
auth_components: Optional[AuthComponents] = None
|
|
104
|
+
"""Authentication and authorization components."""
|
|
105
|
+
|
|
106
|
+
config: Dict[str, Any] = field(default_factory=dict)
|
|
107
|
+
"""Full configuration dictionary."""
|
|
108
|
+
|
|
109
|
+
def shutdown(self) -> None:
|
|
110
|
+
"""Graceful shutdown of all components.
|
|
111
|
+
|
|
112
|
+
Stops background workers, discovery orchestrator, and cleans up resources.
|
|
113
|
+
"""
|
|
114
|
+
logger.info("application_context_shutdown_start")
|
|
115
|
+
|
|
116
|
+
# Stop background workers
|
|
117
|
+
for worker in self.background_workers:
|
|
118
|
+
try:
|
|
119
|
+
worker.stop()
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.warning(
|
|
122
|
+
"worker_stop_failed",
|
|
123
|
+
task=worker.task,
|
|
124
|
+
error=str(e),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Stop discovery orchestrator
|
|
128
|
+
if self.discovery_orchestrator:
|
|
129
|
+
try:
|
|
130
|
+
asyncio.run(self.discovery_orchestrator.stop())
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.warning("discovery_orchestrator_stop_failed", error=str(e))
|
|
133
|
+
|
|
134
|
+
# Stop all providers
|
|
135
|
+
for provider_id, provider in PROVIDERS.items():
|
|
136
|
+
try:
|
|
137
|
+
provider.stop()
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.warning(
|
|
140
|
+
"provider_stop_failed",
|
|
141
|
+
provider_id=provider_id,
|
|
142
|
+
error=str(e),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
logger.info("application_context_shutdown_complete")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# =============================================================================
|
|
149
|
+
# Bootstrap Functions
|
|
150
|
+
# =============================================================================
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def bootstrap(config_path: Optional[str] = None) -> ApplicationContext:
|
|
154
|
+
"""Bootstrap the application.
|
|
155
|
+
|
|
156
|
+
Initializes all components in correct order:
|
|
157
|
+
1. Ensure data directory exists
|
|
158
|
+
2. Initialize runtime (event bus, command bus, query bus)
|
|
159
|
+
3. Initialize event store (for event sourcing)
|
|
160
|
+
4. Initialize application context
|
|
161
|
+
5. Register event handlers
|
|
162
|
+
6. Register CQRS handlers
|
|
163
|
+
7. Initialize sagas
|
|
164
|
+
8. Load configuration and providers
|
|
165
|
+
9. Initialize retry configuration
|
|
166
|
+
10. Initialize knowledge base (if enabled)
|
|
167
|
+
11. Create MCP server with tools
|
|
168
|
+
12. Create background workers (DO NOT START)
|
|
169
|
+
13. Initialize discovery (if enabled, DO NOT START)
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
config_path: Optional path to config.yaml
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Fully initialized ApplicationContext (components not started)
|
|
176
|
+
"""
|
|
177
|
+
logger.info("bootstrap_start", config_path=config_path)
|
|
178
|
+
|
|
179
|
+
# Ensure data directory exists
|
|
180
|
+
_ensure_data_dir()
|
|
181
|
+
|
|
182
|
+
# Initialize runtime and context
|
|
183
|
+
runtime = get_runtime()
|
|
184
|
+
init_context(runtime)
|
|
185
|
+
|
|
186
|
+
# Load configuration early (needed for event store config)
|
|
187
|
+
full_config = load_configuration(config_path)
|
|
188
|
+
|
|
189
|
+
# Initialize event store for event sourcing
|
|
190
|
+
_init_event_store(runtime, full_config)
|
|
191
|
+
|
|
192
|
+
# Initialize event handlers
|
|
193
|
+
_init_event_handlers(runtime)
|
|
194
|
+
|
|
195
|
+
# Initialize CQRS
|
|
196
|
+
_init_cqrs(runtime)
|
|
197
|
+
|
|
198
|
+
# Initialize saga
|
|
199
|
+
_init_saga()
|
|
200
|
+
|
|
201
|
+
logger.info(
|
|
202
|
+
"security_config_loaded",
|
|
203
|
+
rate_limit_rps=runtime.rate_limit_config.requests_per_second,
|
|
204
|
+
burst_size=runtime.rate_limit_config.burst_size,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Initialize authentication and authorization
|
|
208
|
+
auth_config = parse_auth_config(full_config.get("auth"))
|
|
209
|
+
auth_components = bootstrap_auth(
|
|
210
|
+
config=auth_config,
|
|
211
|
+
event_publisher=lambda event: runtime.event_bus.publish(event),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Initialize retry configuration
|
|
215
|
+
_init_retry_config(full_config)
|
|
216
|
+
|
|
217
|
+
# Initialize knowledge base
|
|
218
|
+
_init_knowledge_base(full_config)
|
|
219
|
+
|
|
220
|
+
# Create MCP server and register tools
|
|
221
|
+
mcp_server = FastMCP("mcp-registry")
|
|
222
|
+
_register_all_tools(mcp_server)
|
|
223
|
+
|
|
224
|
+
# Create background workers (not started)
|
|
225
|
+
workers = _create_background_workers()
|
|
226
|
+
|
|
227
|
+
# Initialize discovery (not started)
|
|
228
|
+
discovery_orchestrator = None
|
|
229
|
+
discovery_config = full_config.get("discovery", {})
|
|
230
|
+
if discovery_config.get("enabled", False):
|
|
231
|
+
discovery_orchestrator = _create_discovery_orchestrator(full_config)
|
|
232
|
+
|
|
233
|
+
# Log ready state
|
|
234
|
+
provider_ids = list(PROVIDERS.keys())
|
|
235
|
+
group_ids = list(GROUPS.keys())
|
|
236
|
+
logger.info(
|
|
237
|
+
"bootstrap_complete",
|
|
238
|
+
providers=provider_ids,
|
|
239
|
+
groups=group_ids,
|
|
240
|
+
discovery_enabled=discovery_orchestrator is not None,
|
|
241
|
+
auth_enabled=auth_components.enabled,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return ApplicationContext(
|
|
245
|
+
runtime=runtime,
|
|
246
|
+
mcp_server=mcp_server,
|
|
247
|
+
background_workers=workers,
|
|
248
|
+
discovery_orchestrator=discovery_orchestrator,
|
|
249
|
+
auth_components=auth_components,
|
|
250
|
+
config=full_config,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# =============================================================================
|
|
255
|
+
# Internal Initialization Functions
|
|
256
|
+
# =============================================================================
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _ensure_data_dir() -> None:
|
|
260
|
+
"""Ensure data directory exists for persistent storage."""
|
|
261
|
+
data_dir = Path("./data")
|
|
262
|
+
if not data_dir.exists():
|
|
263
|
+
try:
|
|
264
|
+
data_dir.mkdir(mode=0o755, parents=True, exist_ok=True)
|
|
265
|
+
logger.info("data_directory_created", path=str(data_dir.absolute()))
|
|
266
|
+
except OSError as e:
|
|
267
|
+
logger.warning("data_directory_creation_failed", error=str(e))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _init_event_store(runtime: "Runtime", config: Dict[str, Any]) -> None:
|
|
271
|
+
"""Initialize event store for event sourcing.
|
|
272
|
+
|
|
273
|
+
Configures the event store based on config.yaml settings.
|
|
274
|
+
Defaults to SQLite if not specified.
|
|
275
|
+
|
|
276
|
+
Config example:
|
|
277
|
+
event_store:
|
|
278
|
+
enabled: true
|
|
279
|
+
driver: sqlite # or "memory"
|
|
280
|
+
path: data/events.db
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
runtime: Runtime instance with event bus.
|
|
284
|
+
config: Full configuration dictionary.
|
|
285
|
+
"""
|
|
286
|
+
event_store_config = config.get("event_store", {})
|
|
287
|
+
enabled = event_store_config.get("enabled", True)
|
|
288
|
+
|
|
289
|
+
if not enabled:
|
|
290
|
+
logger.info("event_store_disabled")
|
|
291
|
+
runtime.event_bus.set_event_store(NullEventStore())
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
driver = event_store_config.get("driver", "sqlite")
|
|
295
|
+
|
|
296
|
+
if driver == "memory":
|
|
297
|
+
from ..infrastructure.persistence import InMemoryEventStore
|
|
298
|
+
|
|
299
|
+
event_store = InMemoryEventStore()
|
|
300
|
+
logger.info("event_store_initialized", driver="memory")
|
|
301
|
+
elif driver == "sqlite":
|
|
302
|
+
db_path = event_store_config.get("path", "data/events.db")
|
|
303
|
+
# Ensure directory exists
|
|
304
|
+
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
305
|
+
event_store = SQLiteEventStore(db_path)
|
|
306
|
+
logger.info("event_store_initialized", driver="sqlite", path=db_path)
|
|
307
|
+
else:
|
|
308
|
+
logger.warning(
|
|
309
|
+
"unknown_event_store_driver",
|
|
310
|
+
driver=driver,
|
|
311
|
+
fallback="sqlite",
|
|
312
|
+
)
|
|
313
|
+
event_store = SQLiteEventStore("data/events.db")
|
|
314
|
+
|
|
315
|
+
runtime.event_bus.set_event_store(event_store)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _init_event_handlers(runtime: "Runtime") -> None:
|
|
319
|
+
"""Register all event handlers.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
runtime: Runtime instance with event bus.
|
|
323
|
+
"""
|
|
324
|
+
logging_handler = LoggingEventHandler()
|
|
325
|
+
runtime.event_bus.subscribe_to_all(logging_handler.handle)
|
|
326
|
+
|
|
327
|
+
metrics_handler = MetricsEventHandler()
|
|
328
|
+
runtime.event_bus.subscribe_to_all(metrics_handler.handle)
|
|
329
|
+
|
|
330
|
+
alert_handler = AlertEventHandler()
|
|
331
|
+
runtime.event_bus.subscribe_to_all(alert_handler.handle)
|
|
332
|
+
|
|
333
|
+
audit_handler = AuditEventHandler()
|
|
334
|
+
runtime.event_bus.subscribe_to_all(audit_handler.handle)
|
|
335
|
+
|
|
336
|
+
runtime.event_bus.subscribe_to_all(runtime.security_handler.handle)
|
|
337
|
+
|
|
338
|
+
# Knowledge base handler (PostgreSQL persistence)
|
|
339
|
+
from ..application.event_handlers.knowledge_base_handler import KnowledgeBaseEventHandler
|
|
340
|
+
|
|
341
|
+
kb_handler = KnowledgeBaseEventHandler()
|
|
342
|
+
runtime.event_bus.subscribe_to_all(kb_handler.handle)
|
|
343
|
+
|
|
344
|
+
logger.info(
|
|
345
|
+
"event_handlers_registered",
|
|
346
|
+
handlers=["logging", "metrics", "alert", "audit", "security", "knowledge_base"],
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _init_cqrs(runtime: "Runtime") -> None:
|
|
351
|
+
"""Register command and query handlers.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
runtime: Runtime instance with command and query buses.
|
|
355
|
+
"""
|
|
356
|
+
register_command_handlers(runtime.command_bus, PROVIDER_REPOSITORY, runtime.event_bus)
|
|
357
|
+
register_query_handlers(runtime.query_bus, PROVIDER_REPOSITORY)
|
|
358
|
+
logger.info("cqrs_handlers_registered")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _init_saga() -> None:
|
|
362
|
+
"""Initialize group rebalance saga."""
|
|
363
|
+
ctx = get_context()
|
|
364
|
+
saga = GroupRebalanceSaga(groups=ctx.groups)
|
|
365
|
+
ctx.group_rebalance_saga = saga
|
|
366
|
+
set_group_rebalance_saga(saga) # For backward compatibility
|
|
367
|
+
saga_manager = get_saga_manager()
|
|
368
|
+
saga_manager.register_event_saga(saga)
|
|
369
|
+
logger.info("group_rebalance_saga_registered")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _init_retry_config(config: Dict[str, Any]) -> None:
|
|
373
|
+
"""Initialize retry configuration from config.yaml.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
config: Full configuration dictionary.
|
|
377
|
+
"""
|
|
378
|
+
retry_store = get_retry_store()
|
|
379
|
+
retry_store.load_from_config(config)
|
|
380
|
+
logger.info("retry_config_loaded")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _init_knowledge_base(config: Dict[str, Any]) -> None:
|
|
384
|
+
"""Initialize knowledge base from config.yaml.
|
|
385
|
+
|
|
386
|
+
Supports multiple drivers (postgres, sqlite, memory) with auto-detection.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
config: Full configuration dictionary.
|
|
390
|
+
"""
|
|
391
|
+
from ..infrastructure.knowledge_base import init_knowledge_base, KnowledgeBaseConfig
|
|
392
|
+
|
|
393
|
+
kb_config_dict = config.get("knowledge_base", {})
|
|
394
|
+
kb_config = KnowledgeBaseConfig.from_dict(kb_config_dict)
|
|
395
|
+
|
|
396
|
+
if not kb_config.enabled:
|
|
397
|
+
logger.info("knowledge_base_disabled")
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
# Initialize asynchronously
|
|
401
|
+
async def init():
|
|
402
|
+
kb = await init_knowledge_base(kb_config)
|
|
403
|
+
if kb:
|
|
404
|
+
# Verify health
|
|
405
|
+
healthy = await kb.is_healthy()
|
|
406
|
+
if healthy:
|
|
407
|
+
logger.info("knowledge_base_health_ok")
|
|
408
|
+
else:
|
|
409
|
+
logger.warning("knowledge_base_health_check_failed")
|
|
410
|
+
|
|
411
|
+
asyncio.run(init())
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _register_all_tools(mcp_server: FastMCP) -> None:
|
|
415
|
+
"""Register all MCP tools on the server.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
mcp_server: FastMCP server instance.
|
|
419
|
+
"""
|
|
420
|
+
register_registry_tools(mcp_server)
|
|
421
|
+
register_provider_tools(mcp_server)
|
|
422
|
+
register_health_tools(mcp_server)
|
|
423
|
+
register_discovery_tools(mcp_server)
|
|
424
|
+
register_group_tools(mcp_server)
|
|
425
|
+
logger.info("mcp_tools_registered")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _create_background_workers() -> List[BackgroundWorker]:
|
|
429
|
+
"""Create (but don't start) background workers.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
List of BackgroundWorker instances (not started).
|
|
433
|
+
"""
|
|
434
|
+
gc_worker = BackgroundWorker(
|
|
435
|
+
PROVIDERS,
|
|
436
|
+
interval_s=GC_WORKER_INTERVAL_SECONDS,
|
|
437
|
+
task="gc",
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
health_worker = BackgroundWorker(
|
|
441
|
+
PROVIDERS,
|
|
442
|
+
interval_s=HEALTH_CHECK_INTERVAL_SECONDS,
|
|
443
|
+
task="health_check",
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
logger.info("background_workers_created", workers=["gc", "health_check"])
|
|
447
|
+
return [gc_worker, health_worker]
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# =============================================================================
|
|
451
|
+
# Discovery Initialization
|
|
452
|
+
# =============================================================================
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _create_discovery_orchestrator(config: Dict[str, Any]) -> Optional[DiscoveryOrchestrator]:
|
|
456
|
+
"""Create discovery orchestrator from config (not started).
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
config: Full configuration dictionary.
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
DiscoveryOrchestrator instance or None if disabled.
|
|
463
|
+
"""
|
|
464
|
+
discovery_config = config.get("discovery", {})
|
|
465
|
+
if not discovery_config.get("enabled", False):
|
|
466
|
+
logger.info("discovery_disabled")
|
|
467
|
+
return None
|
|
468
|
+
|
|
469
|
+
logger.info("discovery_initializing")
|
|
470
|
+
|
|
471
|
+
static_providers = set(PROVIDERS.keys())
|
|
472
|
+
orchestrator_config = DiscoveryConfig.from_dict(discovery_config)
|
|
473
|
+
orchestrator = DiscoveryOrchestrator(
|
|
474
|
+
config=orchestrator_config,
|
|
475
|
+
static_providers=static_providers,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
sources_config = discovery_config.get("sources", [])
|
|
479
|
+
for source_config in sources_config:
|
|
480
|
+
source_type = source_config.get("type")
|
|
481
|
+
try:
|
|
482
|
+
source = _create_discovery_source(source_type, source_config)
|
|
483
|
+
if source:
|
|
484
|
+
orchestrator.add_source(source)
|
|
485
|
+
except ImportError as e:
|
|
486
|
+
logger.warning(
|
|
487
|
+
"discovery_source_unavailable",
|
|
488
|
+
source_type=source_type,
|
|
489
|
+
error=str(e),
|
|
490
|
+
)
|
|
491
|
+
except Exception as e:
|
|
492
|
+
logger.error(
|
|
493
|
+
"discovery_source_error",
|
|
494
|
+
source_type=source_type,
|
|
495
|
+
error=str(e),
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Set up registration callbacks
|
|
499
|
+
orchestrator.on_register = _on_provider_register
|
|
500
|
+
orchestrator.on_deregister = _on_provider_deregister
|
|
501
|
+
|
|
502
|
+
set_discovery_orchestrator(orchestrator)
|
|
503
|
+
return orchestrator
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _create_discovery_source(source_type: str, config: Dict[str, Any]):
|
|
507
|
+
"""Create a discovery source based on type and config.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
source_type: Type of discovery source (kubernetes, docker, filesystem, entrypoint).
|
|
511
|
+
config: Source configuration dictionary.
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
Discovery source instance or None.
|
|
515
|
+
"""
|
|
516
|
+
mode_str = config.get("mode", "additive")
|
|
517
|
+
mode = DiscoveryMode.AUTHORITATIVE if mode_str == "authoritative" else DiscoveryMode.ADDITIVE
|
|
518
|
+
|
|
519
|
+
if source_type == "kubernetes":
|
|
520
|
+
from ..infrastructure.discovery import KubernetesDiscoverySource
|
|
521
|
+
|
|
522
|
+
return KubernetesDiscoverySource(
|
|
523
|
+
mode=mode,
|
|
524
|
+
namespaces=config.get("namespaces"),
|
|
525
|
+
label_selector=config.get("label_selector"),
|
|
526
|
+
in_cluster=config.get("in_cluster", True),
|
|
527
|
+
)
|
|
528
|
+
elif source_type == "docker":
|
|
529
|
+
from ..infrastructure.discovery import DockerDiscoverySource
|
|
530
|
+
|
|
531
|
+
return DockerDiscoverySource(
|
|
532
|
+
mode=mode,
|
|
533
|
+
socket_path=config.get("socket_path"),
|
|
534
|
+
)
|
|
535
|
+
elif source_type == "filesystem":
|
|
536
|
+
from ..infrastructure.discovery import FilesystemDiscoverySource
|
|
537
|
+
|
|
538
|
+
path = config.get("path", "/etc/mcp-hangar/providers.d/")
|
|
539
|
+
resolved_path = Path(path)
|
|
540
|
+
if not resolved_path.is_absolute():
|
|
541
|
+
resolved_path = Path.cwd() / resolved_path
|
|
542
|
+
return FilesystemDiscoverySource(
|
|
543
|
+
mode=mode,
|
|
544
|
+
path=str(resolved_path),
|
|
545
|
+
pattern=config.get("pattern", "*.yaml"),
|
|
546
|
+
watch=config.get("watch", True),
|
|
547
|
+
)
|
|
548
|
+
elif source_type == "entrypoint":
|
|
549
|
+
from ..infrastructure.discovery import EntrypointDiscoverySource
|
|
550
|
+
|
|
551
|
+
return EntrypointDiscoverySource(
|
|
552
|
+
mode=mode,
|
|
553
|
+
group=config.get("group", "mcp.providers"),
|
|
554
|
+
)
|
|
555
|
+
else:
|
|
556
|
+
logger.warning("discovery_unknown_source_type", source_type=source_type)
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
async def _on_provider_register(provider) -> bool:
|
|
561
|
+
"""Callback when discovery wants to register a provider.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
provider: Discovered provider information.
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
True if registration succeeded, False otherwise.
|
|
568
|
+
"""
|
|
569
|
+
try:
|
|
570
|
+
conn_info = provider.connection_info
|
|
571
|
+
mode = provider.mode
|
|
572
|
+
|
|
573
|
+
if mode == "container":
|
|
574
|
+
provider_mode = "docker"
|
|
575
|
+
elif mode in ("http", "sse"):
|
|
576
|
+
provider_mode = "remote"
|
|
577
|
+
elif mode in ("subprocess", "docker", "remote"):
|
|
578
|
+
provider_mode = mode
|
|
579
|
+
else:
|
|
580
|
+
logger.warning(
|
|
581
|
+
"unknown_provider_mode_skipping",
|
|
582
|
+
mode=mode,
|
|
583
|
+
provider_name=provider.name,
|
|
584
|
+
)
|
|
585
|
+
return False
|
|
586
|
+
|
|
587
|
+
provider_kwargs = {
|
|
588
|
+
"provider_id": provider.name,
|
|
589
|
+
"mode": provider_mode,
|
|
590
|
+
"description": f"Discovered from {provider.source_type}",
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if provider_mode == "docker":
|
|
594
|
+
image = conn_info.get("image")
|
|
595
|
+
if not image:
|
|
596
|
+
logger.warning(
|
|
597
|
+
"container_provider_no_image_skipping",
|
|
598
|
+
provider_name=provider.name,
|
|
599
|
+
)
|
|
600
|
+
return False
|
|
601
|
+
provider_kwargs["image"] = image
|
|
602
|
+
provider_kwargs["read_only"] = conn_info.get("read_only", False)
|
|
603
|
+
if conn_info.get("command"):
|
|
604
|
+
provider_kwargs["command"] = conn_info.get("command")
|
|
605
|
+
|
|
606
|
+
volumes = conn_info.get("volumes", [])
|
|
607
|
+
if not volumes:
|
|
608
|
+
volumes = _auto_add_volumes(provider.name)
|
|
609
|
+
if volumes:
|
|
610
|
+
provider_kwargs["volumes"] = volumes
|
|
611
|
+
|
|
612
|
+
elif provider_mode == "remote":
|
|
613
|
+
host = conn_info.get("host")
|
|
614
|
+
port = conn_info.get("port")
|
|
615
|
+
endpoint = conn_info.get("endpoint")
|
|
616
|
+
if endpoint:
|
|
617
|
+
provider_kwargs["endpoint"] = endpoint
|
|
618
|
+
elif host and port:
|
|
619
|
+
provider_kwargs["endpoint"] = f"http://{host}:{port}"
|
|
620
|
+
else:
|
|
621
|
+
logger.warning(
|
|
622
|
+
"http_provider_no_endpoint_skipping",
|
|
623
|
+
provider_name=provider.name,
|
|
624
|
+
)
|
|
625
|
+
return False
|
|
626
|
+
else:
|
|
627
|
+
command = conn_info.get("command")
|
|
628
|
+
if not command:
|
|
629
|
+
logger.warning(
|
|
630
|
+
"subprocess_provider_no_command_skipping",
|
|
631
|
+
provider_name=provider.name,
|
|
632
|
+
)
|
|
633
|
+
return False
|
|
634
|
+
provider_kwargs["command"] = command
|
|
635
|
+
|
|
636
|
+
provider_kwargs["env"] = conn_info.get("env", {})
|
|
637
|
+
|
|
638
|
+
new_provider = Provider(**provider_kwargs)
|
|
639
|
+
PROVIDERS[provider.name] = new_provider
|
|
640
|
+
logger.info(
|
|
641
|
+
"discovery_registered_provider",
|
|
642
|
+
provider_name=provider.name,
|
|
643
|
+
mode=provider_mode,
|
|
644
|
+
)
|
|
645
|
+
return True
|
|
646
|
+
except Exception as e:
|
|
647
|
+
logger.error(
|
|
648
|
+
"discovery_registration_failed",
|
|
649
|
+
provider_name=provider.name,
|
|
650
|
+
error=str(e),
|
|
651
|
+
)
|
|
652
|
+
return False
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
async def _on_provider_deregister(name: str, reason: str):
|
|
656
|
+
"""Callback when discovery wants to deregister a provider.
|
|
657
|
+
|
|
658
|
+
Args:
|
|
659
|
+
name: Provider name to deregister.
|
|
660
|
+
reason: Reason for deregistration.
|
|
661
|
+
"""
|
|
662
|
+
try:
|
|
663
|
+
if name in PROVIDERS:
|
|
664
|
+
provider = PROVIDERS.get(name)
|
|
665
|
+
if provider:
|
|
666
|
+
provider.stop()
|
|
667
|
+
del PROVIDERS._repo._providers[name]
|
|
668
|
+
logger.info(
|
|
669
|
+
"discovery_deregistered_provider",
|
|
670
|
+
provider_name=name,
|
|
671
|
+
reason=reason,
|
|
672
|
+
)
|
|
673
|
+
except Exception as e:
|
|
674
|
+
logger.error(
|
|
675
|
+
"discovery_deregistration_failed",
|
|
676
|
+
provider_name=name,
|
|
677
|
+
error=str(e),
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _auto_add_volumes(provider_name: str) -> list:
|
|
682
|
+
"""Auto-add persistent volumes for known stateful providers.
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
provider_name: Provider name to check for known volume patterns.
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
List of volume mount strings.
|
|
689
|
+
"""
|
|
690
|
+
volumes = []
|
|
691
|
+
provider_name_lower = provider_name.lower()
|
|
692
|
+
data_base = Path("./data").absolute()
|
|
693
|
+
|
|
694
|
+
if "memory" in provider_name_lower:
|
|
695
|
+
memory_dir = data_base / "memory"
|
|
696
|
+
memory_dir.mkdir(parents=True, exist_ok=True)
|
|
697
|
+
memory_dir.chmod(0o777)
|
|
698
|
+
volumes.append(f"{memory_dir}:/app/data:rw")
|
|
699
|
+
logger.info(
|
|
700
|
+
"auto_added_memory_volume",
|
|
701
|
+
provider_name=provider_name,
|
|
702
|
+
volume=f"{memory_dir}:/app/data",
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
elif "filesystem" in provider_name_lower:
|
|
706
|
+
fs_dir = data_base / "filesystem"
|
|
707
|
+
fs_dir.mkdir(parents=True, exist_ok=True)
|
|
708
|
+
fs_dir.chmod(0o777)
|
|
709
|
+
volumes.append(f"{fs_dir}:/data:rw")
|
|
710
|
+
logger.info(
|
|
711
|
+
"auto_added_filesystem_volume",
|
|
712
|
+
provider_name=provider_name,
|
|
713
|
+
volume=f"{fs_dir}:/data",
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
return volumes
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
__all__ = [
|
|
720
|
+
"ApplicationContext",
|
|
721
|
+
"bootstrap",
|
|
722
|
+
"GC_WORKER_INTERVAL_SECONDS",
|
|
723
|
+
"HEALTH_CHECK_INTERVAL_SECONDS",
|
|
724
|
+
# Internal functions exported for backward compatibility / testing
|
|
725
|
+
"_auto_add_volumes",
|
|
726
|
+
"_create_background_workers",
|
|
727
|
+
"_create_discovery_source",
|
|
728
|
+
"_ensure_data_dir",
|
|
729
|
+
"_init_cqrs",
|
|
730
|
+
"_init_event_handlers",
|
|
731
|
+
"_init_knowledge_base",
|
|
732
|
+
"_init_retry_config",
|
|
733
|
+
"_init_saga",
|
|
734
|
+
"_register_all_tools",
|
|
735
|
+
]
|