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,411 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""MCP Tool Discovery Service - Discovers MCP-enabled orchestrators from Consul.
|
|
4
|
+
|
|
5
|
+
This service scans Consul for services with MCP tags and converts them to
|
|
6
|
+
MCP tool definitions. It supports both cold start discovery (scan all) and
|
|
7
|
+
incremental discovery (single node lookup).
|
|
8
|
+
|
|
9
|
+
Discovery Flow:
|
|
10
|
+
1. Query Consul catalog for services with tags: mcp-enabled, node-type:orchestrator
|
|
11
|
+
2. For each service, extract mcp-tool:{name} tag for stable tool naming
|
|
12
|
+
3. Load contract metadata from service or fall back to defaults
|
|
13
|
+
4. Generate JSON Schema from Pydantic input model (if available)
|
|
14
|
+
5. Return ModelMCPToolDefinition instances
|
|
15
|
+
|
|
16
|
+
Tag Schema:
|
|
17
|
+
- mcp-enabled: Indicates the service is MCP-enabled
|
|
18
|
+
- mcp-tool:{name}: The stable tool name for MCP invocation
|
|
19
|
+
- node-type:orchestrator: Required for MCP enablement (non-orchestrators ignored)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import logging
|
|
26
|
+
from collections.abc import Mapping, Sequence
|
|
27
|
+
from urllib.parse import urlparse
|
|
28
|
+
from uuid import uuid4
|
|
29
|
+
|
|
30
|
+
import consul
|
|
31
|
+
import requests
|
|
32
|
+
|
|
33
|
+
from omnibase_core.models.container.model_onex_container import ModelONEXContainer
|
|
34
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
35
|
+
from omnibase_infra.errors import (
|
|
36
|
+
InfraConnectionError,
|
|
37
|
+
ModelInfraErrorContext,
|
|
38
|
+
)
|
|
39
|
+
from omnibase_infra.models.mcp.model_mcp_tool_definition import ModelMCPToolDefinition
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ServiceMCPToolDiscovery:
|
|
45
|
+
"""Discovers MCP-enabled orchestrators from Consul.
|
|
46
|
+
|
|
47
|
+
This service provides two main discovery methods:
|
|
48
|
+
1. discover_all(): Cold start scan for all MCP-enabled orchestrators
|
|
49
|
+
2. discover_by_service_id(): Re-fetch single service (Kafka fallback)
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
_container: The ONEX dependency injection container.
|
|
53
|
+
_consul_host: Consul server hostname.
|
|
54
|
+
_consul_port: Consul server port.
|
|
55
|
+
_consul_scheme: HTTP scheme (http/https).
|
|
56
|
+
_consul_token: Optional ACL token for authentication.
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
>>> from omnibase_core.models.container.model_onex_container import (
|
|
60
|
+
... create_model_onex_container,
|
|
61
|
+
... )
|
|
62
|
+
>>> container = await create_model_onex_container()
|
|
63
|
+
>>> discovery = ServiceMCPToolDiscovery(container)
|
|
64
|
+
>>> tools = await discovery.discover_all()
|
|
65
|
+
>>> for tool in tools:
|
|
66
|
+
... print(f"{tool.name}: {tool.description}")
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Tag constants for MCP discovery
|
|
70
|
+
TAG_MCP_ENABLED = "mcp-enabled"
|
|
71
|
+
TAG_NODE_TYPE_ORCHESTRATOR = "node-type:orchestrator"
|
|
72
|
+
TAG_PREFIX_MCP_TOOL = "mcp-tool:"
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
container: ModelONEXContainer | None = None,
|
|
77
|
+
*,
|
|
78
|
+
consul_host: str = "localhost",
|
|
79
|
+
consul_port: int = 8500,
|
|
80
|
+
consul_scheme: str = "http",
|
|
81
|
+
consul_token: str | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Initialize the discovery service.
|
|
84
|
+
|
|
85
|
+
Supports two initialization patterns:
|
|
86
|
+
1. Container-based DI: Pass a ModelONEXContainer to resolve config
|
|
87
|
+
2. Direct injection: Pass individual Consul parameters
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
container: Optional ONEX dependency injection container. If provided,
|
|
91
|
+
Consul configuration is resolved from container.config.
|
|
92
|
+
consul_host: Consul host (used if container not provided)
|
|
93
|
+
consul_port: Consul port (used if container not provided)
|
|
94
|
+
consul_scheme: Consul scheme (used if container not provided)
|
|
95
|
+
consul_token: Consul ACL token (used if container not provided)
|
|
96
|
+
"""
|
|
97
|
+
self._container = container
|
|
98
|
+
|
|
99
|
+
if container is not None:
|
|
100
|
+
# Resolve Consul configuration from container
|
|
101
|
+
# Configuration() returns the underlying dict or Mapping
|
|
102
|
+
config_data = container.config()
|
|
103
|
+
|
|
104
|
+
# Handle both dict and Mapping types (e.g., MappingProxyType, ChainMap)
|
|
105
|
+
if isinstance(config_data, Mapping):
|
|
106
|
+
consul_config_raw = config_data.get("consul", {})
|
|
107
|
+
# consul_config may also be a Mapping, convert to dict for consistency
|
|
108
|
+
if isinstance(consul_config_raw, Mapping):
|
|
109
|
+
consul_config: dict[str, object] = dict(consul_config_raw)
|
|
110
|
+
else:
|
|
111
|
+
logger.warning(
|
|
112
|
+
"Unexpected consul config type, expected Mapping",
|
|
113
|
+
extra={
|
|
114
|
+
"config_type": type(consul_config_raw).__name__,
|
|
115
|
+
"config_value": repr(consul_config_raw)[:100],
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
consul_config = {}
|
|
119
|
+
else:
|
|
120
|
+
logger.warning(
|
|
121
|
+
"Unexpected config_data type from container.config(), "
|
|
122
|
+
"Consul settings may be dropped",
|
|
123
|
+
extra={
|
|
124
|
+
"config_type": type(config_data).__name__,
|
|
125
|
+
"expected_types": "dict or Mapping",
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
consul_config = {}
|
|
129
|
+
|
|
130
|
+
agent_url_raw = consul_config.get("agent_url", "http://localhost:8500")
|
|
131
|
+
agent_url = str(agent_url_raw) if agent_url_raw else "http://localhost:8500"
|
|
132
|
+
|
|
133
|
+
# Parse the agent_url to extract host, port, and scheme
|
|
134
|
+
parsed = urlparse(agent_url)
|
|
135
|
+
self._consul_host = parsed.hostname or "localhost"
|
|
136
|
+
self._consul_port = parsed.port or 8500
|
|
137
|
+
self._consul_scheme = parsed.scheme or "http"
|
|
138
|
+
token_raw = consul_config.get("token")
|
|
139
|
+
self._consul_token = str(token_raw) if token_raw is not None else None
|
|
140
|
+
else:
|
|
141
|
+
# Use directly provided parameters
|
|
142
|
+
self._consul_host = consul_host
|
|
143
|
+
self._consul_port = consul_port
|
|
144
|
+
self._consul_scheme = consul_scheme
|
|
145
|
+
self._consul_token = consul_token
|
|
146
|
+
|
|
147
|
+
logger.debug(
|
|
148
|
+
"ServiceMCPToolDiscovery initialized",
|
|
149
|
+
extra={
|
|
150
|
+
"consul_host": self._consul_host,
|
|
151
|
+
"consul_port": self._consul_port,
|
|
152
|
+
"consul_scheme": self._consul_scheme,
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _create_consul_client(self) -> consul.Consul:
|
|
157
|
+
"""Create a Consul client instance."""
|
|
158
|
+
return consul.Consul(
|
|
159
|
+
host=self._consul_host,
|
|
160
|
+
port=self._consul_port,
|
|
161
|
+
scheme=self._consul_scheme,
|
|
162
|
+
token=self._consul_token,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
async def discover_all(self) -> list[ModelMCPToolDefinition]:
|
|
166
|
+
"""Cold start: scan Consul for all MCP-enabled orchestrators.
|
|
167
|
+
|
|
168
|
+
This method queries Consul for all services with MCP tags and
|
|
169
|
+
converts them to tool definitions.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
List of discovered tool definitions.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
InfraConnectionError: If Consul connection fails.
|
|
176
|
+
"""
|
|
177
|
+
correlation_id = uuid4()
|
|
178
|
+
|
|
179
|
+
logger.info(
|
|
180
|
+
"Starting MCP tool discovery",
|
|
181
|
+
extra={"correlation_id": str(correlation_id)},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
client = self._create_consul_client()
|
|
186
|
+
|
|
187
|
+
# Get all services from catalog (blocking call wrapped with to_thread)
|
|
188
|
+
_, services = await asyncio.to_thread(client.catalog.services)
|
|
189
|
+
|
|
190
|
+
tools: list[ModelMCPToolDefinition] = []
|
|
191
|
+
|
|
192
|
+
for service_name, tags in services.items():
|
|
193
|
+
# Check if service is MCP-enabled orchestrator
|
|
194
|
+
if not self._is_mcp_orchestrator(tags):
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# Extract tool name from tags
|
|
198
|
+
tool_name = self._extract_tool_name(tags)
|
|
199
|
+
if not tool_name:
|
|
200
|
+
logger.warning(
|
|
201
|
+
"MCP-enabled service missing mcp-tool tag",
|
|
202
|
+
extra={
|
|
203
|
+
"service_name": service_name,
|
|
204
|
+
"tags": tags,
|
|
205
|
+
"correlation_id": str(correlation_id),
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
# Get service instances for endpoint info (blocking call wrapped with to_thread)
|
|
211
|
+
_, service_instances = await asyncio.to_thread(
|
|
212
|
+
client.health.service, service_name, passing=True
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Use first healthy instance for endpoint
|
|
216
|
+
endpoint = None
|
|
217
|
+
service_id = None
|
|
218
|
+
if service_instances:
|
|
219
|
+
instance = service_instances[0]
|
|
220
|
+
svc = instance.get("Service", {})
|
|
221
|
+
address = svc.get("Address") or instance.get("Node", {}).get(
|
|
222
|
+
"Address"
|
|
223
|
+
)
|
|
224
|
+
port = svc.get("Port")
|
|
225
|
+
service_id = svc.get("ID")
|
|
226
|
+
if address and port:
|
|
227
|
+
endpoint = f"http://{address}:{port}"
|
|
228
|
+
|
|
229
|
+
# Build tool definition
|
|
230
|
+
tool = ModelMCPToolDefinition(
|
|
231
|
+
name=tool_name,
|
|
232
|
+
description=f"ONEX orchestrator: {service_name}",
|
|
233
|
+
version="1.0.0",
|
|
234
|
+
parameters=[], # Will be populated from contract
|
|
235
|
+
input_schema={"type": "object", "properties": {}},
|
|
236
|
+
orchestrator_node_id=None, # Not available from Consul
|
|
237
|
+
orchestrator_service_id=service_id,
|
|
238
|
+
endpoint=endpoint,
|
|
239
|
+
timeout_seconds=30,
|
|
240
|
+
metadata={
|
|
241
|
+
"service_name": service_name,
|
|
242
|
+
"tags": list(tags),
|
|
243
|
+
"source": "consul_discovery",
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
tools.append(tool)
|
|
247
|
+
|
|
248
|
+
logger.info(
|
|
249
|
+
"Discovered MCP tool",
|
|
250
|
+
extra={
|
|
251
|
+
"tool_name": tool_name,
|
|
252
|
+
"service_name": service_name,
|
|
253
|
+
"endpoint": endpoint,
|
|
254
|
+
"correlation_id": str(correlation_id),
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
logger.info(
|
|
259
|
+
"MCP tool discovery complete",
|
|
260
|
+
extra={
|
|
261
|
+
"tool_count": len(tools),
|
|
262
|
+
"correlation_id": str(correlation_id),
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
return tools
|
|
267
|
+
|
|
268
|
+
except (consul.ConsulException, requests.exceptions.RequestException) as e:
|
|
269
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
270
|
+
correlation_id=correlation_id,
|
|
271
|
+
transport_type=EnumInfraTransportType.CONSUL,
|
|
272
|
+
operation="discover_all",
|
|
273
|
+
target_name="mcp_tool_discovery",
|
|
274
|
+
)
|
|
275
|
+
raise InfraConnectionError(
|
|
276
|
+
f"Failed to discover MCP tools from Consul: {e}",
|
|
277
|
+
context=ctx,
|
|
278
|
+
) from e
|
|
279
|
+
|
|
280
|
+
async def discover_by_service_id(
|
|
281
|
+
self, service_id: str
|
|
282
|
+
) -> ModelMCPToolDefinition | None:
|
|
283
|
+
"""Re-fetch single service (Kafka fallback when event lacks full data).
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
service_id: Consul service ID to look up.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Tool definition if found and MCP-enabled, None otherwise.
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
InfraConnectionError: If Consul connection fails.
|
|
293
|
+
"""
|
|
294
|
+
correlation_id = uuid4()
|
|
295
|
+
|
|
296
|
+
logger.debug(
|
|
297
|
+
"Looking up service by ID",
|
|
298
|
+
extra={
|
|
299
|
+
"service_id": service_id,
|
|
300
|
+
"correlation_id": str(correlation_id),
|
|
301
|
+
},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
client = self._create_consul_client()
|
|
306
|
+
|
|
307
|
+
# Get all services and find the one matching our service_id
|
|
308
|
+
# Note: Consul catalog doesn't directly support service_id lookup,
|
|
309
|
+
# so we need to iterate through service instances
|
|
310
|
+
# (blocking call wrapped with to_thread)
|
|
311
|
+
_, services = await asyncio.to_thread(client.catalog.services)
|
|
312
|
+
|
|
313
|
+
for service_name, tags in services.items():
|
|
314
|
+
if not self._is_mcp_orchestrator(tags):
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
# Get instances for this service (blocking call wrapped with to_thread)
|
|
318
|
+
_, instances = await asyncio.to_thread(
|
|
319
|
+
client.health.service, service_name, passing=True
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
for instance in instances:
|
|
323
|
+
svc = instance.get("Service", {})
|
|
324
|
+
if svc.get("ID") == service_id:
|
|
325
|
+
# Found the service
|
|
326
|
+
tool_name = self._extract_tool_name(tags)
|
|
327
|
+
if not tool_name:
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
address = svc.get("Address") or instance.get("Node", {}).get(
|
|
331
|
+
"Address"
|
|
332
|
+
)
|
|
333
|
+
port = svc.get("Port")
|
|
334
|
+
endpoint = (
|
|
335
|
+
f"http://{address}:{port}" if address and port else None
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
return ModelMCPToolDefinition(
|
|
339
|
+
name=tool_name,
|
|
340
|
+
description=f"ONEX orchestrator: {service_name}",
|
|
341
|
+
version="1.0.0",
|
|
342
|
+
parameters=[],
|
|
343
|
+
input_schema={"type": "object", "properties": {}},
|
|
344
|
+
orchestrator_node_id=None,
|
|
345
|
+
orchestrator_service_id=service_id,
|
|
346
|
+
endpoint=endpoint,
|
|
347
|
+
timeout_seconds=30,
|
|
348
|
+
metadata={
|
|
349
|
+
"service_name": service_name,
|
|
350
|
+
"tags": list(tags),
|
|
351
|
+
"source": "consul_discovery",
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
logger.debug(
|
|
356
|
+
"Service not found or not MCP-enabled",
|
|
357
|
+
extra={
|
|
358
|
+
"service_id": service_id,
|
|
359
|
+
"correlation_id": str(correlation_id),
|
|
360
|
+
},
|
|
361
|
+
)
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
except (consul.ConsulException, requests.exceptions.RequestException) as e:
|
|
365
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
366
|
+
correlation_id=correlation_id,
|
|
367
|
+
transport_type=EnumInfraTransportType.CONSUL,
|
|
368
|
+
operation="discover_by_service_id",
|
|
369
|
+
target_name="mcp_tool_discovery",
|
|
370
|
+
)
|
|
371
|
+
raise InfraConnectionError(
|
|
372
|
+
f"Failed to look up service from Consul: {e}",
|
|
373
|
+
context=ctx,
|
|
374
|
+
) from e
|
|
375
|
+
|
|
376
|
+
def _is_mcp_orchestrator(self, tags: Sequence[str]) -> bool:
|
|
377
|
+
"""Check if service is an MCP-enabled orchestrator.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
tags: List of service tags from Consul.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
True if service has both mcp-enabled and node-type:orchestrator tags.
|
|
384
|
+
"""
|
|
385
|
+
return self.TAG_MCP_ENABLED in tags and self.TAG_NODE_TYPE_ORCHESTRATOR in tags
|
|
386
|
+
|
|
387
|
+
def _extract_tool_name(self, tags: Sequence[str]) -> str | None:
|
|
388
|
+
"""Extract the MCP tool name from service tags.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
tags: List of service tags from Consul.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
The tool name if found, None otherwise.
|
|
395
|
+
"""
|
|
396
|
+
for tag in tags:
|
|
397
|
+
if tag.startswith(self.TAG_PREFIX_MCP_TOOL):
|
|
398
|
+
return tag[len(self.TAG_PREFIX_MCP_TOOL) :]
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
def describe(self) -> dict[str, object]:
|
|
402
|
+
"""Return service metadata for observability."""
|
|
403
|
+
return {
|
|
404
|
+
"service_name": "ServiceMCPToolDiscovery",
|
|
405
|
+
"consul_host": self._consul_host,
|
|
406
|
+
"consul_port": self._consul_port,
|
|
407
|
+
"consul_scheme": self._consul_scheme,
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
__all__ = ["ServiceMCPToolDiscovery"]
|