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.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
- omnibase_infra/capabilities/__init__.py +15 -0
- omnibase_infra/capabilities/capability_inference_rules.py +211 -0
- omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
- omnibase_infra/capabilities/intent_type_extractor.py +160 -0
- omnibase_infra/cli/commands.py +1 -1
- omnibase_infra/configs/widget_mapping.yaml +176 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +5 -2
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +5 -2
- omnibase_infra/enums/__init__.py +6 -0
- omnibase_infra/enums/enum_handler_error_type.py +10 -0
- omnibase_infra/enums/enum_handler_source_mode.py +72 -0
- omnibase_infra/enums/enum_kafka_acks.py +99 -0
- 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/event_bus/event_bus_kafka.py +1 -1
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +59 -10
- omnibase_infra/handlers/__init__.py +8 -1
- omnibase_infra/handlers/handler_consul.py +7 -1
- omnibase_infra/handlers/handler_db.py +10 -3
- omnibase_infra/handlers/handler_graph.py +10 -5
- omnibase_infra/handlers/handler_http.py +8 -2
- omnibase_infra/handlers/handler_intent.py +387 -0
- omnibase_infra/handlers/handler_mcp.py +745 -63
- omnibase_infra/handlers/handler_vault.py +11 -5
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
- omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +7 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +308 -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 +42 -7
- omnibase_infra/mixins/mixin_retry_execution.py +1 -1
- omnibase_infra/models/discovery/model_introspection_config.py +11 -0
- omnibase_infra/models/handlers/__init__.py +48 -5
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
- omnibase_infra/models/handlers/model_contract_discovery_result.py +6 -4
- omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
- omnibase_infra/models/handlers/model_handler_source_config.py +220 -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/models/registration/model_node_introspection_event.py +9 -0
- omnibase_infra/models/runtime/model_handler_contract.py +25 -9
- omnibase_infra/models/runtime/model_loaded_handler.py +9 -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/plugin.py +1 -1
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +4 -3
- 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/node.py +4 -1
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +47 -26
- 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 +28 -20
- 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/__init__.py +2 -0
- omnibase_infra/protocols/protocol_container_aware.py +200 -0
- omnibase_infra/protocols/protocol_event_projector.py +1 -1
- omnibase_infra/runtime/__init__.py +90 -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 +507 -0
- omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
- omnibase_infra/runtime/handler_contract_source.py +267 -186
- omnibase_infra/runtime/handler_identity.py +81 -0
- omnibase_infra/runtime/handler_plugin_loader.py +19 -2
- omnibase_infra/runtime/handler_registry.py +11 -3
- omnibase_infra/runtime/handler_source_resolver.py +326 -0
- 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/protocol_lifecycle_executor.py +6 -6
- omnibase_infra/runtime/protocols/__init__.py +10 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +16 -15
- omnibase_infra/runtime/registry_contract_source.py +693 -0
- omnibase_infra/runtime/registry_policy.py +9 -326
- omnibase_infra/runtime/secret_resolver.py +4 -2
- omnibase_infra/runtime/service_kernel.py +11 -3
- omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
- omnibase_infra/runtime/service_runtime_host_process.py +589 -106
- 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 +17 -4
- omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
- omnibase_infra/services/__init__.py +21 -0
- omnibase_infra/services/corpus_capture.py +7 -1
- omnibase_infra/services/mcp/__init__.py +31 -0
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -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 +261 -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 +837 -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 +20 -3
- omnibase_infra/services/service_timeout_scanner.py +7 -3
- omnibase_infra/services/session/__init__.py +56 -0
- omnibase_infra/services/session/config_consumer.py +120 -0
- omnibase_infra/services/session/config_store.py +139 -0
- omnibase_infra/services/session/consumer.py +1007 -0
- omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
- omnibase_infra/services/session/store.py +997 -0
- omnibase_infra/utils/__init__.py +19 -0
- omnibase_infra/utils/util_atomic_file.py +261 -0
- omnibase_infra/utils/util_db_transaction.py +239 -0
- omnibase_infra/utils/util_dsn_validation.py +1 -1
- omnibase_infra/utils/util_retry_optimistic.py +281 -0
- 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 +140 -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.3.dist-info}/METADATA +3 -3
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/RECORD +161 -98
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""MCP Tool Sync Service - Kafka listener for hot reload with idempotency.
|
|
4
|
+
|
|
5
|
+
This service subscribes to node registration events on Kafka and updates
|
|
6
|
+
the MCP tool registry in real-time. It supports:
|
|
7
|
+
- Hot reload: New/updated orchestrators appear as tools without restart
|
|
8
|
+
- Deregistration: Removed orchestrators are removed from tool registry
|
|
9
|
+
- Idempotency: Duplicate/out-of-order events are handled correctly
|
|
10
|
+
|
|
11
|
+
Event Topic: node.registration.v1
|
|
12
|
+
Event Types:
|
|
13
|
+
- registered: New node registered → upsert tool
|
|
14
|
+
- updated: Node updated → upsert tool
|
|
15
|
+
- deregistered: Node deregistered → remove tool
|
|
16
|
+
- expired: Node liveness expired → remove tool
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
from uuid import uuid4
|
|
26
|
+
|
|
27
|
+
from omnibase_core.container import ModelONEXContainer
|
|
28
|
+
from omnibase_core.types import JsonType
|
|
29
|
+
from omnibase_infra.models.mcp.model_mcp_tool_definition import (
|
|
30
|
+
ModelMCPToolDefinition,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
|
|
35
|
+
from omnibase_infra.event_bus.models import ModelEventMessage
|
|
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
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ServiceMCPToolSync:
|
|
47
|
+
"""Kafka listener for MCP tool hot reload with idempotency.
|
|
48
|
+
|
|
49
|
+
This service subscribes to node registration events and updates the
|
|
50
|
+
tool registry accordingly. It handles:
|
|
51
|
+
- registered/updated events → upsert tool in registry
|
|
52
|
+
- deregistered/expired events → remove tool from registry
|
|
53
|
+
|
|
54
|
+
Idempotency:
|
|
55
|
+
Uses event_id (from event payload or Kafka offset) to ensure
|
|
56
|
+
out-of-order and duplicate events are handled correctly.
|
|
57
|
+
|
|
58
|
+
Consul Fallback:
|
|
59
|
+
When registration events don't contain full contract info,
|
|
60
|
+
the service falls back to Consul discovery to re-fetch the
|
|
61
|
+
tool definition.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
_registry: Tool registry for storing tool definitions.
|
|
65
|
+
_discovery: Consul discovery service for fallback lookups.
|
|
66
|
+
_bus: Kafka event bus for subscriptions.
|
|
67
|
+
_unsubscribe: Callback to unsubscribe from topic.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> from omnibase_core.container import ModelONEXContainer
|
|
71
|
+
>>> container = ModelONEXContainer()
|
|
72
|
+
>>> # Ensure services are registered in container first
|
|
73
|
+
>>> sync = ServiceMCPToolSync(container)
|
|
74
|
+
>>> await sync.start()
|
|
75
|
+
>>> # ... process events ...
|
|
76
|
+
>>> await sync.stop()
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
# Topic for node registration events
|
|
80
|
+
TOPIC = "node.registration.v1"
|
|
81
|
+
GROUP_ID = "mcp-tool-sync"
|
|
82
|
+
|
|
83
|
+
# MCP tag constants
|
|
84
|
+
TAG_MCP_ENABLED = "mcp-enabled"
|
|
85
|
+
TAG_NODE_TYPE_ORCHESTRATOR = "node-type:orchestrator"
|
|
86
|
+
TAG_PREFIX_MCP_TOOL = "mcp-tool:"
|
|
87
|
+
|
|
88
|
+
# Event types
|
|
89
|
+
EVENT_TYPE_REGISTERED = "registered"
|
|
90
|
+
EVENT_TYPE_UPDATED = "updated"
|
|
91
|
+
EVENT_TYPE_DEREGISTERED = "deregistered"
|
|
92
|
+
EVENT_TYPE_EXPIRED = "expired"
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
container: ModelONEXContainer | None = None,
|
|
97
|
+
*,
|
|
98
|
+
registry: ServiceMCPToolRegistry | None = None,
|
|
99
|
+
discovery: ServiceMCPToolDiscovery | None = None,
|
|
100
|
+
bus: EventBusKafka | None = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Initialize the sync service.
|
|
103
|
+
|
|
104
|
+
Supports two initialization patterns:
|
|
105
|
+
1. Container-based DI: Pass a ModelONEXContainer to resolve dependencies
|
|
106
|
+
2. Direct injection: Pass registry, discovery, and bus directly
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
container: Optional ONEX container for dependency injection.
|
|
110
|
+
registry: Tool registry (used if container not provided)
|
|
111
|
+
discovery: Discovery service for Consul fallback (used if container not provided)
|
|
112
|
+
bus: Kafka event bus (used if container not provided)
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ValueError: If neither container nor all direct dependencies are provided.
|
|
116
|
+
"""
|
|
117
|
+
from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
|
|
118
|
+
from omnibase_infra.services.mcp.service_mcp_tool_discovery import (
|
|
119
|
+
ServiceMCPToolDiscovery,
|
|
120
|
+
)
|
|
121
|
+
from omnibase_infra.services.mcp.service_mcp_tool_registry import (
|
|
122
|
+
ServiceMCPToolRegistry,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self._container = container
|
|
126
|
+
|
|
127
|
+
if container is not None:
|
|
128
|
+
# Resolve from container
|
|
129
|
+
self._registry: ServiceMCPToolRegistry = container.get_service(
|
|
130
|
+
ServiceMCPToolRegistry
|
|
131
|
+
)
|
|
132
|
+
self._discovery: ServiceMCPToolDiscovery = container.get_service(
|
|
133
|
+
ServiceMCPToolDiscovery
|
|
134
|
+
)
|
|
135
|
+
self._bus: EventBusKafka = container.get_service(EventBusKafka)
|
|
136
|
+
elif registry is not None and discovery is not None and bus is not None:
|
|
137
|
+
# Use directly provided dependencies
|
|
138
|
+
self._registry = registry
|
|
139
|
+
self._discovery = discovery
|
|
140
|
+
self._bus = bus
|
|
141
|
+
else:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
"Must provide either container or all of: registry, discovery, bus"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
self._unsubscribe: Callable[[], Awaitable[None]] | None = None
|
|
147
|
+
self._started = False
|
|
148
|
+
|
|
149
|
+
logger.debug(
|
|
150
|
+
"ServiceMCPToolSync initialized",
|
|
151
|
+
extra={
|
|
152
|
+
"topic": self.TOPIC,
|
|
153
|
+
"group_id": self.GROUP_ID,
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def is_running(self) -> bool:
|
|
159
|
+
"""Return True if the sync service is running."""
|
|
160
|
+
return self._started
|
|
161
|
+
|
|
162
|
+
async def start(self) -> None:
|
|
163
|
+
"""Start the Kafka subscription for hot reload.
|
|
164
|
+
|
|
165
|
+
Subscribes to the node registration topic and begins processing
|
|
166
|
+
events. The subscription is idempotent - calling start() multiple
|
|
167
|
+
times has no effect.
|
|
168
|
+
"""
|
|
169
|
+
if self._started:
|
|
170
|
+
logger.debug("ServiceMCPToolSync already started")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
correlation_id = uuid4()
|
|
174
|
+
|
|
175
|
+
logger.info(
|
|
176
|
+
"Starting MCP tool sync",
|
|
177
|
+
extra={
|
|
178
|
+
"topic": self.TOPIC,
|
|
179
|
+
"group_id": self.GROUP_ID,
|
|
180
|
+
"correlation_id": str(correlation_id),
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Subscribe to registration events
|
|
185
|
+
self._unsubscribe = await self._bus.subscribe(
|
|
186
|
+
topic=self.TOPIC,
|
|
187
|
+
group_id=self.GROUP_ID,
|
|
188
|
+
on_message=self._on_message,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
self._started = True
|
|
192
|
+
|
|
193
|
+
logger.info(
|
|
194
|
+
"MCP tool sync started",
|
|
195
|
+
extra={
|
|
196
|
+
"topic": self.TOPIC,
|
|
197
|
+
"correlation_id": str(correlation_id),
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
async def stop(self) -> None:
|
|
202
|
+
"""Stop the Kafka subscription.
|
|
203
|
+
|
|
204
|
+
Unsubscribes from the topic and stops processing events.
|
|
205
|
+
Idempotent - safe to call multiple times.
|
|
206
|
+
"""
|
|
207
|
+
if not self._started:
|
|
208
|
+
logger.debug("ServiceMCPToolSync already stopped")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
correlation_id = uuid4()
|
|
212
|
+
|
|
213
|
+
logger.info(
|
|
214
|
+
"Stopping MCP tool sync",
|
|
215
|
+
extra={"correlation_id": str(correlation_id)},
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if self._unsubscribe is not None:
|
|
219
|
+
await self._unsubscribe()
|
|
220
|
+
self._unsubscribe = None
|
|
221
|
+
|
|
222
|
+
self._started = False
|
|
223
|
+
|
|
224
|
+
logger.info(
|
|
225
|
+
"MCP tool sync stopped",
|
|
226
|
+
extra={"correlation_id": str(correlation_id)},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
async def _on_message(self, msg: ModelEventMessage) -> None:
|
|
230
|
+
"""Process a registration event message.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
msg: Event message from Kafka.
|
|
234
|
+
"""
|
|
235
|
+
correlation_id = msg.headers.correlation_id
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
# Parse event payload
|
|
239
|
+
event = self._parse_event(msg)
|
|
240
|
+
if event is None:
|
|
241
|
+
logger.debug(
|
|
242
|
+
"Skipping non-JSON message",
|
|
243
|
+
extra={"correlation_id": str(correlation_id)},
|
|
244
|
+
)
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
# Extract event metadata
|
|
248
|
+
event_type = event.get("event_type", "")
|
|
249
|
+
tags_raw = event.get("tags", [])
|
|
250
|
+
# Ensure tags is a list of strings for type safety
|
|
251
|
+
tags: list[str] = (
|
|
252
|
+
[str(t) for t in tags_raw]
|
|
253
|
+
if isinstance(tags_raw, (list, tuple))
|
|
254
|
+
else []
|
|
255
|
+
)
|
|
256
|
+
node_id = event.get("node_id")
|
|
257
|
+
service_id = event.get("service_id")
|
|
258
|
+
|
|
259
|
+
# Use event_id from payload or fall back to Kafka offset
|
|
260
|
+
# Numeric offsets are zero-padded to 20 digits for lexicographic ordering
|
|
261
|
+
event_id_raw = event.get("event_id") or msg.offset
|
|
262
|
+
if event_id_raw is None:
|
|
263
|
+
event_id = str(uuid4())
|
|
264
|
+
else:
|
|
265
|
+
event_id_str = str(event_id_raw)
|
|
266
|
+
event_id = (
|
|
267
|
+
event_id_str.zfill(20) if event_id_str.isdigit() else event_id_str
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Check if this is an MCP-enabled orchestrator
|
|
271
|
+
if not self._is_mcp_orchestrator(tags):
|
|
272
|
+
logger.debug(
|
|
273
|
+
"Ignoring non-MCP event",
|
|
274
|
+
extra={
|
|
275
|
+
"event_type": event_type,
|
|
276
|
+
"tags": tags,
|
|
277
|
+
"correlation_id": str(correlation_id),
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
# Route to appropriate handler
|
|
283
|
+
if event_type in (self.EVENT_TYPE_REGISTERED, self.EVENT_TYPE_UPDATED):
|
|
284
|
+
await self._handle_upsert_event(event, event_id, correlation_id)
|
|
285
|
+
elif event_type in (self.EVENT_TYPE_DEREGISTERED, self.EVENT_TYPE_EXPIRED):
|
|
286
|
+
await self._handle_remove_event(tags, event_id, correlation_id)
|
|
287
|
+
else:
|
|
288
|
+
logger.debug(
|
|
289
|
+
"Unknown event type",
|
|
290
|
+
extra={
|
|
291
|
+
"event_type": event_type,
|
|
292
|
+
"correlation_id": str(correlation_id),
|
|
293
|
+
},
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.exception(
|
|
298
|
+
"Error processing registration event",
|
|
299
|
+
extra={
|
|
300
|
+
"error": str(e),
|
|
301
|
+
"correlation_id": str(correlation_id),
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def _parse_event(self, msg: ModelEventMessage) -> dict[str, JsonType] | None:
|
|
306
|
+
"""Parse event payload from message.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
msg: Event message.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Parsed event dict or None if not JSON.
|
|
313
|
+
"""
|
|
314
|
+
if msg.value is None:
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
value = msg.value
|
|
319
|
+
if isinstance(value, bytes):
|
|
320
|
+
value_str = value.decode("utf-8")
|
|
321
|
+
elif isinstance(value, str):
|
|
322
|
+
value_str = value
|
|
323
|
+
else:
|
|
324
|
+
return None
|
|
325
|
+
parsed: dict[str, JsonType] = json.loads(value_str)
|
|
326
|
+
return parsed
|
|
327
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def _is_mcp_orchestrator(self, tags: Sequence[str]) -> bool:
|
|
331
|
+
"""Check if event is for an MCP-enabled orchestrator.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
tags: List of tags from the event.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
True if the event is for an MCP-enabled orchestrator.
|
|
338
|
+
"""
|
|
339
|
+
return self.TAG_MCP_ENABLED in tags and self.TAG_NODE_TYPE_ORCHESTRATOR in tags
|
|
340
|
+
|
|
341
|
+
def _extract_tool_name(self, tags: Sequence[str]) -> str | None:
|
|
342
|
+
"""Extract the MCP tool name from tags.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
tags: List of tags from the event.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
The tool name if found, None otherwise.
|
|
349
|
+
"""
|
|
350
|
+
for tag in tags:
|
|
351
|
+
if tag.startswith(self.TAG_PREFIX_MCP_TOOL):
|
|
352
|
+
return tag[len(self.TAG_PREFIX_MCP_TOOL) :]
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
def _extract_tags_list(self, tags_raw: object) -> list[str]:
|
|
356
|
+
"""Safely extract tags as a list of strings.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
tags_raw: Raw tags value from event (could be any type).
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
List of string tags, empty list if input is invalid.
|
|
363
|
+
"""
|
|
364
|
+
if isinstance(tags_raw, (list, tuple)):
|
|
365
|
+
return [str(t) for t in tags_raw]
|
|
366
|
+
return []
|
|
367
|
+
|
|
368
|
+
async def _handle_upsert_event(
|
|
369
|
+
self,
|
|
370
|
+
event: dict[str, JsonType],
|
|
371
|
+
event_id: str,
|
|
372
|
+
correlation_id: object,
|
|
373
|
+
) -> None:
|
|
374
|
+
"""Handle registered/updated events by upserting tool.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
event: Parsed event payload (JSON-compatible values).
|
|
378
|
+
event_id: Unique event identifier for idempotency.
|
|
379
|
+
correlation_id: Correlation ID for tracing.
|
|
380
|
+
"""
|
|
381
|
+
tags_raw = event.get("tags", [])
|
|
382
|
+
tags: list[str] = (
|
|
383
|
+
[str(t) for t in tags_raw] if isinstance(tags_raw, (list, tuple)) else []
|
|
384
|
+
)
|
|
385
|
+
tool_name = self._extract_tool_name(tags)
|
|
386
|
+
|
|
387
|
+
if not tool_name:
|
|
388
|
+
logger.warning(
|
|
389
|
+
"MCP event missing tool name tag",
|
|
390
|
+
extra={
|
|
391
|
+
"tags": tags,
|
|
392
|
+
"correlation_id": str(correlation_id),
|
|
393
|
+
},
|
|
394
|
+
)
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
# Try to build tool from event data
|
|
398
|
+
tool = self._build_tool_from_event(event, tool_name)
|
|
399
|
+
|
|
400
|
+
# Fallback: if event lacks full info, re-fetch from Consul
|
|
401
|
+
if tool is None:
|
|
402
|
+
service_id = event.get("service_id")
|
|
403
|
+
if service_id and isinstance(service_id, str):
|
|
404
|
+
logger.debug(
|
|
405
|
+
"Event lacks full info, falling back to Consul",
|
|
406
|
+
extra={
|
|
407
|
+
"service_id": service_id,
|
|
408
|
+
"correlation_id": str(correlation_id),
|
|
409
|
+
},
|
|
410
|
+
)
|
|
411
|
+
tool = await self._discovery.discover_by_service_id(service_id)
|
|
412
|
+
|
|
413
|
+
if tool is None:
|
|
414
|
+
logger.warning(
|
|
415
|
+
"Could not build tool definition from event or Consul",
|
|
416
|
+
extra={
|
|
417
|
+
"tool_name": tool_name,
|
|
418
|
+
"correlation_id": str(correlation_id),
|
|
419
|
+
},
|
|
420
|
+
)
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
# Upsert in registry
|
|
424
|
+
updated = await self._registry.upsert_tool(tool, event_id)
|
|
425
|
+
if updated:
|
|
426
|
+
logger.info(
|
|
427
|
+
"Tool upserted from event",
|
|
428
|
+
extra={
|
|
429
|
+
"tool_name": tool_name,
|
|
430
|
+
"event_id": event_id,
|
|
431
|
+
"correlation_id": str(correlation_id),
|
|
432
|
+
},
|
|
433
|
+
)
|
|
434
|
+
else:
|
|
435
|
+
logger.debug(
|
|
436
|
+
"Tool upsert skipped (stale event)",
|
|
437
|
+
extra={
|
|
438
|
+
"tool_name": tool_name,
|
|
439
|
+
"event_id": event_id,
|
|
440
|
+
"correlation_id": str(correlation_id),
|
|
441
|
+
},
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
async def _handle_remove_event(
|
|
445
|
+
self,
|
|
446
|
+
tags: Sequence[str],
|
|
447
|
+
event_id: str,
|
|
448
|
+
correlation_id: object,
|
|
449
|
+
) -> None:
|
|
450
|
+
"""Handle deregistered/expired events by removing tool.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
tags: Tags from the event (used to extract tool name).
|
|
454
|
+
event_id: Unique event identifier for idempotency.
|
|
455
|
+
correlation_id: Correlation ID for tracing.
|
|
456
|
+
"""
|
|
457
|
+
tool_name = self._extract_tool_name(tags)
|
|
458
|
+
if not tool_name:
|
|
459
|
+
logger.debug(
|
|
460
|
+
"Remove event missing tool name tag",
|
|
461
|
+
extra={
|
|
462
|
+
"tags": tags,
|
|
463
|
+
"correlation_id": str(correlation_id),
|
|
464
|
+
},
|
|
465
|
+
)
|
|
466
|
+
return
|
|
467
|
+
|
|
468
|
+
removed = await self._registry.remove_tool(tool_name, event_id)
|
|
469
|
+
if removed:
|
|
470
|
+
logger.info(
|
|
471
|
+
"Tool removed from registry",
|
|
472
|
+
extra={
|
|
473
|
+
"tool_name": tool_name,
|
|
474
|
+
"event_id": event_id,
|
|
475
|
+
"correlation_id": str(correlation_id),
|
|
476
|
+
},
|
|
477
|
+
)
|
|
478
|
+
else:
|
|
479
|
+
logger.debug(
|
|
480
|
+
"Tool removal skipped (stale event or not found)",
|
|
481
|
+
extra={
|
|
482
|
+
"tool_name": tool_name,
|
|
483
|
+
"event_id": event_id,
|
|
484
|
+
"correlation_id": str(correlation_id),
|
|
485
|
+
},
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
def _build_tool_from_event(
|
|
489
|
+
self,
|
|
490
|
+
event: dict[str, JsonType],
|
|
491
|
+
tool_name: str,
|
|
492
|
+
) -> ModelMCPToolDefinition | None:
|
|
493
|
+
"""Build a tool definition from event data.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
event: Parsed event payload (JSON-compatible values).
|
|
497
|
+
tool_name: Extracted tool name.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Tool definition if event contains enough info, None otherwise.
|
|
501
|
+
"""
|
|
502
|
+
# Check if event has the minimum required fields
|
|
503
|
+
service_name = event.get("service_name")
|
|
504
|
+
if not service_name:
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
# Extract optional fields
|
|
508
|
+
service_id = event.get("service_id")
|
|
509
|
+
node_id = event.get("node_id")
|
|
510
|
+
endpoint = event.get("endpoint")
|
|
511
|
+
description = event.get("description")
|
|
512
|
+
timeout_seconds = event.get("timeout_seconds", 30)
|
|
513
|
+
|
|
514
|
+
# Validate timeout
|
|
515
|
+
if not isinstance(timeout_seconds, int) or timeout_seconds < 1:
|
|
516
|
+
timeout_seconds = 30
|
|
517
|
+
|
|
518
|
+
return ModelMCPToolDefinition(
|
|
519
|
+
name=tool_name,
|
|
520
|
+
description=str(description)
|
|
521
|
+
if description
|
|
522
|
+
else f"ONEX orchestrator: {service_name}",
|
|
523
|
+
version="1.0.0",
|
|
524
|
+
parameters=[],
|
|
525
|
+
input_schema={"type": "object", "properties": {}},
|
|
526
|
+
orchestrator_node_id=str(node_id) if node_id else None,
|
|
527
|
+
orchestrator_service_id=str(service_id) if service_id else None,
|
|
528
|
+
endpoint=str(endpoint) if endpoint else None,
|
|
529
|
+
timeout_seconds=timeout_seconds,
|
|
530
|
+
metadata={
|
|
531
|
+
"service_name": str(service_name),
|
|
532
|
+
"tags": self._extract_tags_list(event.get("tags", [])),
|
|
533
|
+
"source": "kafka_event",
|
|
534
|
+
},
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
def describe(self) -> dict[str, object]:
|
|
538
|
+
"""Return service metadata for observability."""
|
|
539
|
+
return {
|
|
540
|
+
"service_name": "ServiceMCPToolSync",
|
|
541
|
+
"topic": self.TOPIC,
|
|
542
|
+
"group_id": self.GROUP_ID,
|
|
543
|
+
"is_running": self._started,
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
__all__ = ["ServiceMCPToolSync"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Registry API Service Module.
|
|
4
|
+
|
|
5
|
+
Provides a FastAPI-based HTTP API for registry discovery operations,
|
|
6
|
+
exposing node registrations and live Consul instances for dashboard
|
|
7
|
+
consumption.
|
|
8
|
+
|
|
9
|
+
This module bridges the existing ProjectionReaderRegistration and
|
|
10
|
+
HandlerServiceDiscoveryConsul services with a REST API layer.
|
|
11
|
+
|
|
12
|
+
Related Tickets:
|
|
13
|
+
- OMN-1278: Contract-Driven Dashboard - Registry Discovery
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from omnibase_infra.services.registry_api.main import create_app
|
|
17
|
+
from omnibase_infra.services.registry_api.models import (
|
|
18
|
+
ModelPaginationInfo,
|
|
19
|
+
ModelRegistryDiscoveryResponse,
|
|
20
|
+
ModelRegistryHealthResponse,
|
|
21
|
+
ModelRegistryInstanceView,
|
|
22
|
+
ModelRegistryNodeView,
|
|
23
|
+
ModelRegistrySummary,
|
|
24
|
+
ModelWarning,
|
|
25
|
+
ModelWidgetMapping,
|
|
26
|
+
)
|
|
27
|
+
from omnibase_infra.services.registry_api.service import ServiceRegistryDiscovery
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"create_app",
|
|
31
|
+
"ModelPaginationInfo",
|
|
32
|
+
"ModelRegistryDiscoveryResponse",
|
|
33
|
+
"ModelRegistryHealthResponse",
|
|
34
|
+
"ModelRegistryInstanceView",
|
|
35
|
+
"ModelRegistryNodeView",
|
|
36
|
+
"ModelRegistrySummary",
|
|
37
|
+
"ModelWarning",
|
|
38
|
+
"ModelWidgetMapping",
|
|
39
|
+
"ServiceRegistryDiscovery",
|
|
40
|
+
]
|