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