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