mcp-mesh 0.5.7__py3-none-any.whl → 0.6.1__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 (57) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/base_injector.py +171 -0
  3. _mcp_mesh/engine/decorator_registry.py +162 -35
  4. _mcp_mesh/engine/dependency_injector.py +105 -19
  5. _mcp_mesh/engine/http_wrapper.py +5 -22
  6. _mcp_mesh/engine/llm_config.py +45 -0
  7. _mcp_mesh/engine/llm_errors.py +115 -0
  8. _mcp_mesh/engine/mesh_llm_agent.py +626 -0
  9. _mcp_mesh/engine/mesh_llm_agent_injector.py +617 -0
  10. _mcp_mesh/engine/provider_handlers/__init__.py +20 -0
  11. _mcp_mesh/engine/provider_handlers/base_provider_handler.py +122 -0
  12. _mcp_mesh/engine/provider_handlers/claude_handler.py +138 -0
  13. _mcp_mesh/engine/provider_handlers/generic_handler.py +156 -0
  14. _mcp_mesh/engine/provider_handlers/openai_handler.py +163 -0
  15. _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +167 -0
  16. _mcp_mesh/engine/response_parser.py +205 -0
  17. _mcp_mesh/engine/signature_analyzer.py +229 -99
  18. _mcp_mesh/engine/tool_executor.py +169 -0
  19. _mcp_mesh/engine/tool_schema_builder.py +126 -0
  20. _mcp_mesh/engine/unified_mcp_proxy.py +14 -12
  21. _mcp_mesh/generated/.openapi-generator/FILES +7 -0
  22. _mcp_mesh/generated/.openapi-generator-ignore +0 -1
  23. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +7 -16
  24. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +7 -0
  25. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +11 -1
  26. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +108 -0
  27. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +95 -0
  28. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +111 -0
  29. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +141 -0
  30. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +93 -0
  31. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +103 -0
  32. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +1 -1
  33. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +35 -1
  34. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +11 -1
  35. _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +112 -0
  36. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +9 -72
  37. _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +3 -3
  38. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +35 -10
  39. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +7 -4
  40. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +260 -0
  41. _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +118 -35
  42. _mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +8 -1
  43. _mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +111 -5
  44. _mcp_mesh/pipeline/mcp_startup/server_discovery.py +77 -48
  45. _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +2 -2
  46. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +2 -2
  47. _mcp_mesh/shared/health_check_cache.py +246 -0
  48. _mcp_mesh/shared/registry_client_wrapper.py +87 -4
  49. _mcp_mesh/utils/fastmcp_schema_extractor.py +476 -0
  50. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.1.dist-info}/METADATA +1 -1
  51. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.1.dist-info}/RECORD +57 -32
  52. mesh/__init__.py +18 -4
  53. mesh/decorators.py +439 -31
  54. mesh/helpers.py +259 -0
  55. mesh/types.py +197 -97
  56. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.1.dist-info}/WHEEL +0 -0
  57. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.1.dist-info}/licenses/LICENSE +0 -0
_mcp_mesh/__init__.py CHANGED
@@ -31,7 +31,7 @@ from .engine.decorator_registry import (
31
31
  get_decorator_stats,
32
32
  )
33
33
 
34
- __version__ = "0.5.7"
34
+ __version__ = "0.6.1"
35
35
 
36
36
  # Store reference to runtime processor if initialized
37
37
  _runtime_processor = None
@@ -0,0 +1,171 @@
1
+ """
2
+ Base injector class with shared wrapper creation logic.
3
+
4
+ Provides common functionality for DependencyInjector and MeshLlmAgentInjector.
5
+ """
6
+
7
+ import functools
8
+ import inspect
9
+ import logging
10
+ import weakref
11
+ from collections.abc import Callable
12
+ from typing import Any
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class BaseInjector:
18
+ """
19
+ Base class for injection systems.
20
+
21
+ Provides shared functionality for creating and managing function wrappers
22
+ that support dynamic injection with two-phase updates.
23
+
24
+ Two-Phase Injection Pattern:
25
+ 1. Phase 1 (decorator time): Create wrapper with initial state (None)
26
+ 2. Phase 2 (runtime): Update wrapper with actual instances via update method
27
+
28
+ Subclasses must implement:
29
+ - Wrapper logic (what to inject and how)
30
+ - Update method signature
31
+ """
32
+
33
+ def __init__(self):
34
+ """Initialize base injector with function registry."""
35
+ self._function_registry: weakref.WeakValueDictionary = (
36
+ weakref.WeakValueDictionary()
37
+ )
38
+ logger.debug(f"🔧 {self.__class__.__name__} initialized")
39
+
40
+ def _register_wrapper(self, function_id: str, wrapper: Callable) -> None:
41
+ """
42
+ Register a wrapper in the function registry.
43
+
44
+ Args:
45
+ function_id: Unique function identifier
46
+ wrapper: Wrapper function to register
47
+ """
48
+ self._function_registry[function_id] = wrapper
49
+ logger.debug(f"🔧 Registered wrapper for {function_id} at {hex(id(wrapper))}")
50
+
51
+ def _create_async_wrapper(
52
+ self,
53
+ func: Callable,
54
+ function_id: str,
55
+ injection_logic: Callable[[Callable, tuple, dict], tuple],
56
+ metadata: dict[str, Any],
57
+ ) -> Callable:
58
+ """
59
+ Create async wrapper with injection logic.
60
+
61
+ Args:
62
+ func: Original async function to wrap
63
+ function_id: Unique function identifier
64
+ injection_logic: Callable that takes (func, args, kwargs) and returns (args, kwargs)
65
+ This function should modify kwargs to inject dependencies
66
+ metadata: Additional metadata to store on wrapper
67
+
68
+ Returns:
69
+ Async wrapper function
70
+ """
71
+
72
+ @functools.wraps(func)
73
+ async def async_wrapper(*args, **kwargs):
74
+ # Apply injection logic to modify kwargs
75
+ args, kwargs = injection_logic(func, args, kwargs)
76
+
77
+ # Execute original function
78
+ return await func(*args, **kwargs)
79
+
80
+ # Store metadata on wrapper
81
+ async_wrapper._mesh_original_func = func
82
+ async_wrapper._mesh_function_id = function_id
83
+
84
+ # Store additional metadata
85
+ for key, value in metadata.items():
86
+ setattr(async_wrapper, key, value)
87
+
88
+ return async_wrapper
89
+
90
+ def _create_sync_wrapper(
91
+ self,
92
+ func: Callable,
93
+ function_id: str,
94
+ injection_logic: Callable[[Callable, tuple, dict], tuple],
95
+ metadata: dict[str, Any],
96
+ ) -> Callable:
97
+ """
98
+ Create sync wrapper with injection logic.
99
+
100
+ Args:
101
+ func: Original sync function to wrap
102
+ function_id: Unique function identifier
103
+ injection_logic: Callable that takes (func, args, kwargs) and returns (args, kwargs)
104
+ This function should modify kwargs to inject dependencies
105
+ metadata: Additional metadata to store on wrapper
106
+
107
+ Returns:
108
+ Sync wrapper function
109
+ """
110
+
111
+ @functools.wraps(func)
112
+ def sync_wrapper(*args, **kwargs):
113
+ # Apply injection logic to modify kwargs
114
+ args, kwargs = injection_logic(func, args, kwargs)
115
+
116
+ # Execute original function
117
+ return func(*args, **kwargs)
118
+
119
+ # Store metadata on wrapper
120
+ sync_wrapper._mesh_original_func = func
121
+ sync_wrapper._mesh_function_id = function_id
122
+
123
+ # Store additional metadata
124
+ for key, value in metadata.items():
125
+ setattr(sync_wrapper, key, value)
126
+
127
+ return sync_wrapper
128
+
129
+ def create_wrapper_with_injection(
130
+ self,
131
+ func: Callable,
132
+ function_id: str,
133
+ injection_logic: Callable[[Callable, tuple, dict], tuple],
134
+ metadata: dict[str, Any],
135
+ register: bool = True,
136
+ ) -> Callable:
137
+ """
138
+ Create wrapper (async or sync) based on function type.
139
+
140
+ This is the main entry point for creating wrappers. It automatically
141
+ detects if the function is async or sync and creates the appropriate wrapper.
142
+
143
+ Args:
144
+ func: Function to wrap
145
+ function_id: Unique function identifier
146
+ injection_logic: Callable that takes (func, args, kwargs) and returns (args, kwargs)
147
+ metadata: Additional metadata to store on wrapper
148
+ register: Whether to register wrapper in function_registry (default: True)
149
+
150
+ Returns:
151
+ Wrapped function with injection capability
152
+ """
153
+ # Detect async vs sync
154
+ is_async = inspect.iscoroutinefunction(func)
155
+
156
+ if is_async:
157
+ wrapper = self._create_async_wrapper(
158
+ func, function_id, injection_logic, metadata
159
+ )
160
+ logger.debug(f"✅ Created async wrapper for {function_id}")
161
+ else:
162
+ wrapper = self._create_sync_wrapper(
163
+ func, function_id, injection_logic, metadata
164
+ )
165
+ logger.debug(f"✅ Created sync wrapper for {function_id}")
166
+
167
+ # Register wrapper if requested
168
+ if register:
169
+ self._register_wrapper(function_id, wrapper)
170
+
171
+ return wrapper
@@ -32,6 +32,18 @@ class DecoratedFunction:
32
32
  self.metadata["function_name"] = self.function.__name__
33
33
 
34
34
 
35
+ @dataclass
36
+ class LLMAgentMetadata:
37
+ """Metadata for a function decorated with @mesh.llm."""
38
+
39
+ function: Callable
40
+ config: dict[str, Any] # LLM configuration (provider, model, filter, etc.)
41
+ output_type: Optional[type] # Pydantic model type from return annotation
42
+ param_name: str # Name of MeshLlmAgent parameter
43
+ function_id: str # Unique function ID for registry
44
+ registered_at: datetime
45
+
46
+
35
47
  class DecoratorRegistry:
36
48
  """
37
49
  Central registry for ALL MCP Mesh decorators.
@@ -52,19 +64,23 @@ class DecoratorRegistry:
52
64
  _mesh_tools: dict[str, DecoratedFunction] = {} # Future use
53
65
  _mesh_resources: dict[str, DecoratedFunction] = {} # Future use
54
66
  _mesh_workflows: dict[str, DecoratedFunction] = {} # Future use
67
+ _mesh_llm_agents: dict[str, "LLMAgentMetadata"] = {} # LLM agents with agentic loop
55
68
 
56
69
  # Registry for new decorator types (extensibility)
57
70
  _custom_decorators: dict[str, dict[str, DecoratedFunction]] = {}
58
-
71
+
59
72
  # Immediate uvicorn server storage (for preventing shutdown state)
60
73
  _immediate_uvicorn_server: Optional[dict[str, Any]] = None
61
-
74
+
62
75
  # FastMCP lifespan storage (for proper integration with FastAPI)
63
76
  _fastmcp_lifespan: Optional[Any] = None
64
-
77
+
65
78
  # FastMCP HTTP app storage (the same app instance whose lifespan was extracted)
66
79
  _fastmcp_http_app: Optional[Any] = None
67
80
 
81
+ # FastMCP server info storage (for schema extraction during heartbeat)
82
+ _fastmcp_server_info: Optional[dict[str, Any]] = None
83
+
68
84
  @classmethod
69
85
  def register_mesh_agent(cls, func: Callable, metadata: dict[str, Any]) -> None:
70
86
  """
@@ -101,11 +117,13 @@ class DecoratorRegistry:
101
117
  if func_name in cls._mesh_tools:
102
118
  old_func = cls._mesh_tools[func_name].function
103
119
  cls._mesh_tools[func_name].function = new_func
104
- print(
120
+ logger.debug(
105
121
  f"🔄 DecoratorRegistry: Updated '{func_name}' from {hex(id(old_func))} to {hex(id(new_func))}"
106
122
  )
107
123
  else:
108
- print(f"⚠️ DecoratorRegistry: Function '{func_name}' not found for update")
124
+ logger.debug(
125
+ f"⚠️ DecoratorRegistry: Function '{func_name}' not found for update"
126
+ )
109
127
 
110
128
  @classmethod
111
129
  def register_mesh_resource(cls, func: Callable, metadata: dict[str, Any]) -> None:
@@ -131,6 +149,53 @@ class DecoratorRegistry:
131
149
 
132
150
  cls._mesh_workflows[func.__name__] = decorated_func
133
151
 
152
+ @classmethod
153
+ def register_mesh_llm(
154
+ cls,
155
+ func: Callable,
156
+ config: dict[str, Any],
157
+ output_type: Optional[type],
158
+ param_name: str,
159
+ function_id: str,
160
+ ) -> None:
161
+ """
162
+ Register a @mesh.llm decorated function.
163
+
164
+ Args:
165
+ func: The decorated function
166
+ config: LLM configuration (provider, model, filter, etc.)
167
+ output_type: Pydantic model type from return annotation
168
+ param_name: Name of MeshLlmAgent parameter
169
+ function_id: Unique function ID for registry
170
+ """
171
+ llm_metadata = LLMAgentMetadata(
172
+ function=func,
173
+ config=config.copy(),
174
+ output_type=output_type,
175
+ param_name=param_name,
176
+ function_id=function_id,
177
+ registered_at=datetime.now(),
178
+ )
179
+
180
+ cls._mesh_llm_agents[function_id] = llm_metadata
181
+ logger.info(
182
+ f"🤖 Registered LLM agent: {func.__name__} (function_id={function_id}, param={param_name}, filter={config.get('filter')}, provider={config.get('provider')})"
183
+ )
184
+
185
+ @classmethod
186
+ def update_mesh_llm_function(cls, function_id: str, new_func: Callable) -> None:
187
+ """Update the function reference for a registered LLM agent (used for wrapper injection)."""
188
+ if function_id in cls._mesh_llm_agents:
189
+ old_func = cls._mesh_llm_agents[function_id].function
190
+ cls._mesh_llm_agents[function_id].function = new_func
191
+ logger.info(
192
+ f"🔄 DecoratorRegistry: Updated LLM function '{function_id}' from {hex(id(old_func))} to {hex(id(new_func))}"
193
+ )
194
+ else:
195
+ logger.warning(
196
+ f"⚠️ DecoratorRegistry: LLM function '{function_id}' not found for update"
197
+ )
198
+
134
199
  @classmethod
135
200
  def register_custom_decorator(
136
201
  cls, decorator_type: str, func: Callable, metadata: dict[str, Any]
@@ -175,6 +240,11 @@ class DecoratorRegistry:
175
240
  """Get all @mesh_workflow decorated functions."""
176
241
  return cls._mesh_workflows.copy()
177
242
 
243
+ @classmethod
244
+ def get_mesh_llm_agents(cls) -> dict[str, LLMAgentMetadata]:
245
+ """Get all @mesh.llm decorated functions."""
246
+ return cls._mesh_llm_agents.copy()
247
+
178
248
  @classmethod
179
249
  def get_all_by_type(cls, decorator_type: str) -> dict[str, DecoratedFunction]:
180
250
  """
@@ -256,6 +326,7 @@ class DecoratorRegistry:
256
326
  cls._mesh_tools.clear()
257
327
  cls._mesh_resources.clear()
258
328
  cls._mesh_workflows.clear()
329
+ cls._mesh_llm_agents.clear()
259
330
  cls._custom_decorators.clear()
260
331
 
261
332
  # Also clear the shared agent ID from mesh.decorators
@@ -290,23 +361,21 @@ class DecoratorRegistry:
290
361
  def update_agent_config(cls, updates: dict[str, Any]) -> None:
291
362
  """
292
363
  Update the cached agent configuration with new values.
293
-
364
+
294
365
  This is useful for API services that generate their agent ID
295
366
  during pipeline execution and need to store it for telemetry.
296
-
367
+
297
368
  Args:
298
369
  updates: Dictionary of config values to update
299
370
  """
300
371
  if cls._cached_agent_config is None:
301
372
  # Initialize with current resolved config if not cached yet
302
373
  cls._cached_agent_config = cls.get_resolved_agent_config().copy()
303
-
374
+
304
375
  # Update with new values
305
376
  cls._cached_agent_config.update(updates)
306
-
307
- logger.debug(
308
- f"🔧 Updated cached agent configuration with: {updates}"
309
- )
377
+
378
+ logger.debug(f"🔧 Updated cached agent configuration with: {updates}")
310
379
 
311
380
  @classmethod
312
381
  def get_resolved_agent_config(cls) -> dict[str, Any]:
@@ -320,7 +389,9 @@ class DecoratorRegistry:
320
389
  dict: Pre-resolved configuration with consistent agent_id
321
390
  """
322
391
  # Step 1: Check if cached configuration already has agent_id (from API pipeline)
323
- if cls._cached_agent_config is not None and cls._cached_agent_config.get('agent_id'):
392
+ if cls._cached_agent_config is not None and cls._cached_agent_config.get(
393
+ "agent_id"
394
+ ):
324
395
  logger.debug(
325
396
  f"🔧 Using cached agent configuration: agent_id='{cls._cached_agent_config.get('agent_id')}'"
326
397
  )
@@ -348,15 +419,16 @@ class DecoratorRegistry:
348
419
  # Check if we're in an API context (have mesh_route decorators)
349
420
  mesh_routes = cls.get_all_by_type("mesh_route")
350
421
  is_api_context = len(mesh_routes) > 0
351
-
422
+
352
423
  if is_api_context:
353
424
  # Use API service ID generation logic for consistency
354
425
  agent_id = cls._generate_api_service_id_fallback()
355
426
  else:
356
427
  # Use standard MCP agent ID generation
357
428
  from mesh.decorators import _get_or_create_agent_id
429
+
358
430
  agent_id = _get_or_create_agent_id()
359
-
431
+
360
432
  fallback_config = {
361
433
  "name": None,
362
434
  "version": get_config_value(
@@ -415,44 +487,44 @@ class DecoratorRegistry:
415
487
  def _generate_api_service_id_fallback(cls) -> str:
416
488
  """
417
489
  Generate API service ID as fallback using same priority logic as API pipeline.
418
-
490
+
419
491
  Priority order:
420
- 1. MCP_MESH_API_NAME environment variable
492
+ 1. MCP_MESH_API_NAME environment variable
421
493
  2. MCP_MESH_AGENT_NAME environment variable (fallback)
422
494
  3. Default to "api-{uuid8}"
423
-
495
+
424
496
  Returns:
425
497
  Generated service ID with UUID suffix matching API service format
426
498
  """
427
499
  import uuid
428
-
500
+
429
501
  from ..shared.config_resolver import ValidationRule, get_config_value
430
-
502
+
431
503
  # Check for API-specific environment variable first (same as API pipeline)
432
504
  api_name = get_config_value(
433
505
  "MCP_MESH_API_NAME",
434
506
  default=None,
435
507
  rule=ValidationRule.STRING_RULE,
436
508
  )
437
-
509
+
438
510
  # Fallback to general agent name env var
439
511
  if not api_name:
440
512
  api_name = get_config_value(
441
- "MCP_MESH_AGENT_NAME",
513
+ "MCP_MESH_AGENT_NAME",
442
514
  default=None,
443
515
  rule=ValidationRule.STRING_RULE,
444
516
  )
445
-
517
+
446
518
  # Clean the service name if provided
447
519
  if api_name:
448
520
  cleaned_name = api_name.lower().replace(" ", "-").replace("_", "-")
449
521
  cleaned_name = "-".join(part for part in cleaned_name.split("-") if part)
450
522
  else:
451
523
  cleaned_name = ""
452
-
524
+
453
525
  # Generate UUID suffix
454
526
  uuid_suffix = str(uuid.uuid4())[:8]
455
-
527
+
456
528
  # Apply same naming logic as API pipeline
457
529
  if not cleaned_name:
458
530
  # No name provided: default to "api-{uuid8}"
@@ -463,8 +535,10 @@ class DecoratorRegistry:
463
535
  else:
464
536
  # Name doesn't contain "api": use "{name}-api-{uuid8}"
465
537
  service_id = f"{cleaned_name}-api-{uuid_suffix}"
466
-
467
- logger.debug(f"Generated fallback API service ID: '{service_id}' from env name: '{api_name}'")
538
+
539
+ logger.debug(
540
+ f"Generated fallback API service ID: '{service_id}' from env name: '{api_name}'"
541
+ )
468
542
  return service_id
469
543
 
470
544
  @classmethod
@@ -521,23 +595,25 @@ class DecoratorRegistry:
521
595
  def store_immediate_uvicorn_server(cls, server_info: dict[str, Any]) -> None:
522
596
  """
523
597
  Store reference to immediate uvicorn server started in decorator.
524
-
598
+
525
599
  Args:
526
600
  server_info: Dictionary containing server information:
527
601
  - 'app': FastAPI app instance
528
602
  - 'host': Server host
529
- - 'port': Server port
603
+ - 'port': Server port
530
604
  - 'thread': Thread object
531
605
  - Any other relevant server metadata
532
606
  """
533
607
  cls._immediate_uvicorn_server = server_info
534
- logger.debug(f"🔄 REGISTRY: Stored immediate uvicorn server reference: {server_info.get('host')}:{server_info.get('port')}")
608
+ logger.debug(
609
+ f"🔄 REGISTRY: Stored immediate uvicorn server reference: {server_info.get('host')}:{server_info.get('port')}"
610
+ )
535
611
 
536
612
  @classmethod
537
613
  def get_immediate_uvicorn_server(cls) -> Optional[dict[str, Any]]:
538
614
  """
539
615
  Get stored immediate uvicorn server reference.
540
-
616
+
541
617
  Returns:
542
618
  Server info dict if available, None otherwise
543
619
  """
@@ -549,11 +625,33 @@ class DecoratorRegistry:
549
625
  cls._immediate_uvicorn_server = None
550
626
  logger.debug("🔄 REGISTRY: Cleared immediate uvicorn server reference")
551
627
 
628
+ # Health check result storage
629
+ _health_check_result: dict | None = None
630
+
631
+ @classmethod
632
+ def store_health_check_result(cls, result: dict) -> None:
633
+ """Store health check result for /health endpoint."""
634
+ cls._health_check_result = result
635
+ logger.debug(
636
+ f"💾 REGISTRY: Stored health check result: {result.get('status', 'unknown')}"
637
+ )
638
+
639
+ @classmethod
640
+ def get_health_check_result(cls) -> dict | None:
641
+ """Get stored health check result."""
642
+ return cls._health_check_result
643
+
644
+ @classmethod
645
+ def clear_health_check_result(cls) -> None:
646
+ """Clear stored health check result."""
647
+ cls._health_check_result = None
648
+ logger.debug("🗑️ REGISTRY: Cleared health check result")
649
+
552
650
  @classmethod
553
651
  def store_fastmcp_lifespan(cls, lifespan: Any) -> None:
554
652
  """
555
653
  Store FastMCP lifespan for integration with FastAPI.
556
-
654
+
557
655
  Args:
558
656
  lifespan: FastMCP lifespan function
559
657
  """
@@ -564,7 +662,7 @@ class DecoratorRegistry:
564
662
  def get_fastmcp_lifespan(cls) -> Optional[Any]:
565
663
  """
566
664
  Get stored FastMCP lifespan.
567
-
665
+
568
666
  Returns:
569
667
  FastMCP lifespan if available, None otherwise
570
668
  """
@@ -580,7 +678,7 @@ class DecoratorRegistry:
580
678
  def store_fastmcp_http_app(cls, http_app: Any) -> None:
581
679
  """
582
680
  Store FastMCP HTTP app (the same instance whose lifespan was extracted).
583
-
681
+
584
682
  Args:
585
683
  http_app: FastMCP HTTP app instance
586
684
  """
@@ -591,7 +689,7 @@ class DecoratorRegistry:
591
689
  def get_fastmcp_http_app(cls) -> Optional[Any]:
592
690
  """
593
691
  Get stored FastMCP HTTP app.
594
-
692
+
595
693
  Returns:
596
694
  FastMCP HTTP app if available, None otherwise
597
695
  """
@@ -603,6 +701,35 @@ class DecoratorRegistry:
603
701
  cls._fastmcp_http_app = None
604
702
  logger.debug("🔄 REGISTRY: Cleared FastMCP HTTP app reference")
605
703
 
704
+ @classmethod
705
+ def store_fastmcp_server_info(cls, server_info: dict[str, Any]) -> None:
706
+ """
707
+ Store FastMCP server info for schema extraction during heartbeat.
708
+
709
+ Args:
710
+ server_info: Dictionary of server_name -> server metadata (including tools)
711
+ """
712
+ cls._fastmcp_server_info = server_info
713
+ logger.debug(
714
+ f"🔄 REGISTRY: Stored FastMCP server info for {len(server_info)} servers"
715
+ )
716
+
717
+ @classmethod
718
+ def get_fastmcp_server_info(cls) -> Optional[dict[str, Any]]:
719
+ """
720
+ Get stored FastMCP server info.
721
+
722
+ Returns:
723
+ FastMCP server info if available, None otherwise
724
+ """
725
+ return cls._fastmcp_server_info
726
+
727
+ @classmethod
728
+ def clear_fastmcp_server_info(cls) -> None:
729
+ """Clear stored FastMCP server info reference."""
730
+ cls._fastmcp_server_info = None
731
+ logger.debug("🔄 REGISTRY: Cleared FastMCP server info reference")
732
+
606
733
 
607
734
  # Convenience functions for external access
608
735
  def get_all_mesh_agents() -> dict[str, DecoratedFunction]: