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.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/adapters/adapter_onex_tool_execution.py +446 -0
- omnibase_infra/cli/commands.py +1 -1
- omnibase_infra/configs/widget_mapping.yaml +176 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +4 -1
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +4 -1
- omnibase_infra/errors/error_compute_registry.py +4 -1
- omnibase_infra/errors/error_event_bus_registry.py +4 -1
- omnibase_infra/errors/error_infra.py +3 -1
- omnibase_infra/errors/error_policy_registry.py +4 -1
- omnibase_infra/handlers/handler_db.py +2 -1
- omnibase_infra/handlers/handler_graph.py +10 -5
- omnibase_infra/handlers/handler_mcp.py +736 -63
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
- omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +301 -4
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
- omnibase_infra/mixins/mixin_node_introspection.py +24 -7
- omnibase_infra/mixins/mixin_retry_execution.py +1 -1
- omnibase_infra/models/handlers/__init__.py +10 -0
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
- omnibase_infra/models/mcp/__init__.py +15 -0
- omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
- omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
- omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
- omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
- omnibase_infra/models/registration/model_node_capabilities.py +11 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
- omnibase_infra/nodes/effects/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +46 -25
- omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +24 -19
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
- omnibase_infra/plugins/plugin_compute_base.py +16 -2
- omnibase_infra/protocols/protocol_event_projector.py +1 -1
- omnibase_infra/runtime/__init__.py +51 -1
- omnibase_infra/runtime/binding_config_resolver.py +102 -37
- omnibase_infra/runtime/constants_notification.py +75 -0
- omnibase_infra/runtime/contract_handler_discovery.py +6 -1
- omnibase_infra/runtime/handler_bootstrap_source.py +514 -0
- omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
- omnibase_infra/runtime/handler_contract_source.py +289 -167
- omnibase_infra/runtime/handler_plugin_loader.py +4 -2
- omnibase_infra/runtime/mixin_semver_cache.py +25 -1
- omnibase_infra/runtime/mixins/__init__.py +7 -0
- omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
- omnibase_infra/runtime/models/__init__.py +24 -0
- omnibase_infra/runtime/models/model_health_check_result.py +2 -1
- omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
- omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1 -1
- omnibase_infra/runtime/projector_shell.py +229 -1
- omnibase_infra/runtime/protocols/__init__.py +10 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +3 -2
- omnibase_infra/runtime/registry_policy.py +9 -326
- omnibase_infra/runtime/secret_resolver.py +4 -2
- omnibase_infra/runtime/service_kernel.py +10 -2
- omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
- omnibase_infra/runtime/service_runtime_host_process.py +225 -15
- omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
- omnibase_infra/runtime/transition_notification_publisher.py +764 -0
- omnibase_infra/runtime/util_container_wiring.py +6 -5
- omnibase_infra/runtime/util_wiring.py +5 -1
- omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
- omnibase_infra/services/mcp/__init__.py +31 -0
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +443 -0
- omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
- omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
- omnibase_infra/services/registry_api/__init__.py +40 -0
- omnibase_infra/services/registry_api/main.py +243 -0
- omnibase_infra/services/registry_api/models/__init__.py +66 -0
- omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
- omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
- omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
- omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
- omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
- omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
- omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
- omnibase_infra/services/registry_api/models/model_warning.py +49 -0
- omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
- omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
- omnibase_infra/services/registry_api/routes.py +371 -0
- omnibase_infra/services/registry_api/service.py +846 -0
- omnibase_infra/services/service_capability_query.py +4 -4
- omnibase_infra/services/service_health.py +3 -2
- omnibase_infra/services/service_timeout_emitter.py +13 -2
- omnibase_infra/utils/util_dsn_validation.py +1 -1
- omnibase_infra/validation/__init__.py +3 -19
- omnibase_infra/validation/contracts/security.validation.yaml +114 -0
- omnibase_infra/validation/infra_validators.py +35 -24
- omnibase_infra/validation/validation_exemptions.yaml +113 -9
- omnibase_infra/validation/validator_chain_propagation.py +2 -2
- omnibase_infra/validation/validator_runtime_shape.py +1 -1
- omnibase_infra/validation/validator_security.py +473 -370
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/METADATA +2 -2
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/RECORD +116 -74
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/entry_points.txt +0 -0
- {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"]
|