omnibase_infra 0.2.1__py3-none-any.whl → 0.2.2__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 (116) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +446 -0
  3. omnibase_infra/cli/commands.py +1 -1
  4. omnibase_infra/configs/widget_mapping.yaml +176 -0
  5. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +4 -1
  6. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +4 -1
  7. omnibase_infra/errors/error_compute_registry.py +4 -1
  8. omnibase_infra/errors/error_event_bus_registry.py +4 -1
  9. omnibase_infra/errors/error_infra.py +3 -1
  10. omnibase_infra/errors/error_policy_registry.py +4 -1
  11. omnibase_infra/handlers/handler_db.py +2 -1
  12. omnibase_infra/handlers/handler_graph.py +10 -5
  13. omnibase_infra/handlers/handler_mcp.py +736 -63
  14. omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
  15. omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
  16. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +301 -4
  17. omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
  18. omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
  19. omnibase_infra/mixins/mixin_node_introspection.py +24 -7
  20. omnibase_infra/mixins/mixin_retry_execution.py +1 -1
  21. omnibase_infra/models/handlers/__init__.py +10 -0
  22. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  23. omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
  24. omnibase_infra/models/mcp/__init__.py +15 -0
  25. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  26. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  27. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  28. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  29. omnibase_infra/models/registration/model_node_capabilities.py +11 -0
  30. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
  31. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
  32. omnibase_infra/nodes/effects/contract.yaml +0 -5
  33. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
  34. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
  35. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
  36. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
  37. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
  38. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
  39. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +46 -25
  40. omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
  41. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
  42. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +24 -19
  43. omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
  44. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
  45. omnibase_infra/plugins/plugin_compute_base.py +16 -2
  46. omnibase_infra/protocols/protocol_event_projector.py +1 -1
  47. omnibase_infra/runtime/__init__.py +51 -1
  48. omnibase_infra/runtime/binding_config_resolver.py +102 -37
  49. omnibase_infra/runtime/constants_notification.py +75 -0
  50. omnibase_infra/runtime/contract_handler_discovery.py +6 -1
  51. omnibase_infra/runtime/handler_bootstrap_source.py +514 -0
  52. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  53. omnibase_infra/runtime/handler_contract_source.py +289 -167
  54. omnibase_infra/runtime/handler_plugin_loader.py +4 -2
  55. omnibase_infra/runtime/mixin_semver_cache.py +25 -1
  56. omnibase_infra/runtime/mixins/__init__.py +7 -0
  57. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  58. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
  59. omnibase_infra/runtime/models/__init__.py +24 -0
  60. omnibase_infra/runtime/models/model_health_check_result.py +2 -1
  61. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  62. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  63. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  64. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  65. omnibase_infra/runtime/projector_plugin_loader.py +1 -1
  66. omnibase_infra/runtime/projector_shell.py +229 -1
  67. omnibase_infra/runtime/protocols/__init__.py +10 -0
  68. omnibase_infra/runtime/registry/registry_protocol_binding.py +3 -2
  69. omnibase_infra/runtime/registry_policy.py +9 -326
  70. omnibase_infra/runtime/secret_resolver.py +4 -2
  71. omnibase_infra/runtime/service_kernel.py +10 -2
  72. omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
  73. omnibase_infra/runtime/service_runtime_host_process.py +225 -15
  74. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  75. omnibase_infra/runtime/transition_notification_publisher.py +764 -0
  76. omnibase_infra/runtime/util_container_wiring.py +6 -5
  77. omnibase_infra/runtime/util_wiring.py +5 -1
  78. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  79. omnibase_infra/services/mcp/__init__.py +31 -0
  80. omnibase_infra/services/mcp/mcp_server_lifecycle.py +443 -0
  81. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  82. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  83. omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
  84. omnibase_infra/services/registry_api/__init__.py +40 -0
  85. omnibase_infra/services/registry_api/main.py +243 -0
  86. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  87. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  88. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  89. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  90. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  91. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  92. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  93. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  94. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  95. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  96. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  97. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  98. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  99. omnibase_infra/services/registry_api/routes.py +371 -0
  100. omnibase_infra/services/registry_api/service.py +846 -0
  101. omnibase_infra/services/service_capability_query.py +4 -4
  102. omnibase_infra/services/service_health.py +3 -2
  103. omnibase_infra/services/service_timeout_emitter.py +13 -2
  104. omnibase_infra/utils/util_dsn_validation.py +1 -1
  105. omnibase_infra/validation/__init__.py +3 -19
  106. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  107. omnibase_infra/validation/infra_validators.py +35 -24
  108. omnibase_infra/validation/validation_exemptions.yaml +113 -9
  109. omnibase_infra/validation/validator_chain_propagation.py +2 -2
  110. omnibase_infra/validation/validator_runtime_shape.py +1 -1
  111. omnibase_infra/validation/validator_security.py +473 -370
  112. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/METADATA +2 -2
  113. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/RECORD +116 -74
  114. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/WHEEL +0 -0
  115. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/entry_points.txt +0 -0
  116. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,443 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """MCP Server Lifecycle - Orchestrates startup and shutdown of MCP services.
4
+
5
+ This module provides the MCPServerLifecycle class that manages the complete
6
+ lifecycle of the MCP server, including:
7
+ - Cold start: Discover tools from Consul and populate registry
8
+ - Hot reload: Start Kafka subscription for real-time updates
9
+ - Handler initialization: Set up HandlerMCP with registry and executor
10
+ - Graceful shutdown: Clean up all resources
11
+
12
+ Architecture:
13
+ MCPServerLifecycle acts as the composition root for MCP services, wiring
14
+ together the discovery, registry, sync, and execution components.
15
+
16
+ Usage:
17
+ ```python
18
+ lifecycle = MCPServerLifecycle(config)
19
+ await lifecycle.start()
20
+ # ... server is running ...
21
+ await lifecycle.shutdown()
22
+ ```
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ from typing import TYPE_CHECKING
29
+ from uuid import UUID, uuid4
30
+
31
+ from omnibase_infra.adapters.adapter_onex_tool_execution import (
32
+ AdapterONEXToolExecution,
33
+ )
34
+ from omnibase_infra.models.mcp.model_mcp_server_config import ModelMCPServerConfig
35
+ from omnibase_infra.services.mcp.service_mcp_tool_discovery import (
36
+ ServiceMCPToolDiscovery,
37
+ )
38
+ from omnibase_infra.services.mcp.service_mcp_tool_registry import (
39
+ ServiceMCPToolRegistry,
40
+ )
41
+ from omnibase_infra.services.mcp.service_mcp_tool_sync import ServiceMCPToolSync
42
+
43
+ if TYPE_CHECKING:
44
+ from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
45
+ from omnibase_infra.handlers.handler_mcp import HandlerMCP
46
+ from omnibase_infra.models.mcp.model_mcp_tool_definition import (
47
+ ModelMCPToolDefinition,
48
+ )
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ class MCPServerLifecycle:
54
+ """Orchestrates startup and shutdown of MCP server components.
55
+
56
+ This class manages the lifecycle of all MCP-related services:
57
+ - ServiceMCPToolRegistry: In-memory cache of tool definitions
58
+ - ServiceMCPToolDiscovery: Consul scanner for MCP-enabled orchestrators
59
+ - ServiceMCPToolSync: Kafka listener for hot reload
60
+ - AdapterONEXToolExecution: Dispatcher bridge for tool execution
61
+
62
+ Lifecycle Phases:
63
+ 1. start(): Initialize services and populate registry
64
+ - Create registry, discovery, executor
65
+ - Cold start: scan Consul for MCP-enabled orchestrators
66
+ - Start Kafka subscription (if enabled)
67
+ 2. get_handler(): Create configured HandlerMCP
68
+ 3. shutdown(): Clean up all resources
69
+
70
+ Attributes:
71
+ _config: Server configuration.
72
+ _registry: Tool registry instance.
73
+ _discovery: Consul discovery service.
74
+ _sync: Kafka sync service (if enabled).
75
+ _executor: Tool execution adapter.
76
+ _started: Whether the lifecycle has been started.
77
+
78
+ Example:
79
+ >>> config = ModelMCPServerConfig(
80
+ ... consul_host="consul.local",
81
+ ... http_port=8090,
82
+ ... )
83
+ >>> lifecycle = MCPServerLifecycle(config)
84
+ >>> await lifecycle.start()
85
+ >>> handler = lifecycle.get_handler(container)
86
+ >>> # Use handler with uvicorn/transport
87
+ >>> await lifecycle.shutdown()
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ config: ModelMCPServerConfig,
93
+ bus: EventBusKafka | None = None,
94
+ ) -> None:
95
+ """Initialize the lifecycle manager.
96
+
97
+ Args:
98
+ config: Server configuration.
99
+ bus: Optional Kafka event bus for hot reload. If not provided,
100
+ Kafka subscription is skipped even if kafka_enabled=True.
101
+ """
102
+ self._config = config
103
+ self._bus = bus
104
+
105
+ # Services (initialized during start())
106
+ self._registry: ServiceMCPToolRegistry | None = None
107
+ self._discovery: ServiceMCPToolDiscovery | None = None
108
+ self._sync: ServiceMCPToolSync | None = None
109
+ self._executor: AdapterONEXToolExecution | None = None
110
+
111
+ # State
112
+ self._started = False
113
+
114
+ logger.debug(
115
+ "MCPServerLifecycle initialized",
116
+ extra={
117
+ "consul_host": config.consul_host,
118
+ "consul_port": config.consul_port,
119
+ "kafka_enabled": config.kafka_enabled,
120
+ "http_port": config.http_port,
121
+ },
122
+ )
123
+
124
+ @property
125
+ def is_running(self) -> bool:
126
+ """Return True if the lifecycle has been started."""
127
+ return self._started
128
+
129
+ @property
130
+ def registry(self) -> ServiceMCPToolRegistry | None:
131
+ """Return the tool registry (available after start())."""
132
+ return self._registry
133
+
134
+ @property
135
+ def executor(self) -> AdapterONEXToolExecution | None:
136
+ """Return the execution adapter (available after start())."""
137
+ return self._executor
138
+
139
+ async def start(self) -> None:
140
+ """Start all MCP server components.
141
+
142
+ This method performs the following steps:
143
+ 1. Create registry, discovery, and executor instances
144
+ 2. Cold start: discover all MCP-enabled tools from Consul
145
+ 3. Populate the registry with discovered tools
146
+ 4. Start Kafka subscription for hot reload (if enabled)
147
+
148
+ Raises:
149
+ RuntimeError: If already started.
150
+ """
151
+ if self._started:
152
+ logger.debug("MCPServerLifecycle already started")
153
+ return
154
+
155
+ correlation_id = uuid4()
156
+
157
+ logger.info(
158
+ "Starting MCP server lifecycle",
159
+ extra={"correlation_id": str(correlation_id)},
160
+ )
161
+
162
+ # Create services
163
+ self._registry = ServiceMCPToolRegistry()
164
+ self._executor = AdapterONEXToolExecution(
165
+ default_timeout=self._config.default_timeout,
166
+ )
167
+
168
+ # Dev mode: scan local contracts instead of Consul
169
+ if self._config.dev_mode:
170
+ logger.info(
171
+ "Dev mode: discovering tools from local contracts",
172
+ extra={
173
+ "contracts_dir": self._config.contracts_dir,
174
+ "correlation_id": str(correlation_id),
175
+ },
176
+ )
177
+ tools = await self._discover_from_contracts(correlation_id)
178
+ for tool in tools:
179
+ await self._registry.upsert_tool(tool, "dev-mode")
180
+ logger.info(
181
+ "Dev mode discovery complete",
182
+ extra={
183
+ "tool_count": len(tools),
184
+ "correlation_id": str(correlation_id),
185
+ },
186
+ )
187
+ else:
188
+ # Production mode: discover from Consul
189
+ self._discovery = ServiceMCPToolDiscovery(
190
+ consul_host=self._config.consul_host,
191
+ consul_port=self._config.consul_port,
192
+ consul_scheme=self._config.consul_scheme,
193
+ consul_token=self._config.consul_token,
194
+ )
195
+
196
+ logger.info(
197
+ "Cold start: discovering tools from Consul",
198
+ extra={"correlation_id": str(correlation_id)},
199
+ )
200
+
201
+ try:
202
+ tools = await self._discovery.discover_all()
203
+
204
+ # Populate registry
205
+ for tool in tools:
206
+ # Use "0" as event_id to ensure any future Kafka updates take precedence
207
+ # (numeric offsets like "1", "2" sort after "0" alphabetically)
208
+ await self._registry.upsert_tool(tool, "0")
209
+
210
+ logger.info(
211
+ "Cold start complete",
212
+ extra={
213
+ "tool_count": len(tools),
214
+ "correlation_id": str(correlation_id),
215
+ },
216
+ )
217
+
218
+ except Exception as e:
219
+ logger.warning(
220
+ "Cold start discovery failed - continuing with empty registry",
221
+ extra={
222
+ "error": str(e),
223
+ "correlation_id": str(correlation_id),
224
+ },
225
+ )
226
+
227
+ # Start Kafka sync (if enabled, bus provided, and not in dev mode)
228
+ # Dev mode doesn't use Consul discovery, so Kafka sync is not applicable
229
+ if (
230
+ self._config.kafka_enabled
231
+ and self._bus is not None
232
+ and self._discovery is not None
233
+ ):
234
+ logger.info(
235
+ "Starting Kafka subscription for hot reload",
236
+ extra={"correlation_id": str(correlation_id)},
237
+ )
238
+
239
+ self._sync = ServiceMCPToolSync(
240
+ registry=self._registry,
241
+ discovery=self._discovery,
242
+ bus=self._bus,
243
+ )
244
+ await self._sync.start()
245
+
246
+ self._started = True
247
+
248
+ logger.info(
249
+ "MCP server lifecycle started",
250
+ extra={
251
+ "tool_count": self._registry.tool_count,
252
+ "kafka_enabled": self._config.kafka_enabled and self._bus is not None,
253
+ "correlation_id": str(correlation_id),
254
+ },
255
+ )
256
+
257
+ async def _discover_from_contracts(
258
+ self, correlation_id: UUID
259
+ ) -> list[ModelMCPToolDefinition]:
260
+ """Scan local contracts for MCP-enabled orchestrators (dev mode).
261
+
262
+ Args:
263
+ correlation_id: Correlation ID for logging.
264
+
265
+ Returns:
266
+ List of discovered tool definitions from local contracts.
267
+ """
268
+ from pathlib import Path
269
+
270
+ import yaml
271
+
272
+ from omnibase_infra.models.mcp.model_mcp_tool_definition import (
273
+ ModelMCPToolDefinition,
274
+ )
275
+
276
+ tools: list[ModelMCPToolDefinition] = []
277
+ contracts_dir = self._config.contracts_dir
278
+
279
+ if not contracts_dir:
280
+ logger.warning(
281
+ "Dev mode enabled but no contracts_dir specified",
282
+ extra={"correlation_id": str(correlation_id)},
283
+ )
284
+ return tools
285
+
286
+ contracts_path = Path(contracts_dir)
287
+ if not contracts_path.exists():
288
+ logger.warning(
289
+ "Contracts directory does not exist",
290
+ extra={
291
+ "contracts_dir": contracts_dir,
292
+ "correlation_id": str(correlation_id),
293
+ },
294
+ )
295
+ return tools
296
+
297
+ # Scan for contract.yaml files
298
+ for contract_file in contracts_path.rglob("contract.yaml"):
299
+ try:
300
+ with contract_file.open("r") as f:
301
+ contract = yaml.safe_load(f)
302
+
303
+ if not contract:
304
+ continue
305
+
306
+ # Check for MCP configuration
307
+ mcp_config = contract.get("mcp", {})
308
+ if not mcp_config.get("expose", False):
309
+ continue
310
+
311
+ # Only orchestrators can be exposed
312
+ node_type = contract.get("node_type", "")
313
+ if "ORCHESTRATOR" not in node_type:
314
+ logger.debug(
315
+ "Skipping non-orchestrator with mcp.expose",
316
+ extra={
317
+ "contract": str(contract_file),
318
+ "node_type": node_type,
319
+ "correlation_id": str(correlation_id),
320
+ },
321
+ )
322
+ continue
323
+
324
+ # Build tool definition
325
+ name = contract.get("name", contract_file.parent.name)
326
+ tool_name = mcp_config.get("tool_name", name)
327
+ description = mcp_config.get(
328
+ "description", contract.get("description", f"ONEX: {name}")
329
+ )
330
+ timeout = mcp_config.get("timeout_seconds", 30)
331
+ version = contract.get("node_version", "1.0.0")
332
+
333
+ tool = ModelMCPToolDefinition(
334
+ name=tool_name,
335
+ description=description,
336
+ version=version,
337
+ parameters=[], # Will be populated from input_model
338
+ input_schema={"type": "object", "properties": {}},
339
+ orchestrator_node_id=name,
340
+ orchestrator_service_id=None,
341
+ endpoint=None, # Local dev mode - no endpoint
342
+ timeout_seconds=timeout,
343
+ metadata={
344
+ "contract_path": str(contract_file),
345
+ "node_type": node_type,
346
+ "source": "local_contract",
347
+ },
348
+ )
349
+ tools.append(tool)
350
+
351
+ logger.info(
352
+ "Discovered MCP tool from contract",
353
+ extra={
354
+ "tool_name": tool_name,
355
+ "contract": str(contract_file),
356
+ "correlation_id": str(correlation_id),
357
+ },
358
+ )
359
+
360
+ except yaml.YAMLError as e:
361
+ logger.warning(
362
+ "Failed to parse contract YAML",
363
+ extra={
364
+ "contract": str(contract_file),
365
+ "error": str(e),
366
+ "correlation_id": str(correlation_id),
367
+ },
368
+ )
369
+ except Exception as e:
370
+ logger.warning(
371
+ "Error processing contract",
372
+ extra={
373
+ "contract": str(contract_file),
374
+ "error": str(e),
375
+ "correlation_id": str(correlation_id),
376
+ },
377
+ )
378
+
379
+ return tools
380
+
381
+ async def shutdown(self) -> None:
382
+ """Shutdown all MCP server components.
383
+
384
+ This method performs graceful cleanup:
385
+ 1. Stop Kafka subscription
386
+ 2. Clear registry
387
+ 3. Close executor HTTP client
388
+
389
+ Safe to call multiple times.
390
+ """
391
+ if not self._started:
392
+ logger.debug("MCPServerLifecycle already stopped")
393
+ return
394
+
395
+ correlation_id = uuid4()
396
+
397
+ logger.info(
398
+ "Shutting down MCP server lifecycle",
399
+ extra={"correlation_id": str(correlation_id)},
400
+ )
401
+
402
+ # Stop Kafka sync
403
+ if self._sync is not None:
404
+ await self._sync.stop()
405
+ self._sync = None
406
+
407
+ # Clear registry
408
+ if self._registry is not None:
409
+ await self._registry.clear()
410
+ self._registry = None
411
+
412
+ # Close executor
413
+ if self._executor is not None:
414
+ await self._executor.close()
415
+ self._executor = None
416
+
417
+ # Clear discovery (stateless, no cleanup needed)
418
+ self._discovery = None
419
+
420
+ self._started = False
421
+
422
+ logger.info(
423
+ "MCP server lifecycle shutdown complete",
424
+ extra={"correlation_id": str(correlation_id)},
425
+ )
426
+
427
+ def describe(self) -> dict[str, object]:
428
+ """Return lifecycle metadata for observability."""
429
+ return {
430
+ "service_name": "MCPServerLifecycle",
431
+ "started": self._started,
432
+ "config": {
433
+ "consul_host": self._config.consul_host,
434
+ "consul_port": self._config.consul_port,
435
+ "kafka_enabled": self._config.kafka_enabled,
436
+ "http_port": self._config.http_port,
437
+ },
438
+ "registry_tool_count": (self._registry.tool_count if self._registry else 0),
439
+ "sync_running": self._sync.is_running if self._sync else False,
440
+ }
441
+
442
+
443
+ __all__ = ["MCPServerLifecycle", "ModelMCPServerConfig"]