mcp-mesh 0.5.6__py3-none-any.whl → 0.6.0__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 (40) 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 +136 -33
  4. _mcp_mesh/engine/dependency_injector.py +201 -63
  5. _mcp_mesh/engine/http_wrapper.py +5 -22
  6. _mcp_mesh/engine/llm_config.py +41 -0
  7. _mcp_mesh/engine/llm_errors.py +115 -0
  8. _mcp_mesh/engine/mesh_llm_agent.py +440 -0
  9. _mcp_mesh/engine/mesh_llm_agent_injector.py +487 -0
  10. _mcp_mesh/engine/response_parser.py +240 -0
  11. _mcp_mesh/engine/signature_analyzer.py +225 -86
  12. _mcp_mesh/engine/tool_executor.py +169 -0
  13. _mcp_mesh/engine/tool_schema_builder.py +125 -0
  14. _mcp_mesh/engine/unified_mcp_proxy.py +14 -12
  15. _mcp_mesh/generated/.openapi-generator/FILES +4 -0
  16. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +81 -44
  17. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +72 -35
  18. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +132 -0
  19. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +172 -0
  20. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +92 -0
  21. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +121 -0
  22. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +98 -51
  23. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +93 -44
  24. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +84 -41
  25. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +115 -129
  26. _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +58 -30
  27. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +6 -3
  28. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +222 -0
  29. _mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +7 -0
  30. _mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +65 -4
  31. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +2 -2
  32. _mcp_mesh/shared/registry_client_wrapper.py +60 -4
  33. _mcp_mesh/utils/fastmcp_schema_extractor.py +476 -0
  34. {mcp_mesh-0.5.6.dist-info → mcp_mesh-0.6.0.dist-info}/METADATA +1 -1
  35. {mcp_mesh-0.5.6.dist-info → mcp_mesh-0.6.0.dist-info}/RECORD +40 -26
  36. mesh/__init__.py +8 -4
  37. mesh/decorators.py +344 -2
  38. mesh/types.py +145 -94
  39. {mcp_mesh-0.5.6.dist-info → mcp_mesh-0.6.0.dist-info}/WHEEL +0 -0
  40. {mcp_mesh-0.5.6.dist-info → mcp_mesh-0.6.0.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.6"
34
+ __version__ = "0.6.0"
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
  """
@@ -131,6 +147,53 @@ class DecoratorRegistry:
131
147
 
132
148
  cls._mesh_workflows[func.__name__] = decorated_func
133
149
 
150
+ @classmethod
151
+ def register_mesh_llm(
152
+ cls,
153
+ func: Callable,
154
+ config: dict[str, Any],
155
+ output_type: Optional[type],
156
+ param_name: str,
157
+ function_id: str,
158
+ ) -> None:
159
+ """
160
+ Register a @mesh.llm decorated function.
161
+
162
+ Args:
163
+ func: The decorated function
164
+ config: LLM configuration (provider, model, filter, etc.)
165
+ output_type: Pydantic model type from return annotation
166
+ param_name: Name of MeshLlmAgent parameter
167
+ function_id: Unique function ID for registry
168
+ """
169
+ llm_metadata = LLMAgentMetadata(
170
+ function=func,
171
+ config=config.copy(),
172
+ output_type=output_type,
173
+ param_name=param_name,
174
+ function_id=function_id,
175
+ registered_at=datetime.now(),
176
+ )
177
+
178
+ cls._mesh_llm_agents[function_id] = llm_metadata
179
+ logger.info(
180
+ f"🤖 Registered LLM agent: {func.__name__} (function_id={function_id}, param={param_name}, filter={config.get('filter')}, provider={config.get('provider')})"
181
+ )
182
+
183
+ @classmethod
184
+ def update_mesh_llm_function(cls, function_id: str, new_func: Callable) -> None:
185
+ """Update the function reference for a registered LLM agent (used for wrapper injection)."""
186
+ if function_id in cls._mesh_llm_agents:
187
+ old_func = cls._mesh_llm_agents[function_id].function
188
+ cls._mesh_llm_agents[function_id].function = new_func
189
+ logger.info(
190
+ f"🔄 DecoratorRegistry: Updated LLM function '{function_id}' from {hex(id(old_func))} to {hex(id(new_func))}"
191
+ )
192
+ else:
193
+ logger.warning(
194
+ f"⚠️ DecoratorRegistry: LLM function '{function_id}' not found for update"
195
+ )
196
+
134
197
  @classmethod
135
198
  def register_custom_decorator(
136
199
  cls, decorator_type: str, func: Callable, metadata: dict[str, Any]
@@ -175,6 +238,11 @@ class DecoratorRegistry:
175
238
  """Get all @mesh_workflow decorated functions."""
176
239
  return cls._mesh_workflows.copy()
177
240
 
241
+ @classmethod
242
+ def get_mesh_llm_agents(cls) -> dict[str, LLMAgentMetadata]:
243
+ """Get all @mesh.llm decorated functions."""
244
+ return cls._mesh_llm_agents.copy()
245
+
178
246
  @classmethod
179
247
  def get_all_by_type(cls, decorator_type: str) -> dict[str, DecoratedFunction]:
180
248
  """
@@ -256,6 +324,7 @@ class DecoratorRegistry:
256
324
  cls._mesh_tools.clear()
257
325
  cls._mesh_resources.clear()
258
326
  cls._mesh_workflows.clear()
327
+ cls._mesh_llm_agents.clear()
259
328
  cls._custom_decorators.clear()
260
329
 
261
330
  # Also clear the shared agent ID from mesh.decorators
@@ -290,23 +359,21 @@ class DecoratorRegistry:
290
359
  def update_agent_config(cls, updates: dict[str, Any]) -> None:
291
360
  """
292
361
  Update the cached agent configuration with new values.
293
-
362
+
294
363
  This is useful for API services that generate their agent ID
295
364
  during pipeline execution and need to store it for telemetry.
296
-
365
+
297
366
  Args:
298
367
  updates: Dictionary of config values to update
299
368
  """
300
369
  if cls._cached_agent_config is None:
301
370
  # Initialize with current resolved config if not cached yet
302
371
  cls._cached_agent_config = cls.get_resolved_agent_config().copy()
303
-
372
+
304
373
  # Update with new values
305
374
  cls._cached_agent_config.update(updates)
306
-
307
- logger.debug(
308
- f"🔧 Updated cached agent configuration with: {updates}"
309
- )
375
+
376
+ logger.debug(f"🔧 Updated cached agent configuration with: {updates}")
310
377
 
311
378
  @classmethod
312
379
  def get_resolved_agent_config(cls) -> dict[str, Any]:
@@ -320,7 +387,9 @@ class DecoratorRegistry:
320
387
  dict: Pre-resolved configuration with consistent agent_id
321
388
  """
322
389
  # 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'):
390
+ if cls._cached_agent_config is not None and cls._cached_agent_config.get(
391
+ "agent_id"
392
+ ):
324
393
  logger.debug(
325
394
  f"🔧 Using cached agent configuration: agent_id='{cls._cached_agent_config.get('agent_id')}'"
326
395
  )
@@ -348,15 +417,16 @@ class DecoratorRegistry:
348
417
  # Check if we're in an API context (have mesh_route decorators)
349
418
  mesh_routes = cls.get_all_by_type("mesh_route")
350
419
  is_api_context = len(mesh_routes) > 0
351
-
420
+
352
421
  if is_api_context:
353
422
  # Use API service ID generation logic for consistency
354
423
  agent_id = cls._generate_api_service_id_fallback()
355
424
  else:
356
425
  # Use standard MCP agent ID generation
357
426
  from mesh.decorators import _get_or_create_agent_id
427
+
358
428
  agent_id = _get_or_create_agent_id()
359
-
429
+
360
430
  fallback_config = {
361
431
  "name": None,
362
432
  "version": get_config_value(
@@ -415,44 +485,44 @@ class DecoratorRegistry:
415
485
  def _generate_api_service_id_fallback(cls) -> str:
416
486
  """
417
487
  Generate API service ID as fallback using same priority logic as API pipeline.
418
-
488
+
419
489
  Priority order:
420
- 1. MCP_MESH_API_NAME environment variable
490
+ 1. MCP_MESH_API_NAME environment variable
421
491
  2. MCP_MESH_AGENT_NAME environment variable (fallback)
422
492
  3. Default to "api-{uuid8}"
423
-
493
+
424
494
  Returns:
425
495
  Generated service ID with UUID suffix matching API service format
426
496
  """
427
497
  import uuid
428
-
498
+
429
499
  from ..shared.config_resolver import ValidationRule, get_config_value
430
-
500
+
431
501
  # Check for API-specific environment variable first (same as API pipeline)
432
502
  api_name = get_config_value(
433
503
  "MCP_MESH_API_NAME",
434
504
  default=None,
435
505
  rule=ValidationRule.STRING_RULE,
436
506
  )
437
-
507
+
438
508
  # Fallback to general agent name env var
439
509
  if not api_name:
440
510
  api_name = get_config_value(
441
- "MCP_MESH_AGENT_NAME",
511
+ "MCP_MESH_AGENT_NAME",
442
512
  default=None,
443
513
  rule=ValidationRule.STRING_RULE,
444
514
  )
445
-
515
+
446
516
  # Clean the service name if provided
447
517
  if api_name:
448
518
  cleaned_name = api_name.lower().replace(" ", "-").replace("_", "-")
449
519
  cleaned_name = "-".join(part for part in cleaned_name.split("-") if part)
450
520
  else:
451
521
  cleaned_name = ""
452
-
522
+
453
523
  # Generate UUID suffix
454
524
  uuid_suffix = str(uuid.uuid4())[:8]
455
-
525
+
456
526
  # Apply same naming logic as API pipeline
457
527
  if not cleaned_name:
458
528
  # No name provided: default to "api-{uuid8}"
@@ -463,8 +533,10 @@ class DecoratorRegistry:
463
533
  else:
464
534
  # Name doesn't contain "api": use "{name}-api-{uuid8}"
465
535
  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}'")
536
+
537
+ logger.debug(
538
+ f"Generated fallback API service ID: '{service_id}' from env name: '{api_name}'"
539
+ )
468
540
  return service_id
469
541
 
470
542
  @classmethod
@@ -521,23 +593,25 @@ class DecoratorRegistry:
521
593
  def store_immediate_uvicorn_server(cls, server_info: dict[str, Any]) -> None:
522
594
  """
523
595
  Store reference to immediate uvicorn server started in decorator.
524
-
596
+
525
597
  Args:
526
598
  server_info: Dictionary containing server information:
527
599
  - 'app': FastAPI app instance
528
600
  - 'host': Server host
529
- - 'port': Server port
601
+ - 'port': Server port
530
602
  - 'thread': Thread object
531
603
  - Any other relevant server metadata
532
604
  """
533
605
  cls._immediate_uvicorn_server = server_info
534
- logger.debug(f"🔄 REGISTRY: Stored immediate uvicorn server reference: {server_info.get('host')}:{server_info.get('port')}")
606
+ logger.debug(
607
+ f"🔄 REGISTRY: Stored immediate uvicorn server reference: {server_info.get('host')}:{server_info.get('port')}"
608
+ )
535
609
 
536
610
  @classmethod
537
611
  def get_immediate_uvicorn_server(cls) -> Optional[dict[str, Any]]:
538
612
  """
539
613
  Get stored immediate uvicorn server reference.
540
-
614
+
541
615
  Returns:
542
616
  Server info dict if available, None otherwise
543
617
  """
@@ -553,7 +627,7 @@ class DecoratorRegistry:
553
627
  def store_fastmcp_lifespan(cls, lifespan: Any) -> None:
554
628
  """
555
629
  Store FastMCP lifespan for integration with FastAPI.
556
-
630
+
557
631
  Args:
558
632
  lifespan: FastMCP lifespan function
559
633
  """
@@ -564,7 +638,7 @@ class DecoratorRegistry:
564
638
  def get_fastmcp_lifespan(cls) -> Optional[Any]:
565
639
  """
566
640
  Get stored FastMCP lifespan.
567
-
641
+
568
642
  Returns:
569
643
  FastMCP lifespan if available, None otherwise
570
644
  """
@@ -580,7 +654,7 @@ class DecoratorRegistry:
580
654
  def store_fastmcp_http_app(cls, http_app: Any) -> None:
581
655
  """
582
656
  Store FastMCP HTTP app (the same instance whose lifespan was extracted).
583
-
657
+
584
658
  Args:
585
659
  http_app: FastMCP HTTP app instance
586
660
  """
@@ -591,7 +665,7 @@ class DecoratorRegistry:
591
665
  def get_fastmcp_http_app(cls) -> Optional[Any]:
592
666
  """
593
667
  Get stored FastMCP HTTP app.
594
-
668
+
595
669
  Returns:
596
670
  FastMCP HTTP app if available, None otherwise
597
671
  """
@@ -603,6 +677,35 @@ class DecoratorRegistry:
603
677
  cls._fastmcp_http_app = None
604
678
  logger.debug("🔄 REGISTRY: Cleared FastMCP HTTP app reference")
605
679
 
680
+ @classmethod
681
+ def store_fastmcp_server_info(cls, server_info: dict[str, Any]) -> None:
682
+ """
683
+ Store FastMCP server info for schema extraction during heartbeat.
684
+
685
+ Args:
686
+ server_info: Dictionary of server_name -> server metadata (including tools)
687
+ """
688
+ cls._fastmcp_server_info = server_info
689
+ logger.debug(
690
+ f"🔄 REGISTRY: Stored FastMCP server info for {len(server_info)} servers"
691
+ )
692
+
693
+ @classmethod
694
+ def get_fastmcp_server_info(cls) -> Optional[dict[str, Any]]:
695
+ """
696
+ Get stored FastMCP server info.
697
+
698
+ Returns:
699
+ FastMCP server info if available, None otherwise
700
+ """
701
+ return cls._fastmcp_server_info
702
+
703
+ @classmethod
704
+ def clear_fastmcp_server_info(cls) -> None:
705
+ """Clear stored FastMCP server info reference."""
706
+ cls._fastmcp_server_info = None
707
+ logger.debug("🔄 REGISTRY: Cleared FastMCP server info reference")
708
+
606
709
 
607
710
  # Convenience functions for external access
608
711
  def get_all_mesh_agents() -> dict[str, DecoratedFunction]: