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,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
+ ]