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